forked from Kieran/zap.stream
feat: display media URLs on click
This commit is contained in:
parent
7a6e43d638
commit
a11eeef698
20
src/element/collapsible.css
Normal file
20
src/element/collapsible.css
Normal file
@ -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;
|
||||
}
|
32
src/element/collapsible.tsx
Normal file
32
src/element/collapsible.tsx
Normal file
@ -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 = <span className="url-preview">{url.toString()}</span>;
|
||||
return (
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger asChild>{preview}</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="dialog-overlay" />
|
||||
<Dialog.Content className="dialog-content">
|
||||
<div className="collapsible-media">
|
||||
<ExternalLink href={url.toString()}>{url.toString()}</ExternalLink>
|
||||
{children}
|
||||
</div>
|
||||
<Dialog.Close asChild>
|
||||
<button className="btn delete-button" aria-label="Close">
|
||||
Close
|
||||
</button>
|
||||
</Dialog.Close>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<img
|
||||
src={url.toString()}
|
||||
alt={url.toString()}
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
<MediaURL url={url}>
|
||||
<img
|
||||
src={url.toString()}
|
||||
alt={url.toString()}
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
</MediaURL>
|
||||
);
|
||||
}
|
||||
case "wav":
|
||||
case "mp3":
|
||||
case "ogg": {
|
||||
return <audio key={url.toString()} src={url.toString()} controls />;
|
||||
return (
|
||||
<MediaURL url={url}>
|
||||
<audio key={url.toString()} src={url.toString()} controls />;
|
||||
</MediaURL>
|
||||
);
|
||||
}
|
||||
case "mp4":
|
||||
case "mov":
|
||||
@ -41,7 +48,11 @@ export function HyperText({ link, children }: HyperTextProps) {
|
||||
case "avi":
|
||||
case "m4v":
|
||||
case "webm": {
|
||||
return <video key={url.toString()} src={url.toString()} controls />;
|
||||
return (
|
||||
<MediaURL url={url}>
|
||||
<video key={url.toString()} src={url.toString()} controls />
|
||||
</MediaURL>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return <a href={url.toString()}>{children || url.toString()}</a>;
|
||||
|
@ -25,3 +25,9 @@
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.markdown video {
|
||||
width: 100%;
|
||||
aspect-ratio: 4/3;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
@ -30,6 +30,9 @@ export function Markdown({ content, tags = [] }: MarkdownProps) {
|
||||
td: ({ children }: ComponentProps) => {
|
||||
return children && <td>{transformText(children, tags)}</td>;
|
||||
},
|
||||
th: ({ children }: ComponentProps) => {
|
||||
return children && <th>{transformText(children, tags)}</th>;
|
||||
},
|
||||
p: ({ children }: ComponentProps) => {
|
||||
return children && <p>{transformText(children, tags)}</p>;
|
||||
},
|
||||
|
@ -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({
|
||||
<img
|
||||
alt={pLoaded?.name || pubkey}
|
||||
className={avatarClassname ? avatarClassname : ""}
|
||||
src={pLoaded?.picture ?? ""}
|
||||
src={pLoaded?.picture ?? placeholder}
|
||||
/>
|
||||
))}
|
||||
{icon}
|
||||
|
@ -4,3 +4,10 @@
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.text video {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
aspect-ratio: 4/3;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
@ -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<string[]>) {
|
||||
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 <span className="text">{transformText([content], tags)}</span>;
|
||||
|
9
src/hooks/placeholders.ts
Normal file
9
src/hooks/placeholders.ts
Normal file
@ -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;
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
<div className="profile-content">
|
||||
{profile?.picture && (
|
||||
{profile?.picture ? (
|
||||
<img
|
||||
className="avatar"
|
||||
alt={profile.name || link.id}
|
||||
src={profile.picture}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
className="avatar"
|
||||
alt={profile?.name || link.id}
|
||||
src={placeholder}
|
||||
/>
|
||||
)}
|
||||
<div className="status-indicator">
|
||||
{isLive ? (
|
||||
<div className="icon-button pill live" onClick={goToLive}>
|
||||
<div className="live-button pill live" onClick={goToLive}>
|
||||
<Icon name="signal" />
|
||||
<span>live</span>
|
||||
</div>
|
||||
@ -122,9 +130,9 @@ export function ProfilePage() {
|
||||
lnurl={zapTarget}
|
||||
button={
|
||||
<button className="btn">
|
||||
<div className="icon-button">
|
||||
<span>Zap</span>
|
||||
<div className="zap-button">
|
||||
<Icon name="zap-filled" className="zap-button-icon" />
|
||||
<span>Zap</span>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user