diff --git a/src/element/collapsible.css b/src/element/collapsible.css
new file mode 100644
index 0000000..3c8fddc
--- /dev/null
+++ b/src/element/collapsible.css
@@ -0,0 +1,20 @@
+.collapsible-media {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.collapsible-media a {
+ color: var(--text-link);
+ word-wrap: break-word;
+}
+
+.collapsible-media img,
+.collapsible-media video {
+ width: 100%;
+}
+
+.url-preview {
+ color: var(--text-link);
+ cursor: zoom-in;
+}
diff --git a/src/element/collapsible.tsx b/src/element/collapsible.tsx
new file mode 100644
index 0000000..c7b90b1
--- /dev/null
+++ b/src/element/collapsible.tsx
@@ -0,0 +1,32 @@
+import "./collapsible.css";
+import * as Dialog from "@radix-ui/react-dialog";
+import type { ReactNode } from "react";
+import { ExternalLink } from "element/external-link";
+
+interface MediaURLProps {
+ url: URL;
+ children: ReactNode;
+}
+
+export function MediaURL({ url, children }: MediaURLProps) {
+ const preview = {url.toString()};
+ return (
+
+ {preview}
+
+
+
+
+ {url.toString()}
+ {children}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/element/hypertext.tsx b/src/element/hypertext.tsx
index 34293eb..d65be95 100644
--- a/src/element/hypertext.tsx
+++ b/src/element/hypertext.tsx
@@ -1,5 +1,6 @@
import type { ReactNode } from "react";
import { NostrLink } from "element/nostr-link";
+import { MediaURL } from "element/collapsible";
const FileExtensionRegex = /\.([\w]+)$/i;
@@ -23,17 +24,23 @@ export function HyperText({ link, children }: HyperTextProps) {
case "bmp":
case "webp": {
return (
-
+
+
+
);
}
case "wav":
case "mp3":
case "ogg": {
- return ;
+ return (
+
+ ;
+
+ );
}
case "mp4":
case "mov":
@@ -41,7 +48,11 @@ export function HyperText({ link, children }: HyperTextProps) {
case "avi":
case "m4v":
case "webm": {
- return ;
+ return (
+
+
+
+ );
}
default:
return {children || url.toString()};
diff --git a/src/element/markdown.css b/src/element/markdown.css
index d52ac78..f3cc513 100644
--- a/src/element/markdown.css
+++ b/src/element/markdown.css
@@ -25,3 +25,9 @@
width: 100%;
border-radius: 6px;
}
+
+.markdown video {
+ width: 100%;
+ aspect-ratio: 4/3;
+ border-radius: 6px;
+}
diff --git a/src/element/markdown.tsx b/src/element/markdown.tsx
index 9ddadb3..313ed45 100644
--- a/src/element/markdown.tsx
+++ b/src/element/markdown.tsx
@@ -30,6 +30,9 @@ export function Markdown({ content, tags = [] }: MarkdownProps) {
td: ({ children }: ComponentProps) => {
return children &&
{transformText(children, tags)} | ;
},
+ th: ({ children }: ComponentProps) => {
+ return children && {transformText(children, tags)} | ;
+ },
p: ({ children }: ComponentProps) => {
return children && {transformText(children, tags)}
;
},
diff --git a/src/element/profile.tsx b/src/element/profile.tsx
index a36fba2..774b6ee 100644
--- a/src/element/profile.tsx
+++ b/src/element/profile.tsx
@@ -4,7 +4,9 @@ import { Link } from "react-router-dom";
import { useUserProfile } from "@snort/system-react";
import { UserMetadata } from "@snort/system";
import { hexToBech32 } from "@snort/shared";
+
import { Icon } from "element/icon";
+import usePlaceholder from "hooks/placeholders";
import { System } from "index";
import { useInView } from "react-intersection-observer";
@@ -47,6 +49,7 @@ export function Profile({
useUserProfile(System, inView && !profile ? pubkey : undefined) || profile;
const showAvatar = options?.showAvatar ?? true;
const showName = options?.showName ?? true;
+ const placeholder = usePlaceholder(pubkey);
const content = (
<>
@@ -57,7 +60,7 @@ export function Profile({
))}
{icon}
diff --git a/src/element/text.css b/src/element/text.css
index 62e4fdf..c4228f4 100644
--- a/src/element/text.css
+++ b/src/element/text.css
@@ -4,3 +4,10 @@
width: 100%;
border-radius: 6px;
}
+
+.text video {
+ width: 100%;
+ margin-top: 8px;
+ aspect-ratio: 4/3;
+ border-radius: 6px;
+}
diff --git a/src/element/text.tsx b/src/element/text.tsx
index 21327dd..6d2b92c 100644
--- a/src/element/text.tsx
+++ b/src/element/text.tsx
@@ -9,6 +9,7 @@ import { Mention } from "element/mention";
import { Emoji } from "element/emoji";
import { HyperText } from "element/hypertext";
import { splitByUrl } from "utils";
+import type { Tags } from "types";
export type Fragment = string | ReactNode;
@@ -188,7 +189,12 @@ export function transformText(ps: Fragment[], tags: Array) {
return fragments;
}
-export function Text({ content, tags }: { content: string; tags: string[][] }) {
+interface TextProps {
+ content: string;
+ tags: Tags;
+}
+
+export function Text({ content, tags }: TextProps) {
// todo: RTL langugage support
const element = useMemo(() => {
return {transformText([content], tags)};
diff --git a/src/hooks/placeholders.ts b/src/hooks/placeholders.ts
new file mode 100644
index 0000000..98f62e2
--- /dev/null
+++ b/src/hooks/placeholders.ts
@@ -0,0 +1,9 @@
+import { useMemo } from "react";
+
+export default function usePlaceholder(pubkey: string) {
+ const url = useMemo(
+ () => `https://robohash.v0l.io/${pubkey}.png?set=2`,
+ [pubkey]
+ );
+ return url;
+}
diff --git a/src/index.css b/src/index.css
index a0cf362..b9c6702 100644
--- a/src/index.css
+++ b/src/index.css
@@ -254,6 +254,13 @@ div.paper {
margin: 6px;
}
+.dialog-trigger {
+ font-size: 15px;
+ background: transparent;
+ border: none;
+ display: inline;
+}
+
.ctx-menu {
font-size: 16px;
font-weight: 700;
diff --git a/src/pages/profile-page.css b/src/pages/profile-page.css
index b809dc9..f1c323b 100644
--- a/src/pages/profile-page.css
+++ b/src/pages/profile-page.css
@@ -57,11 +57,18 @@
.profile-page .profile-actions {
position: absolute;
display: flex;
- gap: 12px;
+ align-items: flex-start;
+ gap: 4px;
top: 12px;
right: 12px;
}
+@media (min-width: 480px) {
+ .profile-page .profile-actions {
+ gap: 12px;
+ }
+}
+
.profile-page .profile-information {
margin: 12px;
margin-left: 16px;
@@ -87,22 +94,11 @@
line-height: 24px;
}
-.profile-page .icon-button {
+.profile-page .zap-button {
display: flex;
- align-items: center;
gap: 8px;
}
-.profile-page .icon-button span {
- display: none;
-}
-
-@media (min-width: 480px) {
- .profile-page .icon-button span {
- display: block;
- }
-}
-
.profile-page .zap-button-icon {
color: #171717;
}
@@ -234,3 +230,19 @@
font-weight: 500;
line-height: 24px;
}
+
+.profile-page .live-button span {
+ display: none;
+}
+.profile-page .zap-button span {
+ display: none;
+}
+
+@media (min-width: 480px) {
+ .profile-page .zap-button span {
+ display: block;
+ }
+ .profile-page .live-button span {
+ display: block;
+ }
+}
diff --git a/src/pages/profile-page.tsx b/src/pages/profile-page.tsx
index 55183ec..b4ad6fd 100644
--- a/src/pages/profile-page.tsx
+++ b/src/pages/profile-page.tsx
@@ -18,6 +18,7 @@ import { FollowButton } from "element/follow-button";
import { MuteButton } from "element/mute-button";
import { useProfile } from "hooks/profile";
import useTopZappers from "hooks/top-zappers";
+import usePlaceholder from "hooks/placeholders";
import { Text } from "element/text";
import { StreamState, System } from "index";
import { findTag } from "utils";
@@ -52,6 +53,7 @@ export function ProfilePage() {
const navigate = useNavigate();
const params = useParams();
const link = parseNostrLink(params.npub!);
+ const placeholder = usePlaceholder(link.id);
const profile = useUserProfile(System, link.id);
const zapTarget = profile?.lud16 ?? profile?.lud06;
const { streams, zaps } = useProfile(link, true);
@@ -91,16 +93,22 @@ export function ProfilePage() {
src={profile?.banner || defaultBanner}
/>
- {profile?.picture && (
+ {profile?.picture ? (
![{profile.name]({profile.picture})
+ ) : (
+
![{profile?.name]({placeholder})
)}
{isLive ? (
-
+
live
@@ -122,9 +130,9 @@ export function ProfilePage() {
lnurl={zapTarget}
button={