e.stopPropagation()}>
-
-
-
-
- {author &&
}
-
- {props.title || title}
-
-
- {invoiceForm()}
- {error &&
{error}
}
- {payInvoice()}
- {successAction()}
+
+ setCustomAmount(parseInt(e.target.value))}
+ />
+
+
+ );
+ }
+
+ async function payWebLNIfEnabled(invoice: LNURLInvoice) {
+ try {
+ if (webln?.enabled) {
+ let res = await webln.sendPayment(invoice!.pr);
+ console.log(res);
+ setSuccess(invoice!.successAction || {});
+ }
+ } catch (e: any) {
+ setError(e.toString());
+ console.warn(e);
+ }
+ }
+
+ function invoiceForm() {
+ if (invoice) return null;
+ return (
+ <>
+
Zap amount in sats
+
+ {serviceAmounts.map((a) => (
+ selectAmount(a)}
+ >
+ {emojis[a] && <>{emojis[a]} >}
+ {formatShort(a)}
+
+ ))}
+
+ {payService && custom()}
+
+ {(payService?.commentAllowed ?? 0) > 0 && (
+ setComment(e.target.value)}
+ />
+ )}
+
+ {(amount ?? 0) > 0 && (
+
+ )}
+ >
+ );
+ }
+
+ function payInvoice() {
+ if (success) return null;
+ const pr = invoice?.pr;
+ return (
+ <>
+
+ {props.notice &&
{props.notice}}
+
+
+ {pr && (
+ <>
+
+
+
+
+ >
+ )}
-
- )
+
+ >
+ );
+ }
+
+ function successAction() {
+ if (!success) return null;
+ return (
+
+
+
+ {success?.description ?? "Paid!"}
+
+ {success.url && (
+
+
+ {success.url}
+
+
+ )}
+
+ );
+ }
+
+ const defaultTitle = payService?.nostrPubkey ? "Send zap" : "Send sats";
+ const title = target ? `${defaultTitle} to ${target}` : defaultTitle;
+ if (!show) return null;
+ return (
+
+ e.stopPropagation()}>
+
+
+
+
+ {author &&
}
+
{props.title || title}
+
+ {invoiceForm()}
+ {error &&
{error}
}
+ {payInvoice()}
+ {successAction()}
+
+
+ );
}
diff --git a/src/Element/ShowMore.tsx b/src/Element/ShowMore.tsx
index 666e02b5..51027514 100644
--- a/src/Element/ShowMore.tsx
+++ b/src/Element/ShowMore.tsx
@@ -1,20 +1,24 @@
-import './ShowMore.css'
+import "./ShowMore.css";
interface ShowMoreProps {
- text?: string
- className?: string
- onClick: () => void
+ text?: string;
+ className?: string;
+ onClick: () => void;
}
-const ShowMore = ({ text = "Show more", onClick, className = "" }: ShowMoreProps) => {
- const classNames = className ? `show-more ${className}` : "show-more"
+const ShowMore = ({
+ text = "Show more",
+ onClick,
+ className = "",
+}: ShowMoreProps) => {
+ const classNames = className ? `show-more ${className}` : "show-more";
return (
- )
-}
+ );
+};
-export default ShowMore
+export default ShowMore;
diff --git a/src/Element/Skeleton.css b/src/Element/Skeleton.css
index a26348ec..157162c0 100644
--- a/src/Element/Skeleton.css
+++ b/src/Element/Skeleton.css
@@ -1,48 +1,48 @@
-.skeleton {
- display: inline-block;
- height: 1em;
- position: relative;
- overflow: hidden;
- background-color: #dddbdd;
- border-radius: 16px;
-}
-
-.skeleton::after {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- transform: translateX(-100%);
- background-image: linear-gradient(
- 90deg,
- rgba(255, 255, 255, 0) 0,
- rgba(255, 255, 255, 0.2) 20%,
- rgba(255, 255, 255, 0.5) 60%,
- rgba(255, 255, 255, 0)
- );
- animation: shimmer 2s infinite;
- content: "";
-}
-
-@keyframes shimmer {
- 100% {
- transform: translateX(100%);
- }
-}
-
-@media screen and (prefers-color-scheme: dark) {
- .skeleton {
- background-color: #50535a;
- }
-
- .skeleton::after {
- background-image: linear-gradient(
- 90deg,
- #50535a 0%,
- #656871 20%,
- #50535a 40%,
- #50535a 100%
- );
- }
-}
+.skeleton {
+ display: inline-block;
+ height: 1em;
+ position: relative;
+ overflow: hidden;
+ background-color: #dddbdd;
+ border-radius: 16px;
+}
+
+.skeleton::after {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ transform: translateX(-100%);
+ background-image: linear-gradient(
+ 90deg,
+ rgba(255, 255, 255, 0) 0,
+ rgba(255, 255, 255, 0.2) 20%,
+ rgba(255, 255, 255, 0.5) 60%,
+ rgba(255, 255, 255, 0)
+ );
+ animation: shimmer 2s infinite;
+ content: "";
+}
+
+@keyframes shimmer {
+ 100% {
+ transform: translateX(100%);
+ }
+}
+
+@media screen and (prefers-color-scheme: dark) {
+ .skeleton {
+ background-color: #50535a;
+ }
+
+ .skeleton::after {
+ background-image: linear-gradient(
+ 90deg,
+ #50535a 0%,
+ #656871 20%,
+ #50535a 40%,
+ #50535a 100%
+ );
+ }
+}
diff --git a/src/Element/Skeleton.tsx b/src/Element/Skeleton.tsx
index 024919cd..c74e3ef7 100644
--- a/src/Element/Skeleton.tsx
+++ b/src/Element/Skeleton.tsx
@@ -1,30 +1,30 @@
-import "./Skeleton.css";
-
-interface ISkepetonProps {
- children?: React.ReactNode;
- loading?: boolean;
- width?: string;
- height?: string;
- margin?: string;
-}
-
-export default function Skeleton({
- children,
- width,
- height,
- margin,
- loading = true,
-}: ISkepetonProps) {
- if (!loading) {
- return <>{children}>;
- }
-
- return (
-
- {children}
-
- );
-}
+import "./Skeleton.css";
+
+interface ISkepetonProps {
+ children?: React.ReactNode;
+ loading?: boolean;
+ width?: string;
+ height?: string;
+ margin?: string;
+}
+
+export default function Skeleton({
+ children,
+ width,
+ height,
+ margin,
+ loading = true,
+}: ISkepetonProps) {
+ if (!loading) {
+ return <>{children}>;
+ }
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/Element/SoundCloudEmded.tsx b/src/Element/SoundCloudEmded.tsx
index d731563c..a3c6f11a 100644
--- a/src/Element/SoundCloudEmded.tsx
+++ b/src/Element/SoundCloudEmded.tsx
@@ -1,14 +1,13 @@
-const SoundCloudEmbed = ({link}: {link: string}) => {
-
- return(
-
- )
-}
+const SoundCloudEmbed = ({ link }: { link: string }) => {
+ return (
+
+ );
+};
export default SoundCloudEmbed;
diff --git a/src/Element/Tabs.css b/src/Element/Tabs.css
index 35b84ee0..6730f54d 100644
--- a/src/Element/Tabs.css
+++ b/src/Element/Tabs.css
@@ -4,11 +4,11 @@
flex-direction: row;
overflow-x: scroll;
-ms-overflow-style: none; /* for Internet Explorer, Edge */
- scrollbar-width: none; /* Firefox */
+ scrollbar-width: none; /* Firefox */
margin-bottom: 18px;
}
-.tabs::-webkit-scrollbar{
+.tabs::-webkit-scrollbar {
display: none;
}
@@ -31,7 +31,6 @@
color: var(--font-color);
}
-
-.tabs>div {
+.tabs > div {
cursor: pointer;
}
diff --git a/src/Element/Tabs.tsx b/src/Element/Tabs.tsx
index f7855abe..5f7e754e 100644
--- a/src/Element/Tabs.tsx
+++ b/src/Element/Tabs.tsx
@@ -1,39 +1,47 @@
-import './Tabs.css'
+import "./Tabs.css";
export interface Tab {
- text: string, value: number
+ text: string;
+ value: number;
}
interface TabsProps {
- tabs: Tab[]
- tab: Tab
- setTab: (t: Tab) => void
+ tabs: Tab[];
+ tab: Tab;
+ setTab: (t: Tab) => void;
}
-interface TabElementProps extends Omit
{
- t: Tab
+interface TabElementProps extends Omit {
+ t: Tab;
}
export const TabElement = ({ t, tab, setTab }: TabElementProps) => {
return (
- setTab(t)}>
+
setTab(t)}
+ >
{t.text}
- )
-}
+ );
+};
const Tabs = ({ tabs, tab, setTab }: TabsProps) => {
return (
{tabs.map((t) => {
return (
-
setTab(t)}>
+
setTab(t)}
+ >
{t.text}
- )
+ );
})}
- )
-}
+ );
+};
-export default Tabs
+export default Tabs;
diff --git a/src/Element/Text.css b/src/Element/Text.css
index 06204bd9..c46c7fbb 100644
--- a/src/Element/Text.css
+++ b/src/Element/Text.css
@@ -4,70 +4,74 @@
}
.text a {
- color: var(--highlight);
- text-decoration: none;
+ color: var(--highlight);
+ text-decoration: none;
}
.text a:hover {
- text-decoration: underline;
+ text-decoration: underline;
}
.text h1 {
- margin: 0;
+ margin: 0;
}
.text h2 {
- margin: 0;
+ margin: 0;
}
.text h3 {
- margin: 0;
+ margin: 0;
}
.text h4 {
- margin: 0;
+ margin: 0;
}
.text h5 {
- margin: 0;
+ margin: 0;
}
.text h6 {
- margin: 0;
+ margin: 0;
}
.text p {
- margin: 0;
- margin-bottom: 4px;
+ margin: 0;
+ margin-bottom: 4px;
}
.text p:last-child {
- margin-bottom: 0;
+ margin-bottom: 0;
}
.text pre {
- margin: 0;
+ margin: 0;
}
.text li {
- margin-top: -1em;
+ margin-top: -1em;
}
.text li:last-child {
- margin-bottom: -2em;
+ margin-bottom: -2em;
}
.text hr {
- border: 0;
- height: 1px;
- background-image: var(--gray-gradient);
- margin: 20px;
+ border: 0;
+ height: 1px;
+ background-image: var(--gray-gradient);
+ margin: 20px;
}
-.text img, .text video, .text iframe, .text audio {
- max-width: 100%;
- max-height: 500px;
- margin: 10px auto;
- display: block;
- border-radius: 12px;
+.text img,
+.text video,
+.text iframe,
+.text audio {
+ max-width: 100%;
+ max-height: 500px;
+ margin: 10px auto;
+ display: block;
+ border-radius: 12px;
}
-.text iframe, .text video {
- width: -webkit-fill-available;
- aspect-ratio: 16 / 9;
+.text iframe,
+.text video {
+ width: -webkit-fill-available;
+ aspect-ratio: 16 / 9;
}
.text blockquote {
diff --git a/src/Element/Text.tsx b/src/Element/Text.tsx
index 52c1c271..27f19c59 100644
--- a/src/Element/Text.tsx
+++ b/src/Element/Text.tsx
@@ -1,4 +1,4 @@
-import './Text.css'
+import "./Text.css";
import { useMemo, useCallback } from "react";
import { Link } from "react-router-dom";
import ReactMarkdown from "react-markdown";
@@ -12,154 +12,182 @@ import Hashtag from "Element/Hashtag";
import Tag from "Nostr/Tag";
import { MetadataCache } from "State/Users";
import Mention from "Element/Mention";
-import HyperText from 'Element/HyperText';
-import { HexKey } from 'Nostr';
+import HyperText from "Element/HyperText";
+import { HexKey } from "Nostr";
export type Fragment = string | JSX.Element;
export interface TextFragment {
- body: Fragment[],
- tags: Tag[],
- users: Map
+ body: Fragment[];
+ tags: Tag[];
+ users: Map;
}
export interface TextProps {
- content: string,
- creator: HexKey,
- tags: Tag[],
- users: Map
+ content: string;
+ creator: HexKey;
+ tags: Tag[];
+ users: Map;
}
export default function Text({ content, tags, creator, users }: TextProps) {
-
- function extractLinks(fragments: Fragment[]) {
- return fragments.map(f => {
- if (typeof f === "string") {
- return f.split(UrlRegex).map(a => {
- if (a.startsWith("http")) {
- return
- }
- return a;
- });
+ function extractLinks(fragments: Fragment[]) {
+ return fragments
+ .map((f) => {
+ if (typeof f === "string") {
+ return f.split(UrlRegex).map((a) => {
+ if (a.startsWith("http")) {
+ return ;
}
- return f;
- }).flat();
- }
-
- function extractMentions(frag: TextFragment) {
- return frag.body.map(f => {
- if (typeof f === "string") {
- return f.split(MentionRegex).map((match) => {
- let matchTag = match.match(/#\[(\d+)\]/);
- if (matchTag && matchTag.length === 2) {
- let idx = parseInt(matchTag[1]);
- let ref = frag.tags?.find(a => a.Index === idx);
- if (ref) {
- switch (ref.Key) {
- case "p": {
- return
- }
- case "e": {
- let eText = hexToBech32("note", ref.Event!).substring(0, 12);
- return e.stopPropagation()}>#{eText};
- }
- case "t": {
- return
- }
- }
- }
- return {matchTag[0]}?;
- } else {
- return match;
- }
- });
- }
- return f;
- }).flat();
- }
-
- function extractInvoices(fragments: Fragment[]) {
- return fragments.map(f => {
- if (typeof f === "string") {
- return f.split(InvoiceRegex).map(i => {
- if (i.toLowerCase().startsWith("lnbc")) {
- return
- } else {
- return i;
- }
- });
- }
- return f;
- }).flat();
- }
-
- function extractHashtags(fragments: Fragment[]) {
- return fragments.map(f => {
- if (typeof f === "string") {
- return f.split(HashtagRegex).map(i => {
- if (i.toLowerCase().startsWith("#")) {
- return
- } else {
- return i;
- }
- });
- }
- return f;
- }).flat();
- }
-
- function transformLi(frag: TextFragment) {
- let fragments = transformText(frag)
- return {fragments}
- }
-
- function transformParagraph(frag: TextFragment) {
- const fragments = transformText(frag)
- if (fragments.every(f => typeof f === 'string')) {
- return {fragments}
+ return a;
+ });
}
- return <>{fragments}>
- }
+ return f;
+ })
+ .flat();
+ }
- function transformText(frag: TextFragment) {
- if (frag.body === undefined) {
- debugger;
- }
- let fragments = extractMentions(frag);
- fragments = extractLinks(fragments);
- fragments = extractInvoices(fragments);
- fragments = extractHashtags(fragments);
- return fragments;
- }
-
- const components = useMemo(() => {
- return {
- p: (x: any) => transformParagraph({ body: x.children ?? [], tags, users }),
- a: (x: any) => ,
- li: (x: any) => transformLi({ body: x.children ?? [], tags, users }),
- };
- }, [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;
+ function extractMentions(frag: TextFragment) {
+ return frag.body
+ .map((f) => {
+ if (typeof f === "string") {
+ return f.split(MentionRegex).map((match) => {
+ let matchTag = match.match(/#\[(\d+)\]/);
+ if (matchTag && matchTag.length === 2) {
+ let idx = parseInt(matchTag[1]);
+ let ref = frag.tags?.find((a) => a.Index === idx);
+ if (ref) {
+ switch (ref.Key) {
+ case "p": {
+ return ;
+ }
+ case "e": {
+ let eText = hexToBech32("note", ref.Event!).substring(
+ 0,
+ 12
+ );
+ return (
+ e.stopPropagation()}
+ >
+ #{eText}
+
+ );
+ }
+ case "t": {
+ return ;
+ }
+ }
+ }
+ return {matchTag[0]}?;
+ } else {
+ return match;
}
- })
- }, [content]);
- return {content}
+ });
+ }
+ return f;
+ })
+ .flat();
+ }
+
+ function extractInvoices(fragments: Fragment[]) {
+ return fragments
+ .map((f) => {
+ if (typeof f === "string") {
+ return f.split(InvoiceRegex).map((i) => {
+ if (i.toLowerCase().startsWith("lnbc")) {
+ return ;
+ } else {
+ return i;
+ }
+ });
+ }
+ return f;
+ })
+ .flat();
+ }
+
+ function extractHashtags(fragments: Fragment[]) {
+ return fragments
+ .map((f) => {
+ if (typeof f === "string") {
+ return f.split(HashtagRegex).map((i) => {
+ if (i.toLowerCase().startsWith("#")) {
+ return ;
+ } else {
+ return i;
+ }
+ });
+ }
+ return f;
+ })
+ .flat();
+ }
+
+ function transformLi(frag: TextFragment) {
+ let fragments = transformText(frag);
+ return {fragments};
+ }
+
+ function transformParagraph(frag: TextFragment) {
+ const fragments = transformText(frag);
+ if (fragments.every((f) => typeof f === "string")) {
+ return {fragments}
;
+ }
+ return <>{fragments}>;
+ }
+
+ function transformText(frag: TextFragment) {
+ if (frag.body === undefined) {
+ debugger;
+ }
+ let fragments = extractMentions(frag);
+ fragments = extractLinks(fragments);
+ fragments = extractInvoices(fragments);
+ fragments = extractHashtags(fragments);
+ return fragments;
+ }
+
+ const components = useMemo(() => {
+ return {
+ p: (x: any) =>
+ transformParagraph({ body: x.children ?? [], tags, users }),
+ a: (x: any) => ,
+ li: (x: any) => transformLi({ body: x.children ?? [], tags, users }),
+ };
+ }, [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.css b/src/Element/Textarea.css
index d9b1bba6..a54d2b16 100644
--- a/src/Element/Textarea.css
+++ b/src/Element/Textarea.css
@@ -4,12 +4,14 @@
.rta__item:not(:last-child) {
border: none;
}
-.rta__entity--selected .user-item, .rta__entity--selected .emoji-item {
+.rta__entity--selected .user-item,
+.rta__entity--selected .emoji-item {
text-decoration: none;
background: var(--gray-secondary);
}
-.user-item, .emoji-item {
+.user-item,
+.emoji-item {
color: var(--font-color);
background: var(--note-bg);
display: flex;
@@ -19,7 +21,8 @@
padding: 10px;
}
-.user-item:hover, .emoji-item:hover {
+.user-item:hover,
+.emoji-item:hover {
background: var(--gray-tertiary);
}
@@ -37,9 +40,9 @@
}
.user-picture .avatar {
- border-width: 1px;
- width: 40px;
- height: 40px;
+ border-width: 1px;
+ width: 40px;
+ height: 40px;
}
.user-details {
@@ -57,8 +60,8 @@
}
.emoji-item .emoji {
- margin-right: .2em;
- min-width: 20px;
+ margin-right: 0.2em;
+ min-width: 20px;
}
.emoji-item .emoji-name {
diff --git a/src/Element/Textarea.tsx b/src/Element/Textarea.tsx
index fc4103ff..4c352d2a 100644
--- a/src/Element/Textarea.tsx
+++ b/src/Element/Textarea.tsx
@@ -13,8 +13,8 @@ import { MetadataCache } from "State/Users";
import { useQuery } from "State/Users/Hooks";
interface EmojiItemProps {
- name: string
- char: string
+ name: string;
+ char: string;
}
const EmojiItem = ({ entity: { name, char } }: { entity: EmojiItemProps }) => {
@@ -23,11 +23,11 @@ const EmojiItem = ({ entity: { name, char } }: { entity: EmojiItemProps }) => {
{char}
{name}
- )
-}
+ );
+};
const UserItem = (metadata: MetadataCache) => {
- const { pubkey, display_name, picture, nip05, ...rest } = metadata
+ const { pubkey, display_name, picture, nip05, ...rest } = metadata;
return (
@@ -38,24 +38,24 @@ const UserItem = (metadata: MetadataCache) => {
- )
-}
+ );
+};
const Textarea = ({ users, onChange, ...rest }: any) => {
- const [query, setQuery] = useState('')
+ const [query, setQuery] = useState("");
- const allUsers = useQuery(query)
+ const allUsers = useQuery(query);
const userDataProvider = (token: string) => {
- setQuery(token)
- return allUsers
- }
+ setQuery(token);
+ return allUsers;
+ };
const emojiDataProvider = (token: string) => {
return emoji(token)
.slice(0, 5)
.map(({ name, char }) => ({ name, char }));
- }
+ };
return (
{
":": {
dataProvider: emojiDataProvider,
component: EmojiItem,
- output: (item: EmojiItemProps, trigger) => item.char
+ output: (item: EmojiItemProps, trigger) => item.char,
},
"@": {
afterWhitespace: true,
dataProvider: userDataProvider,
component: (props: any) => ,
- output: (item: any) => `@${hexToBech32("npub", item.pubkey)}`
- }
+ output: (item: any) => `@${hexToBech32("npub", item.pubkey)}`,
+ },
}}
/>
- )
-}
+ );
+};
-export default Textarea
+export default Textarea;
diff --git a/src/Element/Thread.css b/src/Element/Thread.css
index d8cef828..11363041 100644
--- a/src/Element/Thread.css
+++ b/src/Element/Thread.css
@@ -63,7 +63,7 @@
}
.subthread-container.subthread-multi .line-container:before {
- content: '';
+ content: "";
position: absolute;
left: 36px;
top: 48px;
@@ -78,7 +78,7 @@
}
.subthread-container.subthread-mid:not(.subthread-last) .line-container:after {
- content: '';
+ content: "";
position: absolute;
left: 36px;
top: 48px;
@@ -87,13 +87,14 @@
}
@media (min-width: 720px) {
- .subthread-container.subthread-mid:not(.subthread-last) .line-container:after {
+ .subthread-container.subthread-mid:not(.subthread-last)
+ .line-container:after {
left: 48px;
}
}
.subthread-container.subthread-mid:not(.subthread-last) .line-container:after {
- content: '';
+ content: "";
position: absolute;
border-left: 1px solid var(--gray-superdark);
left: 36px;
@@ -102,13 +103,14 @@
}
@media (min-width: 720px) {
- .subthread-container.subthread-mid:not(.subthread-last) .line-container:after {
+ .subthread-container.subthread-mid:not(.subthread-last)
+ .line-container:after {
left: 48px;
}
}
.subthread-container.subthread-last .line-container:before {
- content: '';
+ content: "";
position: absolute;
border-left: 1px solid var(--gray-superdark);
left: 36px;
@@ -137,7 +139,8 @@
margin-left: 80px;
}
-.thread-container .collapsed, .thread-container .show-more-container {
+.thread-container .collapsed,
+.thread-container .show-more-container {
background: var(--note-bg);
min-height: 48px;
}
@@ -147,7 +150,7 @@
border-bottom-right-radius: 16px;
}
-.thread-container .collapsed {
+.thread-container .collapsed {
background-color: var(--note-bg);
}
diff --git a/src/Element/Thread.tsx b/src/Element/Thread.tsx
index a4d51497..eac0df0a 100644
--- a/src/Element/Thread.tsx
+++ b/src/Element/Thread.tsx
@@ -13,60 +13,75 @@ import NoteGhost from "Element/NoteGhost";
import Collapsed from "Element/Collapsed";
import type { RootState } from "State/Store";
-function getParent(ev: HexKey, chains: Map): HexKey | undefined {
+function getParent(
+ ev: HexKey,
+ chains: Map
+): HexKey | undefined {
for (let [k, vs] of chains.entries()) {
- const fs = vs.map(a => a.Id)
+ const fs = vs.map((a) => a.Id);
if (fs.includes(ev)) {
- return k
+ return k;
}
}
}
interface DividerProps {
- variant?: "regular" | "small"
+ variant?: "regular" | "small";
}
const Divider = ({ variant = "regular" }: DividerProps) => {
- const className = variant === "small" ? "divider divider-small" : "divider"
+ const className = variant === "small" ? "divider divider-small" : "divider";
return (
- )
-}
+ );
+};
interface SubthreadProps {
- isLastSubthread?: boolean
- from: u256
- active: u256
- path: u256[]
- notes: NEvent[]
- related: TaggedRawEvent[]
- chains: Map
- onNavigate: (e: u256) => void
+ isLastSubthread?: boolean;
+ from: u256;
+ active: u256;
+ path: u256[];
+ notes: NEvent[];
+ related: TaggedRawEvent[];
+ chains: Map;
+ onNavigate: (e: u256) => void;
}
-const Subthread = ({ active, path, from, notes, related, chains, onNavigate }: SubthreadProps) => {
+const Subthread = ({
+ active,
+ path,
+ from,
+ notes,
+ related,
+ chains,
+ onNavigate,
+}: SubthreadProps) => {
const renderSubthread = (a: NEvent, idx: number) => {
- const isLastSubthread = idx === notes.length - 1
- const replies = getReplies(a.Id, chains)
- return (
- <>
- 0 ? 'subthread-multi' : ''}`}>
-
-
-
-
-
- {replies.length > 0 && (
-
+ 0 ? "subthread-multi" : ""
+ }`}
+ >
+
+
+
+
+ {replies.length > 0 && (
+
- )}
- >
- )
- }
+ />
+ )}
+ >
+ );
+ };
- return (
-
- {notes.map(renderSubthread)}
-
- )
+ return {notes.map(renderSubthread)}
;
+};
+
+interface ThreadNoteProps extends Omit {
+ note: NEvent;
+ isLast: boolean;
}
-interface ThreadNoteProps extends Omit {
- note: NEvent
- isLast: boolean
-}
-
-const ThreadNote = ({ active, note, isLast, path, isLastSubthread, from, related, chains, onNavigate }: ThreadNoteProps) => {
- const replies = getReplies(note.Id, chains)
- const activeInReplies = replies.map(r => r.Id).includes(active)
- const [collapsed, setCollapsed] = useState(!activeInReplies)
- const hasMultipleNotes = replies.length > 0
- const isLastVisibleNote = isLastSubthread && isLast && !hasMultipleNotes
- const className = `subthread-container ${isLast && collapsed ? 'subthread-last' : 'subthread-multi subthread-mid'}`
+const ThreadNote = ({
+ active,
+ note,
+ isLast,
+ path,
+ isLastSubthread,
+ from,
+ related,
+ chains,
+ onNavigate,
+}: ThreadNoteProps) => {
+ const replies = getReplies(note.Id, chains);
+ const activeInReplies = replies.map((r) => r.Id).includes(active);
+ const [collapsed, setCollapsed] = useState(!activeInReplies);
+ const hasMultipleNotes = replies.length > 0;
+ const isLastVisibleNote = isLastSubthread && isLast && !hasMultipleNotes;
+ const className = `subthread-container ${
+ isLast && collapsed ? "subthread-last" : "subthread-multi subthread-mid"
+ }`;
return (
<>
- {replies.length > 0 && (
- activeInReplies ? (
+ {replies.length > 0 &&
+ (activeInReplies ? (
) : (
-
+
- )
- )}
+ ))}
>
- )
-}
+ );
+};
-const TierTwo = ({ active, isLastSubthread, path, from, notes, related, chains, onNavigate }: SubthreadProps) => {
- const [first, ...rest] = notes
+const TierTwo = ({
+ active,
+ isLastSubthread,
+ path,
+ from,
+ notes,
+ related,
+ chains,
+ onNavigate,
+}: SubthreadProps) => {
+ const [first, ...rest] = notes;
return (
<>
@@ -163,9 +197,9 @@ const TierTwo = ({ active, isLastSubthread, path, from, notes, related, chains,
/>
{rest.map((r: NEvent, idx: number) => {
- const lastReply = idx === rest.length - 1
+ const lastReply = idx === rest.length - 1;
return (
-
- )
- })
- }
-
+ );
+ })}
>
- )
-}
+ );
+};
-const TierThree = ({ active, path, isLastSubthread, from, notes, related, chains, onNavigate }: SubthreadProps) => {
- const [first, ...rest] = notes
- const replies = getReplies(first.Id, chains)
- const activeInReplies = notes.map(r => r.Id).includes(active) || replies.map(r => r.Id).includes(active)
- const hasMultipleNotes = rest.length > 0 || replies.length > 0
- const isLast = replies.length === 0 && rest.length === 0
+const TierThree = ({
+ active,
+ path,
+ isLastSubthread,
+ from,
+ notes,
+ related,
+ chains,
+ onNavigate,
+}: SubthreadProps) => {
+ const [first, ...rest] = notes;
+ const replies = getReplies(first.Id, chains);
+ const activeInReplies =
+ notes.map((r) => r.Id).includes(active) ||
+ replies.map((r) => r.Id).includes(active);
+ const hasMultipleNotes = rest.length > 0 || replies.length > 0;
+ const isLast = replies.length === 0 && rest.length === 0;
return (
<>
-
+
- {path.length <= 1 || !activeInReplies ? (
- replies.length > 0 && (
-
-
-
- )
- ) : (
- replies.length > 0 && (
-
- )
- )}
+ {path.length <= 1 || !activeInReplies
+ ? replies.length > 0 && (
+
+
+
+ )
+ : replies.length > 0 && (
+
+ )}
{rest.map((r: NEvent, idx: number) => {
- const lastReply = idx === rest.length - 1
- const lastNote = isLastSubthread && lastReply
+ const lastReply = idx === rest.length - 1;
+ const lastNote = isLastSubthread && lastReply;
return (
-
+
- )
- })
- }
-
+ );
+ })}
>
- )
-}
-
+ );
+};
export interface ThreadProps {
- this?: u256,
- notes?: TaggedRawEvent[]
+ this?: u256;
+ notes?: TaggedRawEvent[];
}
export default function Thread(props: ThreadProps) {
- const notes = props.notes ?? [];
- const parsedNotes = notes.map(a => new NEvent(a));
- // root note has no thread info
- const root = useMemo(() => parsedNotes.find(a => a.Thread === null), [notes]);
- const [path, setPath] = useState
([])
- const currentId = path.length > 0 && path[path.length - 1]
- const currentRoot = useMemo(() => parsedNotes.find(a => a.Id === currentId), [notes, currentId]);
- const [navigated, setNavigated] = useState(false)
- const navigate = useNavigate()
- const isSingleNote = parsedNotes.filter(a => a.Kind === EventKind.TextNote).length === 1
- const location = useLocation()
- const urlNoteId = location?.pathname.slice(3)
- const urlNoteHex = urlNoteId && bech32ToHex(urlNoteId)
- const rootNoteId = root && hexToBech32('note', root.Id)
+ const notes = props.notes ?? [];
+ const parsedNotes = notes.map((a) => new NEvent(a));
+ // root note has no thread info
+ const root = useMemo(
+ () => parsedNotes.find((a) => a.Thread === null),
+ [notes]
+ );
+ const [path, setPath] = useState([]);
+ const currentId = path.length > 0 && path[path.length - 1];
+ const currentRoot = useMemo(
+ () => parsedNotes.find((a) => a.Id === currentId),
+ [notes, currentId]
+ );
+ const [navigated, setNavigated] = useState(false);
+ const navigate = useNavigate();
+ const isSingleNote =
+ parsedNotes.filter((a) => a.Kind === EventKind.TextNote).length === 1;
+ const location = useLocation();
+ const urlNoteId = location?.pathname.slice(3);
+ const urlNoteHex = urlNoteId && bech32ToHex(urlNoteId);
+ const rootNoteId = root && hexToBech32("note", root.Id);
- const chains = useMemo(() => {
- let chains = new Map();
- parsedNotes?.filter(a => a.Kind === EventKind.TextNote).sort((a, b) => b.CreatedAt - a.CreatedAt).forEach((v) => {
- let replyTo = v.Thread?.ReplyTo?.Event ?? v.Thread?.Root?.Event;
- if (replyTo) {
- if (!chains.has(replyTo)) {
- chains.set(replyTo, [v]);
- } else {
- chains.get(replyTo)!.push(v);
- }
- } else if (v.Tags.length > 0) {
- console.log("Not replying to anything: ", v);
- }
- });
-
- return chains;
- }, [notes]);
-
- useEffect(() => {
- if (!root) {
- return
- }
-
- if (navigated) {
- return
- }
-
- if (root.Id === urlNoteHex) {
- setPath([root.Id])
- setNavigated(true)
- return
- }
-
- let subthreadPath = []
- let parent = getParent(urlNoteHex, chains)
- while (parent) {
- subthreadPath.unshift(parent)
- parent = getParent(parent, chains)
- }
- setPath(subthreadPath)
- setNavigated(true)
- }, [root, navigated, urlNoteHex, chains])
-
- const brokenChains = useMemo(() => {
- return Array.from(chains?.keys()).filter(a => !parsedNotes?.some(b => b.Id === a));
- }, [chains]);
-
- function renderRoot(note: NEvent) {
- const className = `thread-root ${isSingleNote ? 'thread-root-single' : ''}`
- if (note) {
- return
- } else {
- return (
-
- Loading thread root.. ({notes?.length} notes loaded)
-
- )
+ const chains = useMemo(() => {
+ let chains = new Map();
+ parsedNotes
+ ?.filter((a) => a.Kind === EventKind.TextNote)
+ .sort((a, b) => b.CreatedAt - a.CreatedAt)
+ .forEach((v) => {
+ let replyTo = v.Thread?.ReplyTo?.Event ?? v.Thread?.Root?.Event;
+ if (replyTo) {
+ if (!chains.has(replyTo)) {
+ chains.set(replyTo, [v]);
+ } else {
+ chains.get(replyTo)!.push(v);
+ }
+ } else if (v.Tags.length > 0) {
+ console.log("Not replying to anything: ", v);
}
+ });
+
+ return chains;
+ }, [notes]);
+
+ useEffect(() => {
+ if (!root) {
+ return;
}
- function onNavigate(to: u256) {
- setPath([...path, to])
+ if (navigated) {
+ return;
}
- function renderChain(from: u256): ReactNode {
- if (!from || !chains) {
- return
- }
- let replies = chains.get(from);
- if (replies) {
- return
- }
+ if (root.Id === urlNoteHex) {
+ setPath([root.Id]);
+ setNavigated(true);
+ return;
}
- function goBack() {
- if (path.length > 1) {
- const newPath = path.slice(0, path.length - 1)
- setPath(newPath)
- } else {
- navigate("/")
- }
+ let subthreadPath = [];
+ let parent = getParent(urlNoteHex, chains);
+ while (parent) {
+ subthreadPath.unshift(parent);
+ parent = getParent(parent, chains);
}
+ setPath(subthreadPath);
+ setNavigated(true);
+ }, [root, navigated, urlNoteHex, chains]);
- return (
-
-
1 ? "Parent" : "Back"} />
-
- {currentRoot && renderRoot(currentRoot)}
- {currentRoot && renderChain(currentRoot.Id)}
- {currentRoot === root && (
- <>
- {brokenChains.length > 0 &&
Other replies
}
- {brokenChains.map(a => {
- return (
-
-
- Missing event {a.substring(0, 8)}
-
- {renderChain(a)}
-
- )
- })}
- >
- )}
-
-
+ const brokenChains = useMemo(() => {
+ return Array.from(chains?.keys()).filter(
+ (a) => !parsedNotes?.some((b) => b.Id === a)
);
+ }, [chains]);
+
+ function renderRoot(note: NEvent) {
+ const className = `thread-root ${isSingleNote ? "thread-root-single" : ""}`;
+ if (note) {
+ return (
+
+ );
+ } else {
+ return (
+
+ Loading thread root.. ({notes?.length} notes loaded)
+
+ );
+ }
+ }
+
+ function onNavigate(to: u256) {
+ setPath([...path, to]);
+ }
+
+ function renderChain(from: u256): ReactNode {
+ if (!from || !chains) {
+ return;
+ }
+ let replies = chains.get(from);
+ if (replies) {
+ return (
+
+ );
+ }
+ }
+
+ function goBack() {
+ if (path.length > 1) {
+ const newPath = path.slice(0, path.length - 1);
+ setPath(newPath);
+ } else {
+ navigate("/");
+ }
+ }
+
+ return (
+
+
1 ? "Parent" : "Back"}
+ />
+
+ {currentRoot && renderRoot(currentRoot)}
+ {currentRoot && renderChain(currentRoot.Id)}
+ {currentRoot === root && (
+ <>
+ {brokenChains.length > 0 &&
Other replies
}
+ {brokenChains.map((a) => {
+ return (
+
+
+ Missing event{" "}
+ {a.substring(0, 8)}
+
+ {renderChain(a)}
+
+ );
+ })}
+ >
+ )}
+
+
+ );
}
function getReplies(from: u256, chains?: Map): NEvent[] {
- if (!from || !chains) {
- return []
- }
- let replies = chains.get(from);
- return replies ? replies : []
+ if (!from || !chains) {
+ return [];
+ }
+ let replies = chains.get(from);
+ return replies ? replies : [];
}
-
diff --git a/src/Element/TidalEmbed.tsx b/src/Element/TidalEmbed.tsx
index b97ddf2b..2d25517e 100644
--- a/src/Element/TidalEmbed.tsx
+++ b/src/Element/TidalEmbed.tsx
@@ -4,47 +4,70 @@ import { TidalRegex } from "Const";
// Re-use dom parser across instances of TidalEmbed
const domParser = new DOMParser();
-async function oembedLookup (link: string) {
- // Regex + re-construct to handle both tidal.com/type/id and tidal.com/browse/type/id links.
- const regexResult = TidalRegex.exec(link);
+async function oembedLookup(link: string) {
+ // Regex + re-construct to handle both tidal.com/type/id and tidal.com/browse/type/id links.
+ const regexResult = TidalRegex.exec(link);
- if (!regexResult) {
- return Promise.reject('Not a TIDAL link.');
- }
+ if (!regexResult) {
+ return Promise.reject("Not a TIDAL link.");
+ }
- const [, productType, productId] = regexResult;
- const oembedApi = `https://oembed.tidal.com/?url=https://tidal.com/browse/${productType}/${productId}`;
+ const [, productType, productId] = regexResult;
+ const oembedApi = `https://oembed.tidal.com/?url=https://tidal.com/browse/${productType}/${productId}`;
- const apiResponse = await fetch(oembedApi);
- const json = await apiResponse.json();
+ const apiResponse = await fetch(oembedApi);
+ const json = await apiResponse.json();
- const doc = domParser.parseFromString(json.html, 'text/html');
- const iframe = doc.querySelector('iframe');
+ const doc = domParser.parseFromString(json.html, "text/html");
+ const iframe = doc.querySelector("iframe");
- if (!iframe) {
- return Promise.reject('No iframe delivered.');
- }
+ if (!iframe) {
+ return Promise.reject("No iframe delivered.");
+ }
- return {
- source: iframe.getAttribute('src'),
- height: json.height
- };
+ return {
+ source: iframe.getAttribute("src"),
+ height: json.height,
+ };
}
const TidalEmbed = ({ link }: { link: string }) => {
- const [source, setSource] = useState();
- const [height, setHeight] = useState();
- const extraStyles = link.includes('video') ? { aspectRatio: "16 / 9" } : { height };
+ const [source, setSource] = useState();
+ const [height, setHeight] = useState();
+ const extraStyles = link.includes("video")
+ ? { aspectRatio: "16 / 9" }
+ : { height };
- useEffect(() => {
- oembedLookup(link).then(data => {
- setSource(data.source || undefined);
- setHeight(data.height);
- }).catch(console.error);
- }, [link]);
+ useEffect(() => {
+ oembedLookup(link)
+ .then((data) => {
+ setSource(data.source || undefined);
+ setHeight(data.height);
+ })
+ .catch(console.error);
+ }, [link]);
- if (!source) return e.stopPropagation()} className="ext">{link};
- return ;
-}
+ if (!source)
+ return (
+ e.stopPropagation()}
+ className="ext"
+ >
+ {link}
+
+ );
+ return (
+
+ );
+};
export default TidalEmbed;
diff --git a/src/Element/Timeline.css b/src/Element/Timeline.css
index e0e01fa6..de7af976 100644
--- a/src/Element/Timeline.css
+++ b/src/Element/Timeline.css
@@ -1,5 +1,5 @@
.latest-notes {
- cursor: pointer;
- font-weight: bold;
- user-select: none;
+ cursor: pointer;
+ font-weight: bold;
+ user-select: none;
}
diff --git a/src/Element/Timeline.tsx b/src/Element/Timeline.tsx
index a3223dd5..6d7d65fd 100644
--- a/src/Element/Timeline.tsx
+++ b/src/Element/Timeline.tsx
@@ -15,68 +15,97 @@ import ProfilePreview from "./ProfilePreview";
import Skeleton from "Element/Skeleton";
export interface TimelineProps {
- postsOnly: boolean,
- subject: TimelineSubject,
- method: "TIME_RANGE" | "LIMIT_UNTIL"
- ignoreModeration?: boolean,
- window?: number
+ postsOnly: boolean;
+ subject: TimelineSubject;
+ method: "TIME_RANGE" | "LIMIT_UNTIL";
+ ignoreModeration?: boolean;
+ window?: number;
}
/**
* A list of notes by pubkeys
*/
-export default function Timeline({ subject, postsOnly = false, method, ignoreModeration = false, window }: TimelineProps) {
- const { muted, isMuted } = useModeration();
- const { main, related, latest, parent, loadMore, showLatest } = useTimelineFeed(subject, {
- method,
- window: window
+export default function Timeline({
+ subject,
+ postsOnly = false,
+ method,
+ ignoreModeration = false,
+ window,
+}: TimelineProps) {
+ const { muted, isMuted } = useModeration();
+ const { main, related, latest, parent, loadMore, showLatest } =
+ useTimelineFeed(subject, {
+ method,
+ window: window,
});
- const filterPosts = useCallback((nts: TaggedRawEvent[]) => {
- return [...nts].sort((a, b) => b.created_at - a.created_at)?.filter(a => postsOnly ? !a.tags.some(b => b[0] === "e") : true).filter(a => ignoreModeration || !isMuted(a.pubkey));
- }, [postsOnly, muted]);
+ const filterPosts = useCallback(
+ (nts: TaggedRawEvent[]) => {
+ return [...nts]
+ .sort((a, b) => b.created_at - a.created_at)
+ ?.filter((a) => (postsOnly ? !a.tags.some((b) => b[0] === "e") : true))
+ .filter((a) => ignoreModeration || !isMuted(a.pubkey));
+ },
+ [postsOnly, muted]
+ );
- const mainFeed = useMemo(() => {
- return filterPosts(main.notes);
- }, [main, filterPosts]);
+ const mainFeed = useMemo(() => {
+ return filterPosts(main.notes);
+ }, [main, filterPosts]);
- const latestFeed = useMemo(() => {
- return filterPosts(latest.notes).filter(a => !mainFeed.some(b => b.id === a.id))
- }, [latest, mainFeed, filterPosts]);
-
- function eventElement(e: TaggedRawEvent) {
- switch (e.kind) {
- case EventKind.SetMetadata: {
- return
- }
- case EventKind.TextNote: {
- return
- }
- case EventKind.ZapReceipt: {
- const zap = parseZap(e)
- return zap.e ? null :
- }
- case EventKind.Reaction:
- case EventKind.Repost: {
- let eRef = e.tags.find(a => a[0] === "e")?.at(1);
- return a.id === eRef)} />
- }
- }
- }
-
- return (
-
- {latestFeed.length > 1 && (
showLatest()}>
-
-
- Show latest {latestFeed.length - 1} notes
-
)}
- {mainFeed.map(eventElement)}
-
-
-
-
-
-
+ const latestFeed = useMemo(() => {
+ return filterPosts(latest.notes).filter(
+ (a) => !mainFeed.some((b) => b.id === a.id)
);
+ }, [latest, mainFeed, filterPosts]);
+
+ function eventElement(e: TaggedRawEvent) {
+ switch (e.kind) {
+ case EventKind.SetMetadata: {
+ return ;
+ }
+ case EventKind.TextNote: {
+ return (
+
+ );
+ }
+ case EventKind.ZapReceipt: {
+ const zap = parseZap(e);
+ return zap.e ? null : ;
+ }
+ case EventKind.Reaction:
+ case EventKind.Repost: {
+ let eRef = e.tags.find((a) => a[0] === "e")?.at(1);
+ return (
+ a.id === eRef)}
+ />
+ );
+ }
+ }
+ }
+
+ return (
+
+ {latestFeed.length > 1 && (
+
showLatest()}>
+
+ Show latest {latestFeed.length - 1} notes
+
+ )}
+ {mainFeed.map(eventElement)}
+
+
+
+
+
+
+ );
}
diff --git a/src/Element/UnreadCount.css b/src/Element/UnreadCount.css
index 2fcf75b2..ad10ffbb 100644
--- a/src/Element/UnreadCount.css
+++ b/src/Element/UnreadCount.css
@@ -11,7 +11,7 @@
.pill.unread {
background-color: var(--gray);
- color: var(--font-color);
+ color: var(--font-color);
}
.pill:hover {
diff --git a/src/Element/UnreadCount.tsx b/src/Element/UnreadCount.tsx
index 9685c6b0..86fa08f6 100644
--- a/src/Element/UnreadCount.tsx
+++ b/src/Element/UnreadCount.tsx
@@ -1,11 +1,7 @@
-import "./UnreadCount.css"
+import "./UnreadCount.css";
const UnreadCount = ({ unread }: { unread: number }) => {
- return (
- 0 ? 'unread' : ''}`}>
- {unread}
-
- )
-}
+ return 0 ? "unread" : ""}`}>{unread};
+};
-export default UnreadCount
+export default UnreadCount;
diff --git a/src/Element/Zap.css b/src/Element/Zap.css
index cb66101e..a6bb03e3 100644
--- a/src/Element/Zap.css
+++ b/src/Element/Zap.css
@@ -41,7 +41,7 @@
}
.top-zap .amount:before {
- content: '';
+ content: "";
}
.top-zap .summary {
@@ -66,7 +66,7 @@
}
.top-zap .pfp {
- margin-right: .3em;
+ margin-right: 0.3em;
}
.top-zap .avatar {
diff --git a/src/Element/Zap.tsx b/src/Element/Zap.tsx
index ef1df7e2..9766ebc7 100644
--- a/src/Element/Zap.tsx
+++ b/src/Element/Zap.tsx
@@ -16,28 +16,32 @@ import { RootState } from "State/Store";
function findTag(e: TaggedRawEvent, tag: string) {
const maybeTag = e.tags.find((evTag) => {
- return evTag[0] === tag
- })
- return maybeTag && maybeTag[1]
+ return evTag[0] === tag;
+ });
+ return maybeTag && maybeTag[1];
}
function getInvoice(zap: TaggedRawEvent) {
- const bolt11 = findTag(zap, 'bolt11')
- const decoded = invoiceDecode(bolt11)
+ const bolt11 = findTag(zap, "bolt11");
+ const decoded = invoiceDecode(bolt11);
- const amount = decoded.sections.find((section: any) => section.name === 'amount')?.value
- const hash = decoded.sections.find((section: any) => section.name === 'description_hash')?.value;
+ const amount = decoded.sections.find(
+ (section: any) => section.name === "amount"
+ )?.value;
+ const hash = decoded.sections.find(
+ (section: any) => section.name === "description_hash"
+ )?.value;
return { amount, hash: hash ? bytesToHex(hash) : undefined };
}
interface Zapper {
- pubkey?: HexKey,
- isValid: boolean
+ pubkey?: HexKey;
+ isValid: boolean;
}
function getZapper(zap: TaggedRawEvent, dhash: string): Zapper {
- const zapRequest = findTag(zap, 'description')
+ const zapRequest = findTag(zap, "description");
if (zapRequest) {
const rawEvent: TaggedRawEvent = JSON.parse(zapRequest);
if (Array.isArray(rawEvent)) {
@@ -45,27 +49,27 @@ function getZapper(zap: TaggedRawEvent, dhash: string): Zapper {
return { isValid: false };
}
const metaHash = sha256(zapRequest);
- const ev = new Event(rawEvent)
+ const ev = new Event(rawEvent);
return { pubkey: ev.PubKey, isValid: dhash === metaHash };
}
- return { isValid: false }
+ return { isValid: false };
}
interface ParsedZap {
- id: HexKey
- e?: HexKey
- p: HexKey
- amount: number
- content: string
- zapper?: HexKey
- valid: boolean
+ id: HexKey;
+ e?: HexKey;
+ p: HexKey;
+ amount: number;
+ content: string;
+ zapper?: HexKey;
+ valid: boolean;
}
export function parseZap(zap: TaggedRawEvent): ParsedZap {
- const { amount, hash } = getInvoice(zap)
+ const { amount, hash } = getInvoice(zap);
const zapper = hash ? getZapper(zap, hash) : { isValid: false };
- const e = findTag(zap, 'e')
- const p = findTag(zap, 'p')!
+ const e = findTag(zap, "e");
+ const p = findTag(zap, "p")!;
return {
id: zap.id,
e,
@@ -74,12 +78,18 @@ export function parseZap(zap: TaggedRawEvent): ParsedZap {
zapper: zapper.pubkey,
content: zap.content,
valid: zapper.isValid,
- }
+ };
}
-const Zap = ({ zap, showZapped = true }: { zap: ParsedZap, showZapped?: boolean }) => {
- const { amount, content, zapper, valid, p } = zap
- const pubKey = useSelector((s: RootState) => s.login.publicKey)
+const Zap = ({
+ zap,
+ showZapped = true,
+}: {
+ zap: ParsedZap;
+ showZapped?: boolean;
+}) => {
+ const { amount, content, zapper, valid, p } = zap;
+ const pubKey = useSelector((s: RootState) => s.login.publicKey);
return valid ? (
@@ -99,26 +109,28 @@ const Zap = ({ zap, showZapped = true }: { zap: ParsedZap, showZapped?: boolean
/>
- ) : null
-}
+ ) : null;
+};
-interface ZapsSummaryProps { zaps: ParsedZap[] }
+interface ZapsSummaryProps {
+ zaps: ParsedZap[];
+}
export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
const sortedZaps = useMemo(() => {
- const pub = [...zaps.filter(z => z.zapper)]
- const priv = [...zaps.filter(z => !z.zapper)]
- pub.sort((a, b) => b.amount - a.amount)
- return pub.concat(priv)
- }, [zaps])
+ const pub = [...zaps.filter((z) => z.zapper)];
+ const priv = [...zaps.filter((z) => !z.zapper)];
+ pub.sort((a, b) => b.amount - a.amount);
+ return pub.concat(priv);
+ }, [zaps]);
if (zaps.length === 0) {
- return null
+ return null;
}
- const [topZap, ...restZaps] = sortedZaps
- const restZapsTotal = restZaps.reduce((acc, z) => acc + z.amount, 0)
- const { zapper, amount, content, valid } = topZap
+ const [topZap, ...restZaps] = sortedZaps;
+ const restZapsTotal = restZaps.reduce((acc, z) => acc + z.amount, 0);
+ const { zapper, amount, content, valid } = topZap;
return (
@@ -127,14 +139,16 @@ export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
{zapper &&
}
{restZaps.length > 0 && (
-
and {restZaps.length} other{restZaps.length > 1 ? 's' : ''}
+
+ and {restZaps.length} other{restZaps.length > 1 ? "s" : ""}
+
)}
zapped
)}
- )
-}
+ );
+};
-export default Zap
+export default Zap;
diff --git a/src/Element/ZapButton.tsx b/src/Element/ZapButton.tsx
index b3022d8f..41d2f2e9 100644
--- a/src/Element/ZapButton.tsx
+++ b/src/Element/ZapButton.tsx
@@ -6,22 +6,27 @@ import { useUserProfile } from "Feed/ProfileFeed";
import { HexKey } from "Nostr";
import SendSats from "Element/SendSats";
+const ZapButton = ({ pubkey, svc }: { pubkey?: HexKey; svc?: string }) => {
+ const profile = useUserProfile(pubkey!);
+ const [zap, setZap] = useState(false);
+ const service = svc ?? (profile?.lud16 || profile?.lud06);
-const ZapButton = ({ pubkey, svc }: { pubkey?: HexKey, svc?: string }) => {
- const profile = useUserProfile(pubkey!)
- const [zap, setZap] = useState(false);
- const service = svc ?? (profile?.lud16 || profile?.lud06);
+ if (!service) return null;
- if (!service) return null;
-
- return (
- <>
- setZap(true)}>
-
-
- setZap(false)} author={pubkey} />
- >
- )
-}
+ return (
+ <>
+ setZap(true)}>
+
+
+ setZap(false)}
+ author={pubkey}
+ />
+ >
+ );
+};
export default ZapButton;
diff --git a/src/Feed/EventPublisher.ts b/src/Feed/EventPublisher.ts
index 58dbdc70..bf31d12d 100644
--- a/src/Feed/EventPublisher.ts
+++ b/src/Feed/EventPublisher.ts
@@ -6,342 +6,371 @@ import EventKind from "Nostr/EventKind";
import Tag from "Nostr/Tag";
import { RootState } from "State/Store";
import { HexKey, RawEvent, u256, UserMetadata, Lists } from "Nostr";
-import { bech32ToHex } from "Util"
+import { bech32ToHex } from "Util";
import { DefaultRelays, HashtagRegex } from "Const";
import { RelaySettings } from "Nostr/Connection";
declare global {
- interface Window {
- nostr: {
- getPublicKey: () => Promise,
- signEvent: (event: RawEvent) => Promise,
- getRelays: () => Promise>,
- nip04: {
- encrypt: (pubkey: HexKey, content: string) => Promise,
- decrypt: (pubkey: HexKey, content: string) => Promise
- }
- }
- }
+ interface Window {
+ nostr: {
+ getPublicKey: () => Promise;
+ signEvent: (event: RawEvent) => Promise;
+ getRelays: () => Promise<
+ Record
+ >;
+ nip04: {
+ encrypt: (pubkey: HexKey, content: string) => Promise;
+ decrypt: (pubkey: HexKey, content: string) => Promise;
+ };
+ };
+ }
}
export default function useEventPublisher() {
- const pubKey = useSelector(s => s.login.publicKey);
- const privKey = useSelector(s => s.login.privateKey);
- const follows = useSelector(s => s.login.follows);
- const relays = useSelector((s: RootState) => s.login.relays);
- const hasNip07 = 'nostr' in window;
+ const pubKey = useSelector(
+ (s) => s.login.publicKey
+ );
+ const privKey = useSelector(
+ (s) => s.login.privateKey
+ );
+ const follows = useSelector((s) => s.login.follows);
+ const relays = useSelector((s: RootState) => s.login.relays);
+ const hasNip07 = "nostr" in window;
- async function signEvent(ev: NEvent): Promise {
- if (hasNip07 && !privKey) {
- ev.Id = await ev.CreateId();
- let tmpEv = await barierNip07(() => window.nostr.signEvent(ev.ToObject()));
- return new NEvent(tmpEv);
- } else if (privKey) {
- await ev.Sign(privKey);
- } else {
- console.warn("Count not sign event, no private keys available");
- }
- return ev;
+ async function signEvent(ev: NEvent): Promise {
+ if (hasNip07 && !privKey) {
+ ev.Id = await ev.CreateId();
+ let tmpEv = await barierNip07(() =>
+ window.nostr.signEvent(ev.ToObject())
+ );
+ return new NEvent(tmpEv);
+ } else if (privKey) {
+ await ev.Sign(privKey);
+ } else {
+ console.warn("Count not sign event, no private keys available");
}
+ return ev;
+ }
- function processContent(ev: NEvent, msg: string) {
- const replaceNpub = (match: string) => {
- const npub = match.slice(1);
- try {
- const hex = bech32ToHex(npub);
- const idx = ev.Tags.length;
- ev.Tags.push(new Tag(["p", hex], idx));
- return `#[${idx}]`
- } catch (error) {
- return match
- }
+ function processContent(ev: NEvent, msg: string) {
+ const replaceNpub = (match: string) => {
+ const npub = match.slice(1);
+ try {
+ const hex = bech32ToHex(npub);
+ const idx = ev.Tags.length;
+ ev.Tags.push(new Tag(["p", hex], idx));
+ return `#[${idx}]`;
+ } catch (error) {
+ return match;
+ }
+ };
+ const replaceNoteId = (match: string) => {
+ try {
+ const hex = bech32ToHex(match);
+ const idx = ev.Tags.length;
+ ev.Tags.push(new Tag(["e", hex, "", "mention"], idx));
+ return `#[${idx}]`;
+ } catch (error) {
+ return match;
+ }
+ };
+ const replaceHashtag = (match: string) => {
+ const tag = match.slice(1);
+ const idx = ev.Tags.length;
+ ev.Tags.push(new Tag(["t", tag.toLowerCase()], idx));
+ return match;
+ };
+ const content = msg
+ .replace(/@npub[a-z0-9]+/g, replaceNpub)
+ .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);
+ System.BroadcastEvent(ev);
+ }
+ },
+ /**
+ * Write event to DefaultRelays, this is important for profiles / relay lists to prevent bugs
+ * If a user removes all the DefaultRelays from their relay list and saves that relay list,
+ * 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) {
+ System.WriteOnceToRelay(k, ev);
}
- const replaceNoteId = (match: string) => {
- try {
- const hex = bech32ToHex(match);
- const idx = ev.Tags.length;
- ev.Tags.push(new Tag(["e", hex, "", "mention"], idx));
- return `#[${idx}]`
- } catch (error) {
- return match
- }
+ }
+ },
+ muted: async (keys: HexKey[], priv: HexKey[]) => {
+ if (pubKey) {
+ let ev = NEvent.ForPubKey(pubKey);
+ ev.Kind = EventKind.Lists;
+ ev.Tags.push(new Tag(["d", Lists.Muted], ev.Tags.length));
+ keys.forEach((p) => {
+ ev.Tags.push(new Tag(["p", p], ev.Tags.length));
+ });
+ let content = "";
+ if (priv.length > 0) {
+ const ps = priv.map((p) => ["p", p]);
+ const plaintext = JSON.stringify(ps);
+ if (hasNip07 && !privKey) {
+ content = await barierNip07(() =>
+ window.nostr.nip04.encrypt(pubKey, plaintext)
+ );
+ } else if (privKey) {
+ content = await ev.EncryptData(plaintext, pubKey, privKey);
+ }
}
- const replaceHashtag = (match: string) => {
- const tag = match.slice(1);
- const idx = ev.Tags.length;
- ev.Tags.push(new Tag(["t", tag.toLowerCase()], idx));
- return match;
- }
- const content = msg.replace(/@npub[a-z0-9]+/g, replaceNpub)
- .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);
- System.BroadcastEvent(ev);
- }
- },
- /**
- * Write event to DefaultRelays, this is important for profiles / relay lists to prevent bugs
- * If a user removes all the DefaultRelays from their relay list and saves that relay list,
- * 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) {
- System.WriteOnceToRelay(k, ev);
- }
- }
- },
- muted: async (keys: HexKey[], priv: HexKey[]) => {
- if (pubKey) {
- let ev = NEvent.ForPubKey(pubKey);
- ev.Kind = EventKind.Lists;
- ev.Tags.push(new Tag(["d", Lists.Muted], ev.Tags.length))
- keys.forEach(p => {
- ev.Tags.push(new Tag(["p", p], ev.Tags.length))
- })
- let content = ""
- if (priv.length > 0) {
- const ps = priv.map(p => ["p", p])
- const plaintext = JSON.stringify(ps)
- if (hasNip07 && !privKey) {
- content = await barierNip07(() => window.nostr.nip04.encrypt(pubKey, plaintext));
- } else if (privKey) {
- content = await ev.EncryptData(plaintext, pubKey, privKey)
- }
- }
- ev.Content = content;
- return await signEvent(ev);
- }
- },
- metadata: async (obj: UserMetadata) => {
- if (pubKey) {
- let ev = NEvent.ForPubKey(pubKey);
- ev.Kind = EventKind.SetMetadata;
- ev.Content = JSON.stringify(obj);
- return await signEvent(ev);
- }
- },
- note: async (msg: string) => {
- if (pubKey) {
- let ev = NEvent.ForPubKey(pubKey);
- ev.Kind = EventKind.TextNote;
- processContent(ev, msg);
- return await signEvent(ev);
- }
- },
- zap: async (author: HexKey, note?: HexKey, msg?: string) => {
- if (pubKey) {
- let ev = NEvent.ForPubKey(pubKey);
- ev.Kind = EventKind.ZapRequest;
- if (note) {
- // @ts-ignore
- ev.Tags.push(new Tag(["e", note]))
- }
- // @ts-ignore
- ev.Tags.push(new Tag(["p", author]))
- // @ts-ignore
- const relayTag = ['relays', ...Object.keys(relays).slice(0, 10)]
- // @ts-ignore
- ev.Tags.push(new Tag(relayTag))
- processContent(ev, msg || '');
- return await signEvent(ev);
- }
- },
- /**
- * Reply to a note
- */
- reply: async (replyTo: NEvent, msg: string) => {
- if (pubKey) {
- let ev = NEvent.ForPubKey(pubKey);
- ev.Kind = EventKind.TextNote;
-
- let thread = replyTo.Thread;
- if (thread) {
- if (thread.Root || thread.ReplyTo) {
- ev.Tags.push(new Tag(["e", thread.Root?.Event ?? thread.ReplyTo?.Event!, "", "root"], ev.Tags.length));
- }
- ev.Tags.push(new Tag(["e", replyTo.Id, "", "reply"], ev.Tags.length));
-
- // dont tag self in replies
- if (replyTo.PubKey !== pubKey) {
- ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length));
- }
-
- for (let pk of thread.PubKeys) {
- if (pk === pubKey) {
- continue; // dont tag self in replies
- }
- ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
- }
- } else {
- ev.Tags.push(new Tag(["e", replyTo.Id, "", "reply"], 0));
- // dont tag self in replies
- if (replyTo.PubKey !== pubKey) {
- ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length));
- }
- }
- processContent(ev, msg);
- return await signEvent(ev);
- }
- },
- react: async (evRef: NEvent, content = "+") => {
- if (pubKey) {
- let ev = NEvent.ForPubKey(pubKey);
- ev.Kind = EventKind.Reaction;
- ev.Content = content;
- ev.Tags.push(new Tag(["e", evRef.Id], 0));
- ev.Tags.push(new Tag(["p", evRef.PubKey], 1));
- return await signEvent(ev);
- }
- },
- saveRelays: async () => {
- if (pubKey) {
- let ev = NEvent.ForPubKey(pubKey);
- ev.Kind = EventKind.ContactList;
- ev.Content = JSON.stringify(relays);
- for (let pk of follows) {
- ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
- }
-
- return await signEvent(ev);
- }
- },
- addFollow: async (pkAdd: HexKey | HexKey[], newRelays?: Record) => {
- if (pubKey) {
- let ev = NEvent.ForPubKey(pubKey);
- ev.Kind = EventKind.ContactList;
- ev.Content = JSON.stringify(newRelays ?? relays);
- let temp = new Set(follows);
- if (Array.isArray(pkAdd)) {
- pkAdd.forEach(a => temp.add(a));
- } else {
- temp.add(pkAdd);
- }
- for (let pk of temp) {
- if (pk.length !== 64) {
- continue;
- }
- ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
- }
-
- return await signEvent(ev);
- }
- },
- removeFollow: async (pkRemove: HexKey) => {
- if (pubKey) {
- let ev = NEvent.ForPubKey(pubKey);
- ev.Kind = EventKind.ContactList;
- ev.Content = JSON.stringify(relays);
- for (let pk of follows) {
- if (pk === pkRemove || pk.length !== 64) {
- continue;
- }
- ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
- }
-
- return await signEvent(ev);
- }
- },
- /**
- * Delete an event (NIP-09)
- */
- delete: async (id: u256) => {
- if (pubKey) {
- let ev = NEvent.ForPubKey(pubKey);
- ev.Kind = EventKind.Deletion;
- ev.Content = "";
- ev.Tags.push(new Tag(["e", id], 0));
- return await signEvent(ev);
- }
- },
- /**
- * Respot a note (NIP-18)
- */
- repost: async (note: NEvent) => {
- if (pubKey) {
- let ev = NEvent.ForPubKey(pubKey);
- ev.Kind = EventKind.Repost;
- ev.Content = JSON.stringify(note.Original);
- ev.Tags.push(new Tag(["e", note.Id], 0));
- ev.Tags.push(new Tag(["p", note.PubKey], 1));
- return await signEvent(ev);
- }
- },
- decryptDm: async (note: NEvent): Promise => {
- if (pubKey) {
- if (note.PubKey !== pubKey && !note.Tags.some(a => a.PubKey === pubKey)) {
- return "";
- }
- try {
- let otherPubKey = note.PubKey === pubKey ? note.Tags.filter(a => a.Key === "p")[0].PubKey! : note.PubKey;
- if (hasNip07 && !privKey) {
- return await barierNip07(() => window.nostr.nip04.decrypt(otherPubKey, note.Content));
- } else if (privKey) {
- await note.DecryptDm(privKey, otherPubKey);
- return note.Content;
- }
- } catch (e) {
- console.error("Decyrption failed", e);
- return "";
- }
- }
- },
- sendDm: async (content: string, to: HexKey) => {
- if (pubKey) {
- let ev = NEvent.ForPubKey(pubKey);
- ev.Kind = EventKind.DirectMessage;
- ev.Content = content;
- ev.Tags.push(new Tag(["p", to], 0));
-
- try {
- if (hasNip07 && !privKey) {
- let cx: string = await barierNip07(() => window.nostr.nip04.encrypt(to, content));
- ev.Content = cx;
- return await signEvent(ev);
- } else if (privKey) {
- await ev.EncryptDmForPubkey(to, privKey);
- return await signEvent(ev);
- }
- } catch (e) {
- console.error("Encryption failed", e);
- }
- }
+ return await signEvent(ev);
+ }
+ },
+ metadata: async (obj: UserMetadata) => {
+ if (pubKey) {
+ let ev = NEvent.ForPubKey(pubKey);
+ ev.Kind = EventKind.SetMetadata;
+ ev.Content = JSON.stringify(obj);
+ return await signEvent(ev);
+ }
+ },
+ note: async (msg: string) => {
+ if (pubKey) {
+ let ev = NEvent.ForPubKey(pubKey);
+ ev.Kind = EventKind.TextNote;
+ processContent(ev, msg);
+ return await signEvent(ev);
+ }
+ },
+ zap: async (author: HexKey, note?: HexKey, msg?: string) => {
+ if (pubKey) {
+ let ev = NEvent.ForPubKey(pubKey);
+ ev.Kind = EventKind.ZapRequest;
+ if (note) {
+ // @ts-ignore
+ ev.Tags.push(new Tag(["e", note]));
}
- }
+ // @ts-ignore
+ ev.Tags.push(new Tag(["p", author]));
+ // @ts-ignore
+ const relayTag = ["relays", ...Object.keys(relays).slice(0, 10)];
+ // @ts-ignore
+ ev.Tags.push(new Tag(relayTag));
+ processContent(ev, msg || "");
+ return await signEvent(ev);
+ }
+ },
+ /**
+ * Reply to a note
+ */
+ reply: async (replyTo: NEvent, msg: string) => {
+ if (pubKey) {
+ let ev = NEvent.ForPubKey(pubKey);
+ ev.Kind = EventKind.TextNote;
+
+ let thread = replyTo.Thread;
+ if (thread) {
+ if (thread.Root || thread.ReplyTo) {
+ ev.Tags.push(
+ new Tag(
+ ["e", thread.Root?.Event ?? thread.ReplyTo?.Event!, "", "root"],
+ ev.Tags.length
+ )
+ );
+ }
+ ev.Tags.push(new Tag(["e", replyTo.Id, "", "reply"], ev.Tags.length));
+
+ // dont tag self in replies
+ if (replyTo.PubKey !== pubKey) {
+ ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length));
+ }
+
+ for (let pk of thread.PubKeys) {
+ if (pk === pubKey) {
+ continue; // dont tag self in replies
+ }
+ ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
+ }
+ } else {
+ ev.Tags.push(new Tag(["e", replyTo.Id, "", "reply"], 0));
+ // dont tag self in replies
+ if (replyTo.PubKey !== pubKey) {
+ ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length));
+ }
+ }
+ processContent(ev, msg);
+ return await signEvent(ev);
+ }
+ },
+ react: async (evRef: NEvent, content = "+") => {
+ if (pubKey) {
+ let ev = NEvent.ForPubKey(pubKey);
+ ev.Kind = EventKind.Reaction;
+ ev.Content = content;
+ ev.Tags.push(new Tag(["e", evRef.Id], 0));
+ ev.Tags.push(new Tag(["p", evRef.PubKey], 1));
+ return await signEvent(ev);
+ }
+ },
+ saveRelays: async () => {
+ if (pubKey) {
+ let ev = NEvent.ForPubKey(pubKey);
+ ev.Kind = EventKind.ContactList;
+ ev.Content = JSON.stringify(relays);
+ for (let pk of follows) {
+ ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
+ }
+
+ return await signEvent(ev);
+ }
+ },
+ addFollow: async (
+ pkAdd: HexKey | HexKey[],
+ newRelays?: Record
+ ) => {
+ if (pubKey) {
+ let ev = NEvent.ForPubKey(pubKey);
+ ev.Kind = EventKind.ContactList;
+ ev.Content = JSON.stringify(newRelays ?? relays);
+ let temp = new Set(follows);
+ if (Array.isArray(pkAdd)) {
+ pkAdd.forEach((a) => temp.add(a));
+ } else {
+ temp.add(pkAdd);
+ }
+ for (let pk of temp) {
+ if (pk.length !== 64) {
+ continue;
+ }
+ ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
+ }
+
+ return await signEvent(ev);
+ }
+ },
+ removeFollow: async (pkRemove: HexKey) => {
+ if (pubKey) {
+ let ev = NEvent.ForPubKey(pubKey);
+ ev.Kind = EventKind.ContactList;
+ ev.Content = JSON.stringify(relays);
+ for (let pk of follows) {
+ if (pk === pkRemove || pk.length !== 64) {
+ continue;
+ }
+ ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
+ }
+
+ return await signEvent(ev);
+ }
+ },
+ /**
+ * Delete an event (NIP-09)
+ */
+ delete: async (id: u256) => {
+ if (pubKey) {
+ let ev = NEvent.ForPubKey(pubKey);
+ ev.Kind = EventKind.Deletion;
+ ev.Content = "";
+ ev.Tags.push(new Tag(["e", id], 0));
+ return await signEvent(ev);
+ }
+ },
+ /**
+ * Respot a note (NIP-18)
+ */
+ repost: async (note: NEvent) => {
+ if (pubKey) {
+ let ev = NEvent.ForPubKey(pubKey);
+ ev.Kind = EventKind.Repost;
+ ev.Content = JSON.stringify(note.Original);
+ ev.Tags.push(new Tag(["e", note.Id], 0));
+ ev.Tags.push(new Tag(["p", note.PubKey], 1));
+ return await signEvent(ev);
+ }
+ },
+ decryptDm: async (note: NEvent): Promise => {
+ if (pubKey) {
+ if (
+ note.PubKey !== pubKey &&
+ !note.Tags.some((a) => a.PubKey === pubKey)
+ ) {
+ return "";
+ }
+ try {
+ let otherPubKey =
+ note.PubKey === pubKey
+ ? note.Tags.filter((a) => a.Key === "p")[0].PubKey!
+ : note.PubKey;
+ if (hasNip07 && !privKey) {
+ return await barierNip07(() =>
+ window.nostr.nip04.decrypt(otherPubKey, note.Content)
+ );
+ } else if (privKey) {
+ await note.DecryptDm(privKey, otherPubKey);
+ return note.Content;
+ }
+ } catch (e) {
+ console.error("Decyrption failed", e);
+ return "";
+ }
+ }
+ },
+ sendDm: async (content: string, to: HexKey) => {
+ if (pubKey) {
+ let ev = NEvent.ForPubKey(pubKey);
+ ev.Kind = EventKind.DirectMessage;
+ ev.Content = content;
+ ev.Tags.push(new Tag(["p", to], 0));
+
+ try {
+ if (hasNip07 && !privKey) {
+ let cx: string = await barierNip07(() =>
+ window.nostr.nip04.encrypt(to, content)
+ );
+ ev.Content = cx;
+ return await signEvent(ev);
+ } else if (privKey) {
+ await ev.EncryptDmForPubkey(to, privKey);
+ return await signEvent(ev);
+ }
+ } catch (e) {
+ console.error("Encryption failed", e);
+ }
+ }
+ },
+ };
}
let isNip07Busy = false;
const delay = (t: number) => {
- return new Promise((resolve, reject) => {
- setTimeout(resolve, t);
- });
-}
+ return new Promise((resolve, reject) => {
+ setTimeout(resolve, t);
+ });
+};
export const barierNip07 = async (then: () => Promise) => {
- while (isNip07Busy) {
- await delay(10);
- }
- isNip07Busy = true;
- try {
- return await then();
- } finally {
- isNip07Busy = false;
- }
+ while (isNip07Busy) {
+ await delay(10);
+ }
+ isNip07Busy = true;
+ try {
+ return await then();
+ } finally {
+ isNip07Busy = false;
+ }
};
diff --git a/src/Feed/FollowersFeed.ts b/src/Feed/FollowersFeed.ts
index 5c97e487..8c302fa3 100644
--- a/src/Feed/FollowersFeed.ts
+++ b/src/Feed/FollowersFeed.ts
@@ -5,14 +5,14 @@ import { Subscriptions } from "Nostr/Subscriptions";
import useSubscription from "Feed/Subscription";
export default function useFollowersFeed(pubkey: HexKey) {
- const sub = useMemo(() => {
- let x = new Subscriptions();
- x.Id = `followers:${pubkey.slice(0, 12)}`;
- x.Kinds = new Set([EventKind.ContactList]);
- x.PTags = new Set([pubkey]);
+ const sub = useMemo(() => {
+ let x = new Subscriptions();
+ x.Id = `followers:${pubkey.slice(0, 12)}`;
+ x.Kinds = new Set([EventKind.ContactList]);
+ x.PTags = new Set([pubkey]);
- return x;
- }, [pubkey]);
+ return x;
+ }, [pubkey]);
- return useSubscription(sub);
-}
\ No newline at end of file
+ return useSubscription(sub);
+}
diff --git a/src/Feed/FollowsFeed.ts b/src/Feed/FollowsFeed.ts
index 28004000..bd8a8332 100644
--- a/src/Feed/FollowsFeed.ts
+++ b/src/Feed/FollowsFeed.ts
@@ -1,24 +1,28 @@
import { useMemo } from "react";
import { HexKey } from "Nostr";
import EventKind from "Nostr/EventKind";
-import { Subscriptions} from "Nostr/Subscriptions";
+import { Subscriptions } from "Nostr/Subscriptions";
import useSubscription, { NoteStore } from "Feed/Subscription";
export default function useFollowsFeed(pubkey: HexKey) {
- const sub = useMemo(() => {
- let x = new Subscriptions();
- x.Id = `follows:${pubkey.slice(0, 12)}`;
- x.Kinds = new Set([EventKind.ContactList]);
- x.Authors = new Set([pubkey]);
+ const sub = useMemo(() => {
+ let x = new Subscriptions();
+ x.Id = `follows:${pubkey.slice(0, 12)}`;
+ x.Kinds = new Set([EventKind.ContactList]);
+ x.Authors = new Set([pubkey]);
- return x;
- }, [pubkey]);
+ return x;
+ }, [pubkey]);
- return useSubscription(sub);
+ return useSubscription(sub);
}
export function getFollowers(feed: NoteStore, pubkey: HexKey) {
- let contactLists = feed?.notes.filter(a => a.kind === EventKind.ContactList && a.pubkey === pubkey);
- let pTags = contactLists?.map(a => a.tags.filter(b => b[0] === "p").map(c => c[1]));
- return [...new Set(pTags?.flat())];
+ let contactLists = feed?.notes.filter(
+ (a) => a.kind === EventKind.ContactList && a.pubkey === pubkey
+ );
+ let pTags = contactLists?.map((a) =>
+ a.tags.filter((b) => b[0] === "p").map((c) => c[1])
+ );
+ return [...new Set(pTags?.flat())];
}
diff --git a/src/Feed/ImgProxy.ts b/src/Feed/ImgProxy.ts
index 91b01408..be28272f 100644
--- a/src/Feed/ImgProxy.ts
+++ b/src/Feed/ImgProxy.ts
@@ -1,39 +1,44 @@
-import * as secp from "@noble/secp256k1"
-import * as base64 from "@protobufjs/base64"
+import * as secp from "@noble/secp256k1";
+import * as base64 from "@protobufjs/base64";
import { useSelector } from "react-redux";
import { RootState } from "State/Store";
export interface ImgProxySettings {
- url: string,
- key: string,
- salt: string
+ url: string;
+ key: string;
+ salt: string;
}
export default function useImgProxy() {
- const settings = useSelector((s: RootState) => s.login.preferences.imgProxyConfig);
- const te = new TextEncoder();
+ const settings = useSelector(
+ (s: RootState) => s.login.preferences.imgProxyConfig
+ );
+ const te = new TextEncoder();
- function urlSafe(s: string) {
- return s.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
- }
+ function urlSafe(s: string) {
+ return s.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
+ }
- async function signUrl(u: string) {
- const result = await secp.utils.hmacSha256(
- secp.utils.hexToBytes(settings!.key),
- secp.utils.hexToBytes(settings!.salt),
- te.encode(u));
- return urlSafe(base64.encode(result, 0, result.byteLength));
- }
+ async function signUrl(u: string) {
+ const result = await secp.utils.hmacSha256(
+ secp.utils.hexToBytes(settings!.key),
+ secp.utils.hexToBytes(settings!.salt),
+ te.encode(u)
+ );
+ return urlSafe(base64.encode(result, 0, result.byteLength));
+ }
- return {
- proxy: async (url: string, resize?: number) => {
- if (!settings) return url;
- const opt = resize ? `rs:fit:${resize}:${resize}` : "";
- const urlBytes = te.encode(url);
- const urlEncoded = urlSafe(base64.encode(urlBytes, 0, urlBytes.byteLength));
- const path = `/${opt}/${urlEncoded}`;
- const sig = await signUrl(path);
- return `${new URL(settings.url).toString()}${sig}${path}`;
- }
- }
-}
\ No newline at end of file
+ return {
+ proxy: async (url: string, resize?: number) => {
+ if (!settings) return url;
+ const opt = resize ? `rs:fit:${resize}:${resize}` : "";
+ const urlBytes = te.encode(url);
+ const urlEncoded = urlSafe(
+ base64.encode(urlBytes, 0, urlBytes.byteLength)
+ );
+ const path = `/${opt}/${urlEncoded}`;
+ const sig = await signUrl(path);
+ return `${new URL(settings.url).toString()}${sig}${path}`;
+ },
+ };
+}
diff --git a/src/Feed/LoginFeed.ts b/src/Feed/LoginFeed.ts
index 89c76356..a9e55fb4 100644
--- a/src/Feed/LoginFeed.ts
+++ b/src/Feed/LoginFeed.ts
@@ -6,7 +6,15 @@ import { TaggedRawEvent, HexKey, Lists } from "Nostr";
import EventKind from "Nostr/EventKind";
import Event from "Nostr/Event";
import { Subscriptions } from "Nostr/Subscriptions";
-import { addDirectMessage, setFollows, setRelays, setMuted, setBlocked, sendNotification, setLatestNotifications } from "State/Login";
+import {
+ addDirectMessage,
+ setFollows,
+ setRelays,
+ setMuted,
+ setBlocked,
+ sendNotification,
+ setLatestNotifications,
+} from "State/Login";
import { RootState } from "State/Store";
import { mapEventToProfile, MetadataCache } from "State/Users";
import { useDb } from "State/Users/Db";
@@ -20,7 +28,12 @@ import useModeration from "Hooks/useModeration";
*/
export default function useLoginFeed() {
const dispatch = useDispatch();
- const { publicKey: pubKey, privateKey: privKey, latestMuted, readNotifications } = useSelector((s: RootState) => s.login);
+ const {
+ publicKey: pubKey,
+ privateKey: privKey,
+ latestMuted,
+ readNotifications,
+ } = useSelector((s: RootState) => s.login);
const { isMuted } = useModeration();
const db = useDb();
@@ -31,7 +44,7 @@ export default function useLoginFeed() {
sub.Id = `login:meta`;
sub.Authors = new Set([pubKey]);
sub.Kinds = new Set([EventKind.ContactList, EventKind.SetMetadata]);
- sub.Limit = 2
+ sub.Limit = 2;
return sub;
}, [pubKey]);
@@ -77,35 +90,49 @@ export default function useLoginFeed() {
return dms;
}, [pubKey]);
- const metadataFeed = useSubscription(subMetadata, { leaveOpen: true, cache: true });
- const notificationFeed = useSubscription(subNotification, { leaveOpen: true, cache: true });
+ const metadataFeed = useSubscription(subMetadata, {
+ leaveOpen: true,
+ cache: true,
+ });
+ const notificationFeed = useSubscription(subNotification, {
+ leaveOpen: true,
+ cache: true,
+ });
const dmsFeed = useSubscription(subDms, { leaveOpen: true, cache: true });
const mutedFeed = useSubscription(subMuted, { leaveOpen: true, cache: true });
useEffect(() => {
- let contactList = metadataFeed.store.notes.filter(a => a.kind === EventKind.ContactList);
- let metadata = metadataFeed.store.notes.filter(a => a.kind === EventKind.SetMetadata);
- let profiles = metadata.map(a => mapEventToProfile(a))
- .filter(a => a !== undefined)
- .map(a => a!);
+ let contactList = metadataFeed.store.notes.filter(
+ (a) => a.kind === EventKind.ContactList
+ );
+ let metadata = metadataFeed.store.notes.filter(
+ (a) => a.kind === EventKind.SetMetadata
+ );
+ let profiles = metadata
+ .map((a) => mapEventToProfile(a))
+ .filter((a) => a !== undefined)
+ .map((a) => a!);
for (let cl of contactList) {
if (cl.content !== "" && cl.content !== "{}") {
let relays = JSON.parse(cl.content);
dispatch(setRelays({ relays, createdAt: cl.created_at }));
}
- let pTags = cl.tags.filter(a => a[0] === "p").map(a => a[1]);
+ let pTags = cl.tags.filter((a) => a[0] === "p").map((a) => a[1]);
dispatch(setFollows({ keys: pTags, createdAt: cl.created_at }));
}
(async () => {
- let maxProfile = profiles.reduce((acc, v) => {
- if (v.created > acc.created) {
- acc.profile = v;
- acc.created = v.created;
- }
- return acc;
- }, { created: 0, profile: null as MetadataCache | null });
+ let maxProfile = profiles.reduce(
+ (acc, v) => {
+ if (v.created > acc.created) {
+ acc.profile = v;
+ acc.created = v.created;
+ }
+ return acc;
+ },
+ { created: 0, profile: null as MetadataCache | null }
+ );
if (maxProfile.profile) {
let existing = await db.find(maxProfile.profile.pubkey);
if ((existing?.created ?? 0) < maxProfile.created) {
@@ -116,52 +143,74 @@ export default function useLoginFeed() {
}, [dispatch, metadataFeed.store, db]);
useEffect(() => {
- const replies = notificationFeed.store.notes.
- filter(a => a.kind === EventKind.TextNote && !isMuted(a.pubkey) && a.created_at > readNotifications)
- replies.forEach(nx => {
+ const replies = notificationFeed.store.notes.filter(
+ (a) =>
+ a.kind === EventKind.TextNote &&
+ !isMuted(a.pubkey) &&
+ a.created_at > readNotifications
+ );
+ replies.forEach((nx) => {
dispatch(setLatestNotifications(nx.created_at));
- makeNotification(db, nx).then(notification => {
+ makeNotification(db, nx).then((notification) => {
if (notification) {
// @ts-ignore
- dispatch(sendNotification(notification))
+ dispatch(sendNotification(notification));
}
- })
- })
+ });
+ });
}, [dispatch, notificationFeed.store, db, readNotifications]);
useEffect(() => {
- const muted = getMutedKeys(mutedFeed.store.notes)
- dispatch(setMuted(muted))
+ const muted = getMutedKeys(mutedFeed.store.notes);
+ dispatch(setMuted(muted));
- const newest = getNewest(mutedFeed.store.notes)
- if (newest && newest.content.length > 0 && pubKey && newest.created_at > latestMuted) {
- decryptBlocked(newest, pubKey, privKey).then((plaintext) => {
- try {
- const blocked = JSON.parse(plaintext)
- const keys = blocked.filter((p: any) => p && p.length === 2 && p[0] === "p").map((p: any) => p[1])
- dispatch(setBlocked({
- keys,
- createdAt: newest.created_at,
- }))
- } catch (error) {
- console.debug("Couldn't parse JSON")
- }
- }).catch((error) => console.warn(error))
+ const newest = getNewest(mutedFeed.store.notes);
+ if (
+ newest &&
+ newest.content.length > 0 &&
+ pubKey &&
+ newest.created_at > latestMuted
+ ) {
+ decryptBlocked(newest, pubKey, privKey)
+ .then((plaintext) => {
+ try {
+ const blocked = JSON.parse(plaintext);
+ const keys = blocked
+ .filter((p: any) => p && p.length === 2 && p[0] === "p")
+ .map((p: any) => p[1]);
+ dispatch(
+ setBlocked({
+ keys,
+ createdAt: newest.created_at,
+ })
+ );
+ } catch (error) {
+ console.debug("Couldn't parse JSON");
+ }
+ })
+ .catch((error) => console.warn(error));
}
- }, [dispatch, mutedFeed.store])
+ }, [dispatch, mutedFeed.store]);
useEffect(() => {
- let dms = dmsFeed.store.notes.filter(a => a.kind === EventKind.DirectMessage);
+ let dms = dmsFeed.store.notes.filter(
+ (a) => a.kind === EventKind.DirectMessage
+ );
dispatch(addDirectMessage(dms));
}, [dispatch, dmsFeed.store]);
}
-
-async function decryptBlocked(raw: TaggedRawEvent, pubKey: HexKey, privKey?: HexKey) {
- const ev = new Event(raw)
+async function decryptBlocked(
+ raw: TaggedRawEvent,
+ pubKey: HexKey,
+ privKey?: HexKey
+) {
+ const ev = new Event(raw);
if (pubKey && privKey) {
- return await ev.DecryptData(raw.content, privKey, pubKey)
+ return await ev.DecryptData(raw.content, privKey, pubKey);
} else {
- return await barierNip07(() => window.nostr.nip04.decrypt(pubKey, raw.content));
+ return await barierNip07(() =>
+ window.nostr.nip04.decrypt(pubKey, raw.content)
+ );
}
}
diff --git a/src/Feed/MuteList.ts b/src/Feed/MuteList.ts
index 7a2bb822..142d5a25 100644
--- a/src/Feed/MuteList.ts
+++ b/src/Feed/MuteList.ts
@@ -6,41 +6,46 @@ import { Subscriptions } from "Nostr/Subscriptions";
import useSubscription, { NoteStore } from "Feed/Subscription";
export default function useMutedFeed(pubkey: HexKey) {
- const sub = useMemo(() => {
- let sub = new Subscriptions();
- sub.Id = `muted:${pubkey.slice(0, 12)}`;
- sub.Kinds = new Set([EventKind.Lists]);
- sub.Authors = new Set([pubkey]);
- sub.DTags = new Set([Lists.Muted]);
- sub.Limit = 1;
- return sub;
- }, [pubkey]);
+ const sub = useMemo(() => {
+ let sub = new Subscriptions();
+ sub.Id = `muted:${pubkey.slice(0, 12)}`;
+ sub.Kinds = new Set([EventKind.Lists]);
+ sub.Authors = new Set([pubkey]);
+ sub.DTags = new Set([Lists.Muted]);
+ sub.Limit = 1;
+ return sub;
+ }, [pubkey]);
- return useSubscription(sub);
+ return useSubscription(sub);
}
-export function getNewest(rawNotes: TaggedRawEvent[]){
- const notes = [...rawNotes]
- notes.sort((a, b) => a.created_at - b.created_at)
- if (notes.length > 0) {
- return notes[0]
- }
+export function getNewest(rawNotes: TaggedRawEvent[]) {
+ const notes = [...rawNotes];
+ notes.sort((a, b) => a.created_at - b.created_at);
+ if (notes.length > 0) {
+ return notes[0];
+ }
}
-export function getMutedKeys(rawNotes: TaggedRawEvent[]): { createdAt: number, keys: HexKey[] } {
- const newest = getNewest(rawNotes)
- if (newest) {
- const { created_at, tags } = newest
- const keys = tags.filter(t => t[0] === "p").map(t => t[1])
- return {
- keys,
- createdAt: created_at,
- }
- }
- return { createdAt: 0, keys: [] }
+export function getMutedKeys(rawNotes: TaggedRawEvent[]): {
+ createdAt: number;
+ keys: HexKey[];
+} {
+ const newest = getNewest(rawNotes);
+ if (newest) {
+ const { created_at, tags } = newest;
+ const keys = tags.filter((t) => t[0] === "p").map((t) => t[1]);
+ return {
+ keys,
+ createdAt: created_at,
+ };
+ }
+ return { createdAt: 0, keys: [] };
}
export function getMuted(feed: NoteStore, pubkey: HexKey): HexKey[] {
- let lists = feed?.notes.filter(a => a.kind === EventKind.Lists && a.pubkey === pubkey);
- return getMutedKeys(lists).keys;
+ let lists = feed?.notes.filter(
+ (a) => a.kind === EventKind.Lists && a.pubkey === pubkey
+ );
+ return getMutedKeys(lists).keys;
}
diff --git a/src/Feed/ProfileFeed.ts b/src/Feed/ProfileFeed.ts
index 4c1d5a01..51703020 100644
--- a/src/Feed/ProfileFeed.ts
+++ b/src/Feed/ProfileFeed.ts
@@ -5,28 +5,29 @@ import { HexKey } from "Nostr";
import { System } from "Nostr/System";
export function useUserProfile(pubKey: HexKey): MetadataCache | undefined {
- const users = useKey(pubKey);
+ const users = useKey(pubKey);
- useEffect(() => {
- if (pubKey) {
- System.TrackMetadata(pubKey);
- return () => System.UntrackMetadata(pubKey);
- }
- }, [pubKey]);
+ useEffect(() => {
+ if (pubKey) {
+ System.TrackMetadata(pubKey);
+ return () => System.UntrackMetadata(pubKey);
+ }
+ }, [pubKey]);
- return users;
+ return users;
}
+export function useUserProfiles(
+ pubKeys: Array
+): Map | undefined {
+ const users = useKeys(pubKeys);
-export function useUserProfiles(pubKeys: Array): Map | undefined {
- const users = useKeys(pubKeys);
+ useEffect(() => {
+ if (pubKeys) {
+ System.TrackMetadata(pubKeys);
+ return () => System.UntrackMetadata(pubKeys);
+ }
+ }, [pubKeys]);
- useEffect(() => {
- if (pubKeys) {
- System.TrackMetadata(pubKeys);
- return () => System.UntrackMetadata(pubKeys);
- }
- }, [pubKeys]);
-
- return users;
+ return users;
}
diff --git a/src/Feed/RelayState.ts b/src/Feed/RelayState.ts
index 2c78311b..c7de65e9 100644
--- a/src/Feed/RelayState.ts
+++ b/src/Feed/RelayState.ts
@@ -2,12 +2,17 @@ import { useSyncExternalStore } from "react";
import { System } from "Nostr/System";
import { CustomHook, StateSnapshot } from "Nostr/Connection";
-const noop = (f: CustomHook) => { return () => { }; };
+const noop = (f: CustomHook) => {
+ return () => {};
+};
const noopState = (): StateSnapshot | undefined => {
- return undefined;
+ return undefined;
};
export default function useRelayState(addr: string) {
- let c = System.Sockets.get(addr);
- return useSyncExternalStore(c?.StatusHook.bind(c) ?? noop, c?.GetState.bind(c) ?? noopState);
-}
\ No newline at end of file
+ let c = System.Sockets.get(addr);
+ return useSyncExternalStore(
+ c?.StatusHook.bind(c) ?? noop,
+ c?.GetState.bind(c) ?? noopState
+ );
+}
diff --git a/src/Feed/Subscription.ts b/src/Feed/Subscription.ts
index 73a967cd..69f3925d 100644
--- a/src/Feed/Subscription.ts
+++ b/src/Feed/Subscription.ts
@@ -6,62 +6,59 @@ import { debounce } from "Util";
import { db } from "Db";
export type NoteStore = {
- notes: Array,
- end: boolean
+ notes: Array;
+ end: boolean;
};
export type UseSubscriptionOptions = {
- leaveOpen: boolean,
- cache: boolean
-}
+ leaveOpen: boolean;
+ cache: boolean;
+};
interface ReducerArg {
- type: "END" | "EVENT" | "CLEAR",
- ev?: TaggedRawEvent | Array,
- end?: boolean
+ type: "END" | "EVENT" | "CLEAR";
+ ev?: TaggedRawEvent | Array;
+ end?: boolean;
}
function notesReducer(state: NoteStore, arg: ReducerArg) {
- if (arg.type === "END") {
- return {
- notes: state.notes,
- end: arg.end!
- } as NoteStore;
- }
-
- if (arg.type === "CLEAR") {
- return {
- notes: [],
- end: state.end,
- } as NoteStore;
- }
-
- let evs = arg.ev!;
- if (!Array.isArray(evs)) {
- evs = [evs];
- }
- let existingIds = new Set(state.notes.map(a => a.id));
- evs = evs.filter(a => !existingIds.has(a.id));
- if (evs.length === 0) {
- return state;
- }
+ if (arg.type === "END") {
return {
- notes: [
- ...state.notes,
- ...evs
- ]
+ notes: state.notes,
+ end: arg.end!,
} as NoteStore;
+ }
+
+ if (arg.type === "CLEAR") {
+ return {
+ notes: [],
+ end: state.end,
+ } as NoteStore;
+ }
+
+ let evs = arg.ev!;
+ if (!Array.isArray(evs)) {
+ evs = [evs];
+ }
+ let existingIds = new Set(state.notes.map((a) => a.id));
+ evs = evs.filter((a) => !existingIds.has(a.id));
+ if (evs.length === 0) {
+ return state;
+ }
+ return {
+ notes: [...state.notes, ...evs],
+ } as NoteStore;
}
const initStore: NoteStore = {
- notes: [],
- end: false
+ notes: [],
+ end: false,
};
export interface UseSubscriptionState {
- store: NoteStore,
- clear: () => void,
- append: (notes: TaggedRawEvent[]) => void
+ store: NoteStore;
+ clear: () => void;
+ append: (notes: TaggedRawEvent[]) => void;
}
/**
@@ -70,121 +67,131 @@ export interface UseSubscriptionState {
const DebounceMs = 200;
/**
- *
- * @param {Subscriptions} sub
- * @param {any} opt
- * @returns
+ *
+ * @param {Subscriptions} sub
+ * @param {any} opt
+ * @returns
*/
-export default function useSubscription(sub: Subscriptions | null, options?: UseSubscriptionOptions): UseSubscriptionState {
- const [state, dispatch] = useReducer(notesReducer, initStore);
- const [debounceOutput, setDebounceOutput] = useState(0);
- const [subDebounce, setSubDebounced] = useState();
- const useCache = useMemo(() => options?.cache === true, [options]);
+export default function useSubscription(
+ sub: Subscriptions | null,
+ options?: UseSubscriptionOptions
+): UseSubscriptionState {
+ const [state, dispatch] = useReducer(notesReducer, initStore);
+ const [debounceOutput, setDebounceOutput] = useState(0);
+ const [subDebounce, setSubDebounced] = useState();
+ const useCache = useMemo(() => options?.cache === true, [options]);
- useEffect(() => {
- if (sub) {
- return debounce(DebounceMs, () => {
- setSubDebounced(sub);
- });
- }
- }, [sub, options]);
-
- useEffect(() => {
- if (subDebounce) {
- dispatch({
- type: "END",
- end: false
- });
-
- if (useCache) {
- // preload notes from db
- PreloadNotes(subDebounce.Id)
- .then(ev => {
- dispatch({
- type: "EVENT",
- ev: ev
- });
- })
- .catch(console.warn);
- }
- subDebounce.OnEvent = (e) => {
- dispatch({
- type: "EVENT",
- ev: e
- });
- if (useCache) {
- db.events.put(e);
- }
- };
-
- subDebounce.OnEnd = (c) => {
- if (!(options?.leaveOpen ?? false)) {
- c.RemoveSubscription(subDebounce.Id);
- if (subDebounce.IsFinished()) {
- System.RemoveSubscription(subDebounce.Id);
- }
- }
- dispatch({
- type: "END",
- end: true
- });
- };
-
- console.debug("Adding sub: ", subDebounce.ToObject());
- System.AddSubscription(subDebounce);
- return () => {
- console.debug("Removing sub: ", subDebounce.ToObject());
- System.RemoveSubscription(subDebounce.Id);
- };
- }
- }, [subDebounce, useCache]);
-
- useEffect(() => {
- if (subDebounce && useCache) {
- return debounce(500, () => {
- TrackNotesInFeed(subDebounce.Id, state.notes)
- .catch(console.warn);
- });
- }
- }, [state, useCache]);
-
- useEffect(() => {
- return debounce(DebounceMs, () => {
- setDebounceOutput(s => s += 1);
- });
- }, [state]);
-
- const stateDebounced = useMemo(() => state, [debounceOutput]);
- return {
- store: stateDebounced,
- clear: () => {
- dispatch({ type: "CLEAR" });
- },
- append: (n: TaggedRawEvent[]) => {
- dispatch({
- type: "EVENT",
- ev: n
- });
- }
+ useEffect(() => {
+ if (sub) {
+ return debounce(DebounceMs, () => {
+ setSubDebounced(sub);
+ });
}
+ }, [sub, options]);
+
+ useEffect(() => {
+ if (subDebounce) {
+ dispatch({
+ type: "END",
+ end: false,
+ });
+
+ if (useCache) {
+ // preload notes from db
+ PreloadNotes(subDebounce.Id)
+ .then((ev) => {
+ dispatch({
+ type: "EVENT",
+ ev: ev,
+ });
+ })
+ .catch(console.warn);
+ }
+ subDebounce.OnEvent = (e) => {
+ dispatch({
+ type: "EVENT",
+ ev: e,
+ });
+ if (useCache) {
+ db.events.put(e);
+ }
+ };
+
+ subDebounce.OnEnd = (c) => {
+ if (!(options?.leaveOpen ?? false)) {
+ c.RemoveSubscription(subDebounce.Id);
+ if (subDebounce.IsFinished()) {
+ System.RemoveSubscription(subDebounce.Id);
+ }
+ }
+ dispatch({
+ type: "END",
+ end: true,
+ });
+ };
+
+ console.debug("Adding sub: ", subDebounce.ToObject());
+ System.AddSubscription(subDebounce);
+ return () => {
+ console.debug("Removing sub: ", subDebounce.ToObject());
+ System.RemoveSubscription(subDebounce.Id);
+ };
+ }
+ }, [subDebounce, useCache]);
+
+ useEffect(() => {
+ if (subDebounce && useCache) {
+ return debounce(500, () => {
+ TrackNotesInFeed(subDebounce.Id, state.notes).catch(console.warn);
+ });
+ }
+ }, [state, useCache]);
+
+ useEffect(() => {
+ return debounce(DebounceMs, () => {
+ setDebounceOutput((s) => (s += 1));
+ });
+ }, [state]);
+
+ const stateDebounced = useMemo(() => state, [debounceOutput]);
+ return {
+ store: stateDebounced,
+ clear: () => {
+ dispatch({ type: "CLEAR" });
+ },
+ append: (n: TaggedRawEvent[]) => {
+ dispatch({
+ type: "EVENT",
+ ev: n,
+ });
+ },
+ };
}
/**
* Lookup cached copy of feed
*/
const PreloadNotes = async (id: string): Promise => {
- const feed = await db.feeds.get(id);
- if (feed) {
- const events = await db.events.bulkGet(feed.ids);
- return events.filter(a => a !== undefined).map(a => a!);
- }
- return [];
-}
+ const feed = await db.feeds.get(id);
+ if (feed) {
+ const events = await db.events.bulkGet(feed.ids);
+ return events.filter((a) => a !== undefined).map((a) => a!);
+ }
+ return [];
+};
const TrackNotesInFeed = async (id: string, notes: TaggedRawEvent[]) => {
- const existing = await db.feeds.get(id);
- const ids = Array.from(new Set([...(existing?.ids || []), ...notes.map(a => a.id)]));
- const since = notes.reduce((acc, v) => acc > v.created_at ? v.created_at : acc, +Infinity);
- const until = notes.reduce((acc, v) => acc < v.created_at ? v.created_at : acc, -Infinity);
- await db.feeds.put({ id, ids, since, until });
-}
\ No newline at end of file
+ const existing = await db.feeds.get(id);
+ const ids = Array.from(
+ new Set([...(existing?.ids || []), ...notes.map((a) => a.id)])
+ );
+ const since = notes.reduce(
+ (acc, v) => (acc > v.created_at ? v.created_at : acc),
+ +Infinity
+ );
+ const until = notes.reduce(
+ (acc, v) => (acc < v.created_at ? v.created_at : acc),
+ -Infinity
+ );
+ await db.feeds.put({ id, ids, since, until });
+};
diff --git a/src/Feed/ThreadFeed.ts b/src/Feed/ThreadFeed.ts
index b8054e2e..93dcd258 100644
--- a/src/Feed/ThreadFeed.ts
+++ b/src/Feed/ThreadFeed.ts
@@ -9,51 +9,66 @@ import { UserPreferences } from "State/Login";
import { debounce } from "Util";
export default function useThreadFeed(id: u256) {
- const [trackingEvents, setTrackingEvent] = useState([id]);
- const pref = useSelector(s => s.login.preferences);
+ const [trackingEvents, setTrackingEvent] = useState([id]);
+ const pref = useSelector(
+ (s) => s.login.preferences
+ );
- function addId(id: u256[]) {
- setTrackingEvent((s) => {
- let orig = new Set(s);
- if (id.some(a => !orig.has(a))) {
- let tmp = new Set([...s, ...id]);
- return Array.from(tmp);
- } else {
- return s;
- }
- })
+ function addId(id: u256[]) {
+ setTrackingEvent((s) => {
+ let orig = new Set(s);
+ if (id.some((a) => !orig.has(a))) {
+ let tmp = new Set([...s, ...id]);
+ return Array.from(tmp);
+ } else {
+ return s;
+ }
+ });
+ }
+
+ const sub = useMemo(() => {
+ const thisSub = new Subscriptions();
+ thisSub.Id = `thread:${id.substring(0, 8)}`;
+ thisSub.Ids = new Set(trackingEvents);
+
+ // get replies to this event
+ const subRelated = new Subscriptions();
+ subRelated.Kinds = new Set(
+ pref.enableReactions
+ ? [
+ EventKind.Reaction,
+ EventKind.TextNote,
+ EventKind.Deletion,
+ EventKind.Repost,
+ EventKind.ZapReceipt,
+ ]
+ : [EventKind.TextNote]
+ );
+ subRelated.ETags = thisSub.Ids;
+ thisSub.AddSubscription(subRelated);
+
+ return thisSub;
+ }, [trackingEvents, pref, id]);
+
+ const main = useSubscription(sub, { leaveOpen: true, cache: true });
+
+ useEffect(() => {
+ if (main.store) {
+ return debounce(200, () => {
+ let mainNotes = main.store.notes.filter(
+ (a) => a.kind === EventKind.TextNote
+ );
+
+ let eTags = mainNotes
+ .filter((a) => a.kind === EventKind.TextNote)
+ .map((a) => a.tags.filter((b) => b[0] === "e").map((b) => b[1]))
+ .flat();
+ let ids = mainNotes.map((a) => a.id);
+ let allEvents = new Set([...eTags, ...ids]);
+ addId(Array.from(allEvents));
+ });
}
+ }, [main.store]);
- const sub = useMemo(() => {
- const thisSub = new Subscriptions();
- thisSub.Id = `thread:${id.substring(0, 8)}`;
- thisSub.Ids = new Set(trackingEvents);
-
- // get replies to this event
- const subRelated = new Subscriptions();
- subRelated.Kinds = new Set(pref.enableReactions ? [EventKind.Reaction, EventKind.TextNote, EventKind.Deletion, EventKind.Repost, EventKind.ZapReceipt] : [EventKind.TextNote]);
- subRelated.ETags = thisSub.Ids;
- thisSub.AddSubscription(subRelated);
-
- return thisSub;
- }, [trackingEvents, pref, id]);
-
- const main = useSubscription(sub, { leaveOpen: true, cache: true });
-
- useEffect(() => {
- if (main.store) {
- return debounce(200, () => {
- let mainNotes = main.store.notes.filter(a => a.kind === EventKind.TextNote);
-
- let eTags = mainNotes
- .filter(a => a.kind === EventKind.TextNote)
- .map(a => a.tags.filter(b => b[0] === "e").map(b => b[1])).flat();
- let ids = mainNotes.map(a => a.id);
- let allEvents = new Set([...eTags, ...ids]);
- addId(Array.from(allEvents));
- })
- }
- }, [main.store]);
-
- return main.store;
+ return main.store;
}
diff --git a/src/Feed/TimelineFeed.ts b/src/Feed/TimelineFeed.ts
index 539683d6..d017f215 100644
--- a/src/Feed/TimelineFeed.ts
+++ b/src/Feed/TimelineFeed.ts
@@ -9,169 +9,184 @@ import { RootState } from "State/Store";
import { UserPreferences } from "State/Login";
export interface TimelineFeedOptions {
- method: "TIME_RANGE" | "LIMIT_UNTIL",
- window?: number
+ method: "TIME_RANGE" | "LIMIT_UNTIL";
+ window?: number;
}
export interface TimelineSubject {
- type: "pubkey" | "hashtag" | "global" | "ptag" | "keyword",
- discriminator: string,
- items: string[]
+ type: "pubkey" | "hashtag" | "global" | "ptag" | "keyword";
+ discriminator: string;
+ items: string[];
}
-export default function useTimelineFeed(subject: TimelineSubject, options: TimelineFeedOptions) {
- const now = unixNow();
- const [window] = useState(options.window ?? 60 * 60);
- const [until, setUntil] = useState(now);
- const [since, setSince] = useState(now - window);
- const [trackingEvents, setTrackingEvent] = useState([]);
- const [trackingParentEvents, setTrackingParentEvents] = useState([]);
- const pref = useSelector(s => s.login.preferences);
+export default function useTimelineFeed(
+ subject: TimelineSubject,
+ options: TimelineFeedOptions
+) {
+ const now = unixNow();
+ const [window] = useState(options.window ?? 60 * 60);
+ const [until, setUntil] = useState(now);
+ const [since, setSince] = useState(now - window);
+ const [trackingEvents, setTrackingEvent] = useState([]);
+ const [trackingParentEvents, setTrackingParentEvents] = useState([]);
+ const pref = useSelector(
+ (s) => s.login.preferences
+ );
- const createSub = useCallback(() => {
- if (subject.type !== "global" && subject.items.length === 0) {
- return null;
+ const createSub = useCallback(() => {
+ if (subject.type !== "global" && subject.items.length === 0) {
+ return null;
+ }
+
+ let sub = new Subscriptions();
+ sub.Id = `timeline:${subject.type}:${subject.discriminator}`;
+ sub.Kinds = new Set([EventKind.TextNote, EventKind.Repost]);
+ switch (subject.type) {
+ case "pubkey": {
+ sub.Authors = new Set(subject.items);
+ break;
+ }
+ case "hashtag": {
+ sub.HashTags = new Set(subject.items);
+ break;
+ }
+ case "ptag": {
+ 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, subject.discriminator]);
+
+ const sub = useMemo(() => {
+ let sub = createSub();
+ if (sub) {
+ if (options.method === "LIMIT_UNTIL") {
+ sub.Until = until;
+ sub.Limit = 10;
+ } else {
+ sub.Since = since;
+ sub.Until = until;
+ if (since === undefined) {
+ sub.Limit = 50;
}
+ }
- let sub = new Subscriptions();
- sub.Id = `timeline:${subject.type}:${subject.discriminator}`;
- sub.Kinds = new Set([EventKind.TextNote, EventKind.Repost]);
- switch (subject.type) {
- case "pubkey": {
- sub.Authors = new Set(subject.items);
- break;
- }
- case "hashtag": {
- sub.HashTags = new Set(subject.items);
- break;
- }
- case "ptag": {
- sub.PTags = new Set(subject.items);
- break;
- }
- case "keyword": {
- sub.Kinds.add(EventKind.SetMetadata);
- sub.Search = subject.items[0];
- break;
- }
+ if (pref.autoShowLatest) {
+ // copy properties of main sub but with limit 0
+ // this will put latest directly into main feed
+ let latestSub = new Subscriptions();
+ latestSub.Authors = sub.Authors;
+ latestSub.HashTags = sub.HashTags;
+ latestSub.PTags = sub.PTags;
+ latestSub.Kinds = sub.Kinds;
+ latestSub.Search = sub.Search;
+ latestSub.Limit = 1;
+ latestSub.Since = Math.floor(new Date().getTime() / 1000);
+ sub.AddSubscription(latestSub);
+ }
+ }
+ return sub;
+ }, [until, since, options.method, pref, createSub]);
+
+ const main = useSubscription(sub, { leaveOpen: true, cache: true });
+
+ const subRealtime = useMemo(() => {
+ let subLatest = createSub();
+ if (subLatest && !pref.autoShowLatest) {
+ subLatest.Id = `${subLatest.Id}:latest`;
+ subLatest.Limit = 1;
+ subLatest.Since = Math.floor(new Date().getTime() / 1000);
+ }
+ return subLatest;
+ }, [pref, createSub]);
+
+ const latest = useSubscription(subRealtime, {
+ leaveOpen: true,
+ cache: false,
+ });
+
+ const subNext = useMemo(() => {
+ let sub: Subscriptions | undefined;
+ if (trackingEvents.length > 0 && pref.enableReactions) {
+ sub = new Subscriptions();
+ sub.Id = `timeline-related:${subject.type}`;
+ sub.Kinds = new Set([
+ EventKind.Reaction,
+ EventKind.Deletion,
+ EventKind.ZapReceipt,
+ ]);
+ sub.ETags = new Set(trackingEvents);
+ }
+ return sub ?? null;
+ }, [trackingEvents, pref, subject.type]);
+
+ const others = useSubscription(subNext, { leaveOpen: true, cache: true });
+
+ const subParents = useMemo(() => {
+ if (trackingParentEvents.length > 0) {
+ let parents = new Subscriptions();
+ parents.Id = `timeline-parent:${subject.type}`;
+ parents.Ids = new Set(trackingParentEvents);
+ return parents;
+ }
+ return null;
+ }, [trackingParentEvents, subject.type]);
+
+ const parent = useSubscription(subParents);
+
+ useEffect(() => {
+ if (main.store.notes.length > 0) {
+ setTrackingEvent((s) => {
+ let ids = main.store.notes.map((a) => a.id);
+ if (ids.some((a) => !s.includes(a))) {
+ return Array.from(new Set([...s, ...ids]));
}
- return sub;
- }, [subject.type, subject.items, subject.discriminator]);
+ return s;
+ });
+ let reposts = main.store.notes
+ .filter((a) => a.kind === EventKind.Repost && a.content === "")
+ .map((a) => a.tags.find((b) => b[0] === "e"))
+ .filter((a) => a)
+ .map((a) => a![1]);
+ if (reposts.length > 0) {
+ setTrackingParentEvents((s) => {
+ if (reposts.some((a) => !s.includes(a))) {
+ let temp = new Set([...s, ...reposts]);
+ return Array.from(temp);
+ }
+ return s;
+ });
+ }
+ }
+ }, [main.store]);
- const sub = useMemo(() => {
- let sub = createSub();
- if (sub) {
- if (options.method === "LIMIT_UNTIL") {
- sub.Until = until;
- sub.Limit = 10;
- } else {
- sub.Since = since;
- sub.Until = until;
- if (since === undefined) {
- sub.Limit = 50;
- }
- }
-
- if (pref.autoShowLatest) {
- // copy properties of main sub but with limit 0
- // this will put latest directly into main feed
- let latestSub = new Subscriptions();
- latestSub.Authors = sub.Authors;
- latestSub.HashTags = sub.HashTags;
- latestSub.PTags = sub.PTags;
- latestSub.Kinds = sub.Kinds;
- latestSub.Search = sub.Search;
- latestSub.Limit = 1;
- latestSub.Since = Math.floor(new Date().getTime() / 1000);
- sub.AddSubscription(latestSub);
- }
- }
- return sub;
- }, [until, since, options.method, pref, createSub]);
-
- const main = useSubscription(sub, { leaveOpen: true, cache: true });
-
- const subRealtime = useMemo(() => {
- let subLatest = createSub();
- if (subLatest && !pref.autoShowLatest) {
- subLatest.Id = `${subLatest.Id}:latest`;
- subLatest.Limit = 1;
- subLatest.Since = Math.floor(new Date().getTime() / 1000);
- }
- return subLatest;
- }, [pref, createSub]);
-
- const latest = useSubscription(subRealtime, { leaveOpen: true, cache: false });
-
- const subNext = useMemo(() => {
- let sub: Subscriptions | undefined;
- if (trackingEvents.length > 0 && pref.enableReactions) {
- sub = new Subscriptions();
- sub.Id = `timeline-related:${subject.type}`;
- sub.Kinds = new Set([EventKind.Reaction, EventKind.Deletion, EventKind.ZapReceipt]);
- sub.ETags = new Set(trackingEvents);
- }
- return sub ?? null;
- }, [trackingEvents, pref, subject.type]);
-
- const others = useSubscription(subNext, { leaveOpen: true, cache: true });
-
- const subParents = useMemo(() => {
- if (trackingParentEvents.length > 0) {
- let parents = new Subscriptions();
- parents.Id = `timeline-parent:${subject.type}`;
- parents.Ids = new Set(trackingParentEvents);
- return parents;
- }
- return null;
- }, [trackingParentEvents, subject.type]);
-
- const parent = useSubscription(subParents);
-
- useEffect(() => {
- if (main.store.notes.length > 0) {
- setTrackingEvent(s => {
- let ids = main.store.notes.map(a => a.id);
- if (ids.some(a => !s.includes(a))) {
- return Array.from(new Set([...s, ...ids]));
- }
- return s;
- });
- let reposts = main.store.notes
- .filter(a => a.kind === EventKind.Repost && a.content === "")
- .map(a => a.tags.find(b => b[0] === "e"))
- .filter(a => a)
- .map(a => a![1]);
- if (reposts.length > 0) {
- setTrackingParentEvents(s => {
- if (reposts.some(a => !s.includes(a))) {
- let temp = new Set([...s, ...reposts]);
- return Array.from(temp);
- }
- return s;
- })
- }
- }
- }, [main.store]);
-
- return {
- main: main.store,
- related: others.store,
- latest: latest.store,
- parent: parent.store,
- loadMore: () => {
- console.debug("Timeline load more!")
- if (options.method === "LIMIT_UNTIL") {
- let oldest = main.store.notes.reduce((acc, v) => acc = v.created_at < acc ? v.created_at : acc, unixNow());
- setUntil(oldest);
- } else {
- setUntil(s => s - window);
- setSince(s => s - window);
- }
- },
- showLatest: () => {
- main.append(latest.store.notes);
- latest.clear();
- }
- };
+ return {
+ main: main.store,
+ related: others.store,
+ latest: latest.store,
+ parent: parent.store,
+ loadMore: () => {
+ console.debug("Timeline load more!");
+ if (options.method === "LIMIT_UNTIL") {
+ let oldest = main.store.notes.reduce(
+ (acc, v) => (acc = v.created_at < acc ? v.created_at : acc),
+ unixNow()
+ );
+ setUntil(oldest);
+ } else {
+ setUntil((s) => s - window);
+ setSince((s) => s - window);
+ }
+ },
+ showLatest: () => {
+ main.append(latest.store.notes);
+ latest.clear();
+ },
+ };
}
diff --git a/src/Feed/ZapsFeed.ts b/src/Feed/ZapsFeed.ts
index 84a52aa6..03569e43 100644
--- a/src/Feed/ZapsFeed.ts
+++ b/src/Feed/ZapsFeed.ts
@@ -5,13 +5,13 @@ import { Subscriptions } from "Nostr/Subscriptions";
import useSubscription from "./Subscription";
export default function useZapsFeed(pubkey: HexKey) {
- const sub = useMemo(() => {
- let x = new Subscriptions();
- x.Id = `zaps:${pubkey.slice(0, 12)}`;
- x.Kinds = new Set([EventKind.ZapReceipt]);
- x.PTags = new Set([pubkey]);
- return x;
- }, [pubkey]);
+ const sub = useMemo(() => {
+ let x = new Subscriptions();
+ x.Id = `zaps:${pubkey.slice(0, 12)}`;
+ x.Kinds = new Set([EventKind.ZapReceipt]);
+ x.PTags = new Set([pubkey]);
+ return x;
+ }, [pubkey]);
- return useSubscription(sub, { leaveOpen: true, cache: true });
+ return useSubscription(sub, { leaveOpen: true, cache: true });
}
diff --git a/src/Hooks/useHorizontalScroll.tsx b/src/Hooks/useHorizontalScroll.tsx
index 88009ce0..72208f58 100644
--- a/src/Hooks/useHorizontalScroll.tsx
+++ b/src/Hooks/useHorizontalScroll.tsx
@@ -16,7 +16,7 @@ function useHorizontalScroll() {
return () => el.removeEventListener("wheel", onWheel);
}
}, []);
- return elRef as LegacyRef | undefined
+ return elRef as LegacyRef | undefined;
}
export default useHorizontalScroll;
diff --git a/src/Hooks/useModeration.tsx b/src/Hooks/useModeration.tsx
index a09316ad..44c692d9 100644
--- a/src/Hooks/useModeration.tsx
+++ b/src/Hooks/useModeration.tsx
@@ -5,74 +5,93 @@ import { HexKey } from "Nostr";
import useEventPublisher from "Feed/EventPublisher";
import { setMuted, setBlocked } from "State/Login";
-
export default function useModeration() {
- const dispatch = useDispatch()
- const { blocked, muted } = useSelector((s: RootState) => s.login)
- const publisher = useEventPublisher()
+ const dispatch = useDispatch();
+ const { blocked, muted } = useSelector((s: RootState) => s.login);
+ const publisher = useEventPublisher();
async function setMutedList(pub: HexKey[], priv: HexKey[]) {
try {
- const ev = await publisher.muted(pub, priv)
+ const ev = await publisher.muted(pub, priv);
console.debug(ev);
- publisher.broadcast(ev)
+ publisher.broadcast(ev);
} catch (error) {
- console.debug("Couldn't change mute list")
+ console.debug("Couldn't change mute list");
}
}
function isMuted(id: HexKey) {
- return muted.includes(id) || blocked.includes(id)
+ return muted.includes(id) || blocked.includes(id);
}
function isBlocked(id: HexKey) {
- return blocked.includes(id)
+ return blocked.includes(id);
}
function unmute(id: HexKey) {
- const newMuted = muted.filter(p => p !== id)
- dispatch(setMuted({
- createdAt: new Date().getTime(),
- keys: newMuted
- }))
- setMutedList(newMuted, blocked)
+ const newMuted = muted.filter((p) => p !== id);
+ dispatch(
+ setMuted({
+ createdAt: new Date().getTime(),
+ keys: newMuted,
+ })
+ );
+ setMutedList(newMuted, blocked);
}
function unblock(id: HexKey) {
- const newBlocked = blocked.filter(p => p !== id)
- dispatch(setBlocked({
- createdAt: new Date().getTime(),
- keys: newBlocked
- }))
- setMutedList(muted, newBlocked)
+ const newBlocked = blocked.filter((p) => p !== id);
+ dispatch(
+ setBlocked({
+ createdAt: new Date().getTime(),
+ keys: newBlocked,
+ })
+ );
+ setMutedList(muted, newBlocked);
}
function mute(id: HexKey) {
- const newMuted = muted.includes(id) ? muted : muted.concat([id])
- setMutedList(newMuted, blocked)
- dispatch(setMuted({
- createdAt: new Date().getTime(),
- keys: newMuted
- }))
+ const newMuted = muted.includes(id) ? muted : muted.concat([id]);
+ setMutedList(newMuted, blocked);
+ dispatch(
+ setMuted({
+ createdAt: new Date().getTime(),
+ keys: newMuted,
+ })
+ );
}
function block(id: HexKey) {
- const newBlocked = blocked.includes(id) ? blocked : blocked.concat([id])
- setMutedList(muted, newBlocked)
- dispatch(setBlocked({
- createdAt: new Date().getTime(),
- keys: newBlocked
- }))
+ const newBlocked = blocked.includes(id) ? blocked : blocked.concat([id]);
+ setMutedList(muted, newBlocked);
+ dispatch(
+ setBlocked({
+ createdAt: new Date().getTime(),
+ keys: newBlocked,
+ })
+ );
}
function muteAll(ids: HexKey[]) {
- const newMuted = Array.from(new Set(muted.concat(ids)))
- setMutedList(newMuted, blocked)
- dispatch(setMuted({
- createdAt: new Date().getTime(),
- keys: newMuted
- }))
+ const newMuted = Array.from(new Set(muted.concat(ids)));
+ setMutedList(newMuted, blocked);
+ dispatch(
+ setMuted({
+ createdAt: new Date().getTime(),
+ keys: newMuted,
+ })
+ );
}
- return { muted, mute, muteAll, unmute, isMuted, blocked, block, unblock, isBlocked }
+ return {
+ muted,
+ mute,
+ muteAll,
+ unmute,
+ isMuted,
+ blocked,
+ block,
+ unblock,
+ isBlocked,
+ };
}
diff --git a/src/Hooks/useWebln.ts b/src/Hooks/useWebln.ts
index f348092c..2b7a6e05 100644
--- a/src/Hooks/useWebln.ts
+++ b/src/Hooks/useWebln.ts
@@ -1,25 +1,25 @@
import { useEffect } from "react";
declare global {
- interface Window {
- webln?: {
- enabled: boolean,
- enable: () => Promise,
- sendPayment: (pr: string) => Promise
- }
- }
+ interface Window {
+ webln?: {
+ enabled: boolean;
+ enable: () => Promise;
+ sendPayment: (pr: string) => Promise;
+ };
+ }
}
export default function useWebln(enable = true) {
- const maybeWebLn = "webln" in window ? window.webln : null
+ const maybeWebLn = "webln" in window ? window.webln : null;
useEffect(() => {
if (maybeWebLn && !maybeWebLn.enabled && enable) {
maybeWebLn.enable().catch((error) => {
- console.debug("Couldn't enable WebLN")
- })
+ console.debug("Couldn't enable WebLN");
+ });
}
- }, [maybeWebLn, enable])
+ }, [maybeWebLn, enable]);
- return maybeWebLn
+ return maybeWebLn;
}
diff --git a/src/Icons/ArrowBack.tsx b/src/Icons/ArrowBack.tsx
index 542b8295..ee19bd9c 100644
--- a/src/Icons/ArrowBack.tsx
+++ b/src/Icons/ArrowBack.tsx
@@ -1,9 +1,21 @@
const ArrowBack = () => {
return (
-