diff --git a/packages/app/public/icons.svg b/packages/app/public/icons.svg
index 007d2888..382fecd3 100644
--- a/packages/app/public/icons.svg
+++ b/packages/app/public/icons.svg
@@ -178,5 +178,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/app/src/Element/HyperText.tsx b/packages/app/src/Element/HyperText.tsx
index 685911af..32b4548c 100644
--- a/packages/app/src/Element/HyperText.tsx
+++ b/packages/app/src/Element/HyperText.tsx
@@ -1,7 +1,6 @@
import { TwitterTweetEmbed } from "react-twitter-embed";
import {
- FileExtensionRegex,
YoutubeUrlRegex,
TweetUrlRegex,
TidalRegex,
@@ -23,17 +22,14 @@ import AppleMusicEmbed from "Element/AppleMusicEmbed";
import WavlakeEmbed from "Element/WavlakeEmbed";
import LinkPreview from "Element/LinkPreview";
import NostrLink from "Element/NostrLink";
-import RevealMedia from "Element/RevealMedia";
import MagnetLink from "Element/MagnetLink";
interface HypeTextProps {
link: string;
- creator: string;
depth?: number;
- disableMediaSpotlight?: boolean;
}
-export default function HyperText({ link, creator, depth, disableMediaSpotlight }: HypeTextProps) {
+export default function HyperText({ link, depth }: HypeTextProps) {
const a = link;
try {
const url = new URL(a);
@@ -47,10 +43,7 @@ export default function HyperText({ link, creator, depth, disableMediaSpotlight
const isAppleMusicLink = AppleMusicRegex.test(a);
const isNostrNestsLink = NostrNestsRegex.test(a);
const isWavlakeLink = WavlakeRegex.test(a);
- const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
- if (extension && !isAppleMusicLink) {
- return ;
- } else if (tweetId) {
+ if (tweetId) {
return (
diff --git a/packages/app/src/Element/LinkPreview.css b/packages/app/src/Element/LinkPreview.css
index d7ab0e69..6e7ff60a 100644
--- a/packages/app/src/Element/LinkPreview.css
+++ b/packages/app/src/Element/LinkPreview.css
@@ -1,6 +1,6 @@
.link-preview-container {
- border: 1px solid var(--gray);
- border-radius: 10px;
+ border-radius: 0px 0px 12px 12px;
+ background: #151515;
overflow: hidden;
}
diff --git a/packages/app/src/Element/MediaElement.tsx b/packages/app/src/Element/MediaElement.tsx
index 779be25e..c0001364 100644
--- a/packages/app/src/Element/MediaElement.tsx
+++ b/packages/app/src/Element/MediaElement.tsx
@@ -52,25 +52,29 @@ export function MediaElement(props: MediaElementProps) {
return;
}
- const req = new Request(props.url, {
- method: "OPTIONS",
- headers: {
- accept: "L402",
- },
- });
- const rsp = await fetch(req);
- if (rsp.status === 402) {
- const auth = rsp.headers.get("www-authenticate");
- if (auth?.startsWith("L402")) {
- const vals = kvToObject
(auth.substring(5));
- console.debug(vals);
- setL402(vals);
+ try {
+ const req = new Request(props.url, {
+ method: "OPTIONS",
+ headers: {
+ accept: "L402",
+ },
+ });
+ const rsp = await fetch(req);
+ if (rsp.status === 402) {
+ const auth = rsp.headers.get("www-authenticate");
+ if (auth?.startsWith("L402")) {
+ const vals = kvToObject(auth.substring(5));
+ console.debug(vals);
+ setL402(vals);
- if (vals.invoice) {
- const decoded = decodeInvoice(vals.invoice);
- setInvoice(decoded);
+ if (vals.invoice) {
+ const decoded = decodeInvoice(vals.invoice);
+ setInvoice(decoded);
+ }
}
}
+ } catch (e) {
+ console.error(e);
}
}
diff --git a/packages/app/src/Element/Note.css b/packages/app/src/Element/Note.css
index bd9157a7..091e90fe 100644
--- a/packages/app/src/Element/Note.css
+++ b/packages/app/src/Element/Note.css
@@ -19,16 +19,17 @@
text-decoration-color: var(--highlight);
}
-.note > .header > .info {
+.note .header .info {
font-size: var(--font-size);
margin-left: 4px;
white-space: nowrap;
color: var(--font-secondary-color);
display: flex;
align-items: center;
+ gap: 8px;
}
-.note > .header > .info .saved {
+.note .header .info .saved {
margin-right: 12px;
font-weight: 600;
font-size: 10px;
@@ -39,11 +40,11 @@
align-items: center;
}
-.note > .header > .info .saved svg {
+.note .header .info .saved svg {
margin-right: 8px;
}
-.note > .header > .pinned {
+.note .header .pinned {
font-size: var(--font-size-small);
color: var(--font-secondary-color);
font-weight: 500;
@@ -53,7 +54,7 @@
align-items: center;
}
-.note > .header > .pinned svg {
+.note .header .pinned svg {
margin-right: 8px;
}
@@ -67,10 +68,11 @@
padding-left: 0;
}
-.note > .body {
- margin-top: 4px;
- margin-bottom: 24px;
- padding-left: 56px;
+.note > .body .text-frag {
+ padding-left: 61px;
+}
+
+.note > .body .text-frag {
text-overflow: ellipsis;
white-space: pre-wrap;
word-break: normal;
@@ -78,8 +80,14 @@
overflow-y: visible;
}
+.note > .body img,
+.note > .body video,
+.note > .body audio {
+ margin-top: 16px;
+}
+
.note > .footer {
- padding-left: 46px;
+ padding: 16px 0 0px 61px;
}
.note .footer .footer-reactions {
@@ -88,8 +96,7 @@
align-items: center;
justify-content: center;
margin-left: auto;
- gap: 1em;
- padding-left: 0.8em;
+ gap: 48px;
}
@media (min-width: 720px) {
@@ -98,7 +105,7 @@
}
}
-.note > .footer .ctx-menu {
+.note .ctx-menu {
color: var(--font-secondary-color);
background: transparent;
box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.4);
@@ -108,7 +115,7 @@
border-radius: 16px;
}
-.note > .footer .ctx-menu li {
+.note .ctx-menu li {
background: #1e1e1e;
padding-top: 8px;
padding-bottom: 8px;
@@ -116,28 +123,28 @@
grid-template-columns: 2rem auto;
}
-.light .note > .footer .ctx-menu li {
+.light .note .ctx-menu li {
background: var(--note-bg);
}
-.note > .footer .ctx-menu li:first-of-type {
+.note .ctx-menu li:first-of-type {
padding-top: 12px;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
}
-.note > .footer .ctx-menu li:last-of-type {
+.note .ctx-menu li:last-of-type {
padding-bottom: 12px;
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
}
-.note > .footer .ctx-menu li:hover {
+.note .ctx-menu li:hover {
color: white;
background: #2a2a2a;
}
-.light .note > .footer .ctx-menu li:hover {
+.light .note .ctx-menu li:hover {
color: white;
background: var(--font-secondary-color);
}
@@ -196,11 +203,7 @@
user-select: none;
color: var(--font-secondary-color);
font-feature-settings: "tnum";
-}
-
-.reaction-pill .reaction-pill-number {
- margin-left: 8px;
- font-feature-settings: "tnum";
+ gap: 5px;
}
.reaction-pill.reacted {
diff --git a/packages/app/src/Element/Note.tsx b/packages/app/src/Element/Note.tsx
index 4d61db22..ad880b9a 100644
--- a/packages/app/src/Element/Note.tsx
+++ b/packages/app/src/Element/Note.tsx
@@ -1,5 +1,5 @@
import "./Note.css";
-import React, { useMemo, useState, useLayoutEffect, ReactNode } from "react";
+import React, { useMemo, useState, ReactNode } from "react";
import { useNavigate, Link } from "react-router-dom";
import { useInView } from "react-intersection-observer";
import { useIntl, FormattedMessage } from "react-intl";
@@ -20,7 +20,7 @@ import {
Reaction,
profileLink,
} from "SnortUtils";
-import NoteFooter, { Translation } from "Element/NoteFooter";
+import NoteFooter from "Element/NoteFooter";
import NoteTime from "Element/NoteTime";
import Reveal from "Element/Reveal";
import useModeration from "Hooks/useModeration";
@@ -32,6 +32,8 @@ import { NostrFileElement } from "Element/NostrFileHeader";
import ZapstrEmbed from "Element/ZapstrEmbed";
import PubkeyList from "Element/PubkeyList";
import { LiveEvent } from "Element/LiveEvent";
+import { NoteContextMenu, NoteTranslation } from "Element/NoteContextMenu";
+import Reactions from "Element/Reactions";
import messages from "./messages";
@@ -97,12 +99,10 @@ export default function Note(props: NoteProps) {
const { isMuted } = useModeration();
const isOpMuted = isMuted(ev?.pubkey);
const { ref, inView, entry } = useInView({ triggerOnce: true });
- const [extendable, setExtendable] = useState(false);
- const [showMore, setShowMore] = useState(false);
const login = useLogin();
const { pinned, bookmarked } = login;
const publisher = useEventPublisher();
- const [translated, setTranslated] = useState();
+ const [translated, setTranslated] = useState();
const { formatMessage } = useIntl();
const reactions = useMemo(() => getReactions(related, ev.id, EventKind.Reaction), [related, ev]);
const groupReactions = useMemo(() => {
@@ -208,15 +208,6 @@ export default function Note(props: NoteProps) {
return ;
};
- useLayoutEffect(() => {
- if (entry && inView && extendable === false) {
- const h = (entry?.target as HTMLDivElement)?.offsetHeight ?? 0;
- if (h > 650) {
- setExtendable(true);
- }
- }
- }, [inView, entry, extendable]);
-
function goToEvent(
e: React.MouseEvent,
eTarget: TaggedNostrEvent,
@@ -342,21 +333,31 @@ export default function Note(props: NoteProps) {
subHeader={replyTag() ?? undefined}
link={opt?.canClick === undefined ? undefined : ""}
/>
- {(options.showTime || options.showBookmarked) && (
-
- {options.showBookmarked && (
-
unbookmark(ev.id)}>
-
-
- )}
- {!options.showBookmarked &&
}
-
- )}
- {options.showPinned && (
- unpin(ev.id)}>
-
-
- )}
+
+ {(options.showTime || options.showBookmarked) && (
+ <>
+ {options.showBookmarked && (
+
unbookmark(ev.id)}>
+
+
+ )}
+ {!options.showBookmarked &&
}
+ >
+ )}
+ {options.showPinned && (
+
unpin(ev.id)}>
+
+
+ )}
+
{}}
+ onTranslated={t => setTranslated(t)}
+ setShowReactions={setShowReactions}
+ />
+
)}
goToEvent(e, ev, true)}>
@@ -369,32 +370,21 @@ export default function Note(props: NoteProps) {
)}
- {extendable && !showMore && (
- setShowMore(true)}>
-
-
- )}
- {options.showFooter && (
- setTranslated(t)}
- showReactions={showReactions}
- setShowReactions={setShowReactions}
- />
- )}
+ {options.showFooter && }
+
>
);
}
const note = (
- goToEvent(e, ev)}
- ref={ref}>
+
goToEvent(e, ev)} ref={ref}>
{content()}
);
diff --git a/packages/app/src/Element/NoteContextMenu.tsx b/packages/app/src/Element/NoteContextMenu.tsx
new file mode 100644
index 00000000..b32506b3
--- /dev/null
+++ b/packages/app/src/Element/NoteContextMenu.tsx
@@ -0,0 +1,220 @@
+import { FormattedMessage, useIntl } from "react-intl";
+import { HexKey, Lists, NostrPrefix, TaggedRawEvent, encodeTLV } from "@snort/system";
+import { Menu, MenuItem } from "@szhsin/react-menu";
+import { useDispatch, useSelector } from "react-redux";
+
+import { TranslateHost } from "Const";
+import { System } from "index";
+import Icon from "Icons/Icon";
+import { setPinned, setBookmarked } from "Login";
+import {
+ setNote as setReBroadcastNote,
+ setShow as setReBroadcastShow,
+ reset as resetReBroadcast,
+} from "State/ReBroadcast";
+import messages from "Element/messages";
+import useLogin from "Hooks/useLogin";
+import useModeration from "Hooks/useModeration";
+import useEventPublisher from "Feed/EventPublisher";
+import { RootState } from "State/Store";
+import { ReBroadcaster } from "./ReBroadcaster";
+
+export interface NoteTranslation {
+ text: string;
+ fromLanguage: string;
+ confidence: number;
+}
+
+interface NosteContextMenuProps {
+ ev: TaggedRawEvent;
+ setShowReactions(b: boolean): void;
+ react(content: string): Promise
;
+ onTranslated?: (t: NoteTranslation) => void;
+}
+
+export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
+ const dispatch = useDispatch();
+ const { formatMessage } = useIntl();
+ const login = useLogin();
+ const { pinned, bookmarked, publicKey, preferences: prefs, relays } = login;
+ const { mute, block } = useModeration();
+ const publisher = useEventPublisher();
+ const showReBroadcastModal = useSelector((s: RootState) => s.reBroadcast.show);
+ const reBroadcastNote = useSelector((s: RootState) => s.reBroadcast.note);
+ const willRenderReBroadcast = showReBroadcastModal && reBroadcastNote && reBroadcastNote?.id === ev.id;
+ const lang = window.navigator.language;
+ const langNames = new Intl.DisplayNames([...window.navigator.languages], {
+ type: "language",
+ });
+ const isMine = ev.pubkey === publicKey;
+
+ async function deleteEvent() {
+ if (window.confirm(formatMessage(messages.ConfirmDeletion, { id: ev.id.substring(0, 8) })) && publisher) {
+ const evDelete = await publisher.delete(ev.id);
+ System.BroadcastEvent(evDelete);
+ }
+ }
+
+ async function share() {
+ const link = encodeTLV(NostrPrefix.Event, ev.id, ev.relays);
+ const url = `${window.location.protocol}//${window.location.host}/e/${link}`;
+ if ("share" in window.navigator) {
+ await window.navigator.share({
+ title: "Snort",
+ url: url,
+ });
+ } else {
+ await navigator.clipboard.writeText(url);
+ }
+ }
+
+ async function translate() {
+ const res = await fetch(`${TranslateHost}/translate`, {
+ method: "POST",
+ body: JSON.stringify({
+ q: ev.content,
+ source: "auto",
+ target: lang.split("-")[0],
+ }),
+ headers: { "Content-Type": "application/json" },
+ });
+
+ if (res.ok) {
+ const result = await res.json();
+ if (typeof props.onTranslated === "function" && result) {
+ props.onTranslated({
+ text: result.translatedText,
+ fromLanguage: langNames.of(result.detectedLanguage.language),
+ confidence: result.detectedLanguage.confidence,
+ } as NoteTranslation);
+ }
+ }
+ }
+
+ async function copyId() {
+ const link = encodeTLV(NostrPrefix.Event, ev.id, ev.relays);
+ await navigator.clipboard.writeText(link);
+ }
+
+ async function pin(id: HexKey) {
+ if (publisher) {
+ const es = [...pinned.item, id];
+ const ev = await publisher.noteList(es, Lists.Pinned);
+ System.BroadcastEvent(ev);
+ setPinned(login, es, ev.created_at * 1000);
+ }
+ }
+
+ async function bookmark(id: HexKey) {
+ if (publisher) {
+ const es = [...bookmarked.item, id];
+ const ev = await publisher.noteList(es, Lists.Bookmarked);
+ System.BroadcastEvent(ev);
+ setBookmarked(login, es, ev.created_at * 1000);
+ }
+ }
+
+ async function copyEvent() {
+ await navigator.clipboard.writeText(JSON.stringify(ev, undefined, " "));
+ }
+
+ const handleReBroadcastButtonClick = () => {
+ if (reBroadcastNote?.id !== ev.id) {
+ dispatch(resetReBroadcast());
+ }
+
+ dispatch(setReBroadcastNote(ev));
+ dispatch(setReBroadcastShow(!showReBroadcastModal));
+ };
+
+ function menuItems() {
+ return (
+ <>
+
+ {/* This menu item serves as a "close menu" button;
+ it allows the user to click anywhere nearby the menu to close it. */}
+
+
+
+
+ {!pinned.item.includes(ev.id) && (
+
+ )}
+ {!bookmarked.item.includes(ev.id) && (
+
+ )}
+
+
+ {prefs.enableReactions && (
+
+ )}
+ {ev.pubkey === publicKey && (
+
+ )}
+ {ev.pubkey !== publicKey && (
+
+ )}
+
+ {prefs.showDebugMenus && (
+
+ )}
+ {isMine && (
+
+ )}
+ >
+ );
+ }
+
+ return (
+ <>
+
+ }
+ menuClassName="ctx-menu">
+ {menuItems()}
+
+ {willRenderReBroadcast && }
+ >
+ );
+}
diff --git a/packages/app/src/Element/NoteFooter.tsx b/packages/app/src/Element/NoteFooter.tsx
index 58ed5ae4..cadd5ffe 100644
--- a/packages/app/src/Element/NoteFooter.tsx
+++ b/packages/app/src/Element/NoteFooter.tsx
@@ -1,9 +1,8 @@
import React, { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
-import { useIntl, FormattedMessage } from "react-intl";
-import { Menu, MenuItem } from "@szhsin/react-menu";
+import { useIntl } from "react-intl";
import { useLongPress } from "use-long-press";
-import { TaggedNostrEvent, HexKey, u256, encodeTLV, NostrPrefix, Lists, ParsedZap } from "@snort/system";
+import { TaggedNostrEvent, HexKey, u256, ParsedZap } from "@snort/system";
import { LNURL } from "@snort/shared";
import { useUserProfile } from "@snort/system-react";
@@ -14,22 +13,13 @@ import { formatShort } from "Number";
import useEventPublisher from "Feed/EventPublisher";
import { delay, normalizeReaction, unwrap } from "SnortUtils";
import { NoteCreator } from "Element/NoteCreator";
-import { ReBroadcaster } from "Element/ReBroadcaster";
-import Reactions from "Element/Reactions";
import SendSats from "Element/SendSats";
import { ZapsSummary } from "Element/Zap";
import { RootState } from "State/Store";
import { setReplyTo, setShow, reset } from "State/NoteCreator";
-import {
- setNote as setReBroadcastNote,
- setShow as setReBroadcastShow,
- reset as resetReBroadcast,
-} from "State/ReBroadcast";
-import useModeration from "Hooks/useModeration";
-import { TranslateHost } from "Const";
+
import { useWallet } from "Wallet";
import useLogin from "Hooks/useLogin";
-import { setBookmarked, setPinned } from "Login";
import { useInteractionCache } from "Hooks/useInteractionCache";
import { ZapPoolController } from "ZapPoolController";
import { System } from "index";
@@ -49,49 +39,31 @@ const barrierZapper = async (then: () => Promise): Promise => {
}
};
-export interface Translation {
- text: string;
- fromLanguage: string;
- confidence: number;
-}
-
export interface NoteFooterProps {
reposts: TaggedNostrEvent[];
zaps: ParsedZap[];
positive: TaggedNostrEvent[];
- negative: TaggedNostrEvent[];
- showReactions: boolean;
- setShowReactions(b: boolean): void;
ev: TaggedNostrEvent;
- onTranslated?: (content: Translation) => void;
}
export default function NoteFooter(props: NoteFooterProps) {
- const { ev, showReactions, setShowReactions, positive, negative, reposts, zaps } = props;
+ const { ev, positive, reposts, zaps } = props;
const dispatch = useDispatch();
const { formatMessage } = useIntl();
const login = useLogin();
- const { pinned, bookmarked, publicKey, preferences: prefs, relays } = login;
- const { mute, block } = useModeration();
+ const { publicKey, preferences: prefs, relays } = login;
const author = useUserProfile(System, ev.pubkey);
const interactionCache = useInteractionCache(publicKey, ev.id);
const publisher = useEventPublisher();
const showNoteCreatorModal = useSelector((s: RootState) => s.noteCreator.show);
- const showReBroadcastModal = useSelector((s: RootState) => s.reBroadcast.show);
- const reBroadcastNote = useSelector((s: RootState) => s.reBroadcast.note);
const replyTo = useSelector((s: RootState) => s.noteCreator.replyTo);
const willRenderNoteCreator = showNoteCreatorModal && replyTo?.id === ev.id;
- const willRenderReBroadcast = showReBroadcastModal && reBroadcastNote && reBroadcastNote?.id === ev.id;
const [tip, setTip] = useState(false);
const [zapping, setZapping] = useState(false);
const walletState = useWallet();
const wallet = walletState.wallet;
const isMine = ev.pubkey === publicKey;
- const lang = window.navigator.language;
- const langNames = new Intl.DisplayNames([...window.navigator.languages], {
- type: "language",
- });
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
const didZap = interactionCache.data.zapped || zaps.some(a => a.sender === publicKey);
const longPress = useLongPress(
@@ -123,13 +95,6 @@ export default function NoteFooter(props: NoteFooterProps) {
}
}
- async function deleteEvent() {
- if (window.confirm(formatMessage(messages.ConfirmDeletion, { id: ev.id.substring(0, 8) })) && publisher) {
- const evDelete = await publisher.delete(ev.id);
- System.BroadcastEvent(evDelete);
- }
- }
-
async function repost() {
if (!hasReposted() && publisher) {
if (!prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.id }))) {
@@ -248,145 +213,6 @@ export default function NoteFooter(props: NoteFooterProps) {
);
}
- async function share() {
- const link = encodeTLV(NostrPrefix.Event, ev.id, ev.relays);
- const url = `${window.location.protocol}//${window.location.host}/e/${link}`;
- if ("share" in window.navigator) {
- await window.navigator.share({
- title: "Snort",
- url: url,
- });
- } else {
- await navigator.clipboard.writeText(url);
- }
- }
-
- async function translate() {
- const res = await fetch(`${TranslateHost}/translate`, {
- method: "POST",
- body: JSON.stringify({
- q: ev.content,
- source: "auto",
- target: lang.split("-")[0],
- }),
- headers: { "Content-Type": "application/json" },
- });
-
- if (res.ok) {
- const result = await res.json();
- if (typeof props.onTranslated === "function" && result) {
- props.onTranslated({
- text: result.translatedText,
- fromLanguage: langNames.of(result.detectedLanguage.language),
- confidence: result.detectedLanguage.confidence,
- } as Translation);
- }
- }
- }
-
- async function copyId() {
- const link = encodeTLV(NostrPrefix.Event, ev.id, ev.relays);
- await navigator.clipboard.writeText(link);
- }
-
- async function pin(id: HexKey) {
- if (publisher) {
- const es = [...pinned.item, id];
- const ev = await publisher.noteList(es, Lists.Pinned);
- System.BroadcastEvent(ev);
- setPinned(login, es, ev.created_at * 1000);
- }
- }
-
- async function bookmark(id: HexKey) {
- if (publisher) {
- const es = [...bookmarked.item, id];
- const ev = await publisher.noteList(es, Lists.Bookmarked);
- System.BroadcastEvent(ev);
- setBookmarked(login, es, ev.created_at * 1000);
- }
- }
-
- async function copyEvent() {
- await navigator.clipboard.writeText(JSON.stringify(ev, undefined, " "));
- }
-
- function menuItems() {
- return (
- <>
-
- {/* This menu item serves as a "close menu" button;
- it allows the user to click anywhere nearby the menu to close it. */}
-
-
-
-
- {!pinned.item.includes(ev.id) && (
-
- )}
- {!bookmarked.item.includes(ev.id) && (
-
- )}
-
-
- {prefs.enableReactions && (
-
- )}
- {ev.pubkey === publicKey && (
-
- )}
- {ev.pubkey !== publicKey && (
-
- )}
-
- {prefs.showDebugMenus && (
-
- )}
- {isMine && (
-
- )}
- >
- );
- }
-
const handleReplyButtonClick = () => {
if (replyTo?.id !== ev.id) {
dispatch(reset());
@@ -396,15 +222,6 @@ export default function NoteFooter(props: NoteFooterProps) {
dispatch(setShow(!showNoteCreatorModal));
};
- const handleReBroadcastButtonClick = () => {
- if (reBroadcastNote?.id !== ev.id) {
- dispatch(resetReBroadcast());
- }
-
- dispatch(setReBroadcastNote(ev));
- dispatch(setReBroadcastShow(!showReBroadcastModal));
- };
-
return (
<>
@@ -415,26 +232,8 @@ export default function NoteFooter(props: NoteFooterProps) {
-
- }
- menuClassName="ctx-menu">
- {menuItems()}
-
{willRenderNoteCreator && }
- {willRenderReBroadcast && }
-
setTip(false)}
diff --git a/packages/app/src/Element/ProfileImage.css b/packages/app/src/Element/ProfileImage.css
index 8f346215..4f3c3031 100644
--- a/packages/app/src/Element/ProfileImage.css
+++ b/packages/app/src/Element/ProfileImage.css
@@ -5,11 +5,7 @@
text-decoration: none;
user-select: none;
min-width: 0;
-}
-
-.pfp .avatar-wrapper {
- margin-right: 8px;
- z-index: 2;
+ gap: 12px;
}
.pfp .avatar {
diff --git a/packages/app/src/Element/Text.css b/packages/app/src/Element/Text.css
index 3ede68b3..cf8f6a92 100644
--- a/packages/app/src/Element/Text.css
+++ b/packages/app/src/Element/Text.css
@@ -1,16 +1,18 @@
.text {
font-size: var(--font-size);
line-height: 24px;
- white-space: pre-wrap;
- word-break: break-word;
}
-.text > a {
+.text .text-frag > a {
color: var(--highlight);
text-decoration: none;
}
-.text a:hover {
+.text .text-frag > a:hover {
+ text-decoration: underline;
+}
+
+.text .text-frag .hashtag:hover {
text-decoration: underline;
}
@@ -65,11 +67,8 @@
.text video,
.text iframe,
.text audio {
- max-width: 100%;
- max-height: 500px;
- margin: 10px auto;
+ width: 100%;
display: block;
- border-radius: 12px;
}
.text iframe,
diff --git a/packages/app/src/Element/Text.tsx b/packages/app/src/Element/Text.tsx
index 87585a92..6bc5253d 100644
--- a/packages/app/src/Element/Text.tsx
+++ b/packages/app/src/Element/Text.tsx
@@ -1,23 +1,13 @@
import "./Text.css";
import { useMemo } from "react";
-import { Link, useLocation } from "react-router-dom";
-import { HexKey, NostrPrefix, validateNostrLink } from "@snort/system";
+import { HexKey, ParsedFragment, transformText } from "@snort/system";
-import { MentionRegex, InvoiceRegex, HashtagRegex, CashuRegex } from "Const";
-import { eventLink, hexToBech32, splitByUrl } from "SnortUtils";
import Invoice from "Element/Invoice";
import Hashtag from "Element/Hashtag";
-import Mention from "Element/Mention";
import HyperText from "Element/HyperText";
import CashuNuts from "Element/CashuNuts";
-import { ProxyImg } from "Element/ProxyImg";
-
-export type Fragment = string | React.ReactNode;
-
-export interface TextFragment {
- body: React.ReactNode[];
- tags: Array>;
-}
+import RevealMedia from "./RevealMedia";
+import { ProxyImg } from "./ProxyImg";
export interface TextProps {
content: string;
@@ -29,168 +19,64 @@ export interface TextProps {
}
export default function Text({ content, tags, creator, disableMedia, depth, disableMediaSpotlight }: TextProps) {
- const location = useLocation();
-
- function extractLinks(fragments: Fragment[]) {
- return fragments
- .map(f => {
- if (typeof f === "string") {
- return splitByUrl(f).map(a => {
- const validateLink = () => {
- const normalizedStr = a.toLowerCase();
-
- if (normalizedStr.startsWith("web+nostr:") || normalizedStr.startsWith("nostr:")) {
- return validateNostrLink(normalizedStr);
- }
-
- return (
- normalizedStr.startsWith("http:") ||
- normalizedStr.startsWith("https:") ||
- normalizedStr.startsWith("magnet:")
- );
- };
-
- if (validateLink()) {
- if ((disableMedia ?? false) && !a.startsWith("nostr:")) {
- return (
- e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
- {a}
-
- );
- }
- return (
-
- );
+ function renderChunk(f: Array) {
+ if (f.every(a => a.type === "media") && f.length === 1) {
+ if (disableMedia ?? false) {
+ return (
+ e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
+ {f[0].content}
+
+ );
+ }
+ return ;
+ } else {
+ return (
+
+ {f.map(a => {
+ switch (a.type) {
+ case "invoice":
+ return
;
+ case "hashtag":
+ return
;
+ case "cashu":
+ return
;
+ case "media":
+ case "link":
+ return
;
+ case "custom_emoji":
+ return
;
+ default:
+ return <>{a.content}>;
}
- return a;
- });
+ })}
+
+ );
+ }
+ }
+ const elements = useMemo(() => {
+ const frags = transformText(content, tags);
+ const chunked = frags.reduce((acc, v) => {
+ if (v.type === "media" && !(v.mimeType?.startsWith("unknown") ?? true)) {
+ if (acc.length === 0) {
+ acc.push([], [v]);
+ } else {
+ acc.push([v]);
}
- return f;
- })
- .flat();
- }
-
- function extractCashuTokens(fragments: Fragment[]) {
- return fragments
- .map(f => {
- if (typeof f === "string" && f.includes("cashuA")) {
- return f.split(CashuRegex).map(a => {
- return ;
- });
+ } else {
+ if (acc.length === 0) {
+ acc.push([v]);
+ } else {
+ acc[0].push(v);
}
- return f;
- })
- .flat();
- }
-
- function extractMentions(frag: TextFragment) {
- return frag.body
- .map(f => {
- if (typeof f === "string") {
- return f.split(MentionRegex).map(match => {
- const matchTag = match.match(/#\[(\d+)\]/);
- if (matchTag && matchTag.length === 2) {
- const idx = parseInt(matchTag[1]);
- const ref = frag.tags?.[idx];
- if (ref) {
- switch (ref[0]) {
- case "p": {
- return ;
- }
- case "e": {
- const eText = hexToBech32(NostrPrefix.Event, ref[1]).substring(0, 12);
- return (
- ref[1] && (
- e.stopPropagation()}
- state={{ from: location.pathname }}>
- #{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 extractCustomEmoji(fragments: Fragment[]) {
- return fragments
- .map(f => {
- if (typeof f === "string") {
- return f.split(/:(\w+):/g).map(i => {
- const t = tags.find(a => a[0] === "emoji" && a[1] === i);
- if (t) {
- return ;
- } else {
- return i;
- }
- });
- }
- return f;
- })
- .flat();
- }
-
- function transformText(frag: TextFragment) {
- let fragments = extractMentions(frag);
- fragments = extractLinks(fragments);
- fragments = extractInvoices(fragments);
- fragments = extractHashtags(fragments);
- fragments = extractCashuTokens(fragments);
- fragments = extractCustomEmoji(fragments);
- return fragments;
- }
-
- const element = useMemo(() => {
- return {transformText({ body: [content], tags })}
;
+ }
+ return acc;
+ }, [] as Array>);
+ return chunked.reverse();
}, [content]);
- return {element}
;
+ return (
+
+ {elements.map(a => renderChunk(a))}
+
+ );
}
diff --git a/packages/app/src/Pages/Layout.css b/packages/app/src/Pages/Layout.css
index 38f50842..19b281e0 100644
--- a/packages/app/src/Pages/Layout.css
+++ b/packages/app/src/Pages/Layout.css
@@ -12,14 +12,15 @@
header {
display: flex;
- flex-direction: row;
- align-items: center;
+ padding: 10px 16px;
justify-content: space-between;
+ align-items: center;
+ align-self: stretch;
}
.header-actions .avatar {
- width: 48px;
- height: 48px;
+ width: 40px;
+ height: 40px;
cursor: pointer;
}
diff --git a/packages/app/src/Pages/Layout.tsx b/packages/app/src/Pages/Layout.tsx
index 667767e5..7f2e5533 100644
--- a/packages/app/src/Pages/Layout.tsx
+++ b/packages/app/src/Pages/Layout.tsx
@@ -12,7 +12,6 @@ import { RootState } from "State/Store";
import { setShow, reset } from "State/NoteCreator";
import { System } from "index";
import useLoginFeed from "Feed/LoginFeed";
-import useModeration from "Hooks/useModeration";
import { NoteCreator } from "Element/NoteCreator";
import { mapPlanName } from "./subscribe";
import useLogin from "Hooks/useLogin";
@@ -103,7 +102,7 @@ export default function Layout() {
return (
{!shouldHideHeader && (
-