feat: display media URLs on click

This commit is contained in:
verbiricha 2023-08-02 09:31:45 +02:00
parent 7a6e43d638
commit a11eeef698
12 changed files with 150 additions and 26 deletions

View 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;
}

View 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>
);
}

View File

@ -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>;

View File

@ -25,3 +25,9 @@
width: 100%;
border-radius: 6px;
}
.markdown video {
width: 100%;
aspect-ratio: 4/3;
border-radius: 6px;
}

View File

@ -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>;
},

View File

@ -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}

View File

@ -4,3 +4,10 @@
width: 100%;
border-radius: 6px;
}
.text video {
width: 100%;
margin-top: 8px;
aspect-ratio: 4/3;
border-radius: 6px;
}

View File

@ -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>;

View 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;
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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>
}