Skip to content

Commit

Permalink
feat: html video and audio inscription types, closes #4077 and #3556
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Dec 14, 2023
1 parent 75f4998 commit 3bddc81
Show file tree
Hide file tree
Showing 14 changed files with 256 additions and 36 deletions.
11 changes: 8 additions & 3 deletions scripts/generate-manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,15 @@ const environmentIcons = {
},
};

const devCsp =
"script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; frame-src ordinals.com; frame-ancestors 'none';";

const prodCsp = `default-src 'none'; connect-src *; style-src 'unsafe-inline'; img-src 'self' data: https:; script-src 'self' 'wasm-unsafe-eval'; object-src 'none'; frame-src ordinals.com; frame-ancestors 'none';`;

const contentSecurityPolicyEnvironment = {
development:
"script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; frame-src 'none'; frame-ancestors 'none';",
production: `default-src 'none'; connect-src *; style-src 'unsafe-inline'; img-src 'self' data: https:; script-src 'self' 'wasm-unsafe-eval'; object-src 'none'; frame-src 'none'; frame-ancestors 'none';`,
testing: prodCsp,
development: devCsp,
production: prodCsp,
};

const defaultIconEnvironment = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ReactNode } from 'react';

import { AudioIcon } from '@app/ui/components/icons/audio-icon';

import { CollectibleItemLayout, CollectibleItemLayoutProps } from '../collectible-item.layout';
import { CollectiblePlaceholderLayout } from './collectible-placeholder.layout';

interface CollectibleAudioProps extends Omit<CollectibleItemLayoutProps, 'children'> {
icon: ReactNode;
}
export function CollectibleAudio({ icon, ...props }: CollectibleAudioProps) {
return (
<CollectibleItemLayout collectibleTypeIcon={icon} {...props}>
<CollectiblePlaceholderLayout>
<AudioIcon size="xl" />
</CollectiblePlaceholderLayout>
</CollectibleItemLayout>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ReactNode, useState } from 'react';

import { Iframe } from '@app/ui/components/iframe';

import { CollectibleItemLayout, CollectibleItemLayoutProps } from '../collectible-item.layout';
import { ImageUnavailable } from '../image-unavailable';

interface CollectibleIframeProps extends Omit<CollectibleItemLayoutProps, 'children'> {
icon: ReactNode;
src: string;
}
export function CollectibleIframe({ icon, src, ...props }: CollectibleIframeProps) {
const [isError, setIsError] = useState(false);

if (isError)
return (
<CollectibleItemLayout collectibleTypeIcon={icon} {...props}>
<ImageUnavailable />
</CollectibleItemLayout>
);

return (
<CollectibleItemLayout collectibleTypeIcon={icon} {...props}>
<Iframe
onError={() => setIsError(true)}
src={src}
styles={{
aspectRatio: '1 / 1',
height: '100%',
objectFit: 'cover',
overflow: 'hidden',
width: '100%',
zIndex: 99,
}}
/>
</CollectibleItemLayout>
);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useState } from 'react';
import { ReactNode, useState } from 'react';

import { CollectibleItemLayout, CollectibleItemLayoutProps } from '../collectible-item.layout';
import { ImageUnavailable } from '../image-unavailable';

interface CollectibleImageProps extends Omit<CollectibleItemLayoutProps, 'children'> {
alt?: string;
icon: React.JSX.Element;
icon: ReactNode;
src: string;
}
export function CollectibleImage(props: CollectibleImageProps) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Flex } from 'leather-styles/jsx';

import { HasChildren } from '@app/common/has-children';

export function CollectiblePlaceholderLayout({ children }: HasChildren) {
return (
<Flex
alignItems="center"
bg="accent.component-background-default"
flexDirection="column"
height="100%"
justifyContent="center"
textAlign="center"
width="100%"
>
{children}
</Flex>
);
}
38 changes: 30 additions & 8 deletions src/app/features/collectibles/components/bitcoin/inscription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { openInNewTab } from '@app/common/utils/open-in-new-tab';
import { convertInscriptionToSupportedInscriptionType } from '@app/query/bitcoin/ordinals/inscription.hooks';
import { OrdinalIcon } from '@app/ui/components/icons/ordinal-icon';

import { CollectibleAudio } from '../_collectible-types/collectible-audio';
import { CollectibleIframe } from '../_collectible-types/collectible-iframe';
import { CollectibleImage } from '../_collectible-types/collectible-image';
import { CollectibleOther } from '../_collectible-types/collectible-other';
import { InscriptionText } from './inscription-text';
Expand All @@ -26,7 +28,31 @@ export function Inscription({ rawInscription }: InscriptionProps) {
}

switch (inscription.type) {
case 'image': {
case 'audio':
return (
<CollectibleAudio
icon={<OrdinalIcon size="lg" />}
key={inscription.title}
onClickCallToAction={() => openInNewTab(inscription.infoUrl)}
onClickSend={() => openSendInscriptionModal()}
subtitle="Ordinal inscription"
title={`# ${inscription.number}`}
/>
);
case 'html':
case 'video':
return (
<CollectibleIframe
icon={<OrdinalIcon size="lg" />}
key={inscription.title}
onClickCallToAction={() => openInNewTab(inscription.infoUrl)}
onClickSend={() => openSendInscriptionModal()}
src={inscription.src}
subtitle="Ordinal inscription"
title={`# ${inscription.number}`}
/>
);
case 'image':
return (
<CollectibleImage
icon={<OrdinalIcon size="lg" />}
Expand All @@ -38,8 +64,7 @@ export function Inscription({ rawInscription }: InscriptionProps) {
title={`# ${inscription.number}`}
/>
);
}
case 'text': {
case 'text':
return (
<InscriptionText
contentSrc={inscription.contentSrc}
Expand All @@ -48,8 +73,7 @@ export function Inscription({ rawInscription }: InscriptionProps) {
onClickSend={() => openSendInscriptionModal()}
/>
);
}
case 'other': {
case 'other':
return (
<CollectibleOther
key={inscription.title}
Expand All @@ -61,9 +85,7 @@ export function Inscription({ rawInscription }: InscriptionProps) {
<OrdinalIcon />
</CollectibleOther>
);
}
default: {
default:
return null;
}
}
}
15 changes: 12 additions & 3 deletions src/app/features/collectibles/components/collectible-hover.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { ReactNode } from 'react';

import { Box, styled } from 'leather-styles/jsx';

import { ArrowUpIcon } from '@app/ui/components/icons/arrow-up-icon';

interface CollectibleHoverProps {
collectibleTypeIcon?: React.JSX.Element;
collectibleTypeIcon?: ReactNode;
isHovered: boolean;
onClickCallToAction?(): void;
}
Expand All @@ -24,9 +26,15 @@ export function CollectibleHover({
style={{ opacity: isHovered ? 'inherit' : '0' }}
top="0px"
width="100%"
zIndex={999}
>
<Box bottom="space.03" height="30px" left="space.03" position="absolute" width="30px">
<Box
bottom="space.03"
height="30px"
left="space.03"
position="absolute"
width="30px"
zIndex={999}
>
{collectibleTypeIcon}
</Box>
{onClickCallToAction && (
Expand All @@ -48,6 +56,7 @@ export function CollectibleHover({
top="12px"
type="button"
width="30px"
zIndex={999}
>
<ArrowUpIcon transform="rotate(45deg)" />
</styled.button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface CollectibleItemLayoutProps {
onClickCallToAction?(): void;
onClickLayout?(): void;
onClickSend?(): void;
collectibleTypeIcon?: React.JSX.Element;
collectibleTypeIcon?: ReactNode;
showBorder?: boolean;
subtitle: string;
title: string;
Expand Down
23 changes: 9 additions & 14 deletions src/app/features/collectibles/components/image-unavailable.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import { Flex, styled } from 'leather-styles/jsx';
import { styled } from 'leather-styles/jsx';

import { EyeSlashIcon } from '@app/ui/components/icons/eye-slash-icon';

import { CollectiblePlaceholderLayout } from './_collectible-types/collectible-placeholder.layout';

export function ImageUnavailable() {
return (
<Flex
alignItems="center"
bg="accent.component-background-default"
flexDirection="column"
height="100%"
justifyContent="center"
textAlign="center"
width="100%"
>
<EyeSlashIcon pb="12px" size="md" />
<styled.span textStyle="label.03">Image currently</styled.span>
<styled.span textStyle="label.03">unavailable</styled.span>
</Flex>
<CollectiblePlaceholderLayout>
<EyeSlashIcon size="md" />
<styled.span pt="space.02" px="space.04" textStyle="label.03">
Image currently unavailable
</styled.span>
</CollectiblePlaceholderLayout>
);
}
29 changes: 27 additions & 2 deletions src/app/query/bitcoin/ordinals/inscription.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,52 @@ export function createInscriptionInfoUrl(id: string) {
return `https://ordinals.hiro.so/inscription/${id}`;
}

function createHtmlPreviewUrl(id: string) {
return `https://ordinals.com/preview/${id}`;
}

export function convertInscriptionToSupportedInscriptionType(inscription: Inscription) {
const title = `Inscription ${inscription.number}`;
return whenInscriptionType<SupportedInscription>(inscription.content_type, {
audio: () => ({
infoUrl: createInscriptionInfoUrl(inscription.id),
src: createHtmlPreviewUrl(inscription.id),
title,
type: 'audio',
...inscription,
}),
html: () => ({
infoUrl: createInscriptionInfoUrl(inscription.id),
src: createHtmlPreviewUrl(inscription.id),
title,
type: 'html',
...inscription,
}),
image: () => ({
infoUrl: createInscriptionInfoUrl(inscription.id),
src: `${HIRO_INSCRIPTIONS_API_URL}/${inscription.id}/content`,
type: 'image',
title,
type: 'image',
...inscription,
}),
text: () => ({
contentSrc: `${HIRO_INSCRIPTIONS_API_URL}/${inscription.id}/content`,
infoUrl: createInscriptionInfoUrl(inscription.id),
title,
type: 'text',
...inscription,
}),
video: () => ({
infoUrl: createInscriptionInfoUrl(inscription.id),
src: createHtmlPreviewUrl(inscription.id),
title,
type: 'video',
...inscription,
}),
other: () => ({
infoUrl: createInscriptionInfoUrl(inscription.id),
type: 'other',
title,
type: 'other',
...inscription,
}),
});
Expand Down
23 changes: 23 additions & 0 deletions src/app/ui/components/icons/audio-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { styled } from 'leather-styles/jsx';

import { SvgProps } from '@app/ui/ui-types';

export function AudioIcon({ size = 'sm', ...props }: SvgProps) {
return (
<styled.svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M9.9978 18.5C9.9978 19.8807 8.65466 21 6.9978 21C5.34095 21 3.9978 19.8807 3.9978 18.5C3.9978 17.1193 5.34095 16 6.9978 16C8.65466 16 9.9978 17.1193 9.9978 18.5ZM9.9978 18.5V6.74404C9.9978 6.30243 10.2875 5.91311 10.7105 5.78621L18.7105 3.38621C19.3521 3.19373 19.9978 3.67418 19.9978 4.34404V15.5M19.9978 15.5C19.9978 16.8807 18.6547 18 16.9978 18C15.3409 18 13.9978 16.8807 13.9978 15.5C13.9978 14.1193 15.3409 13 16.9978 13C18.6547 13 19.9978 14.1193 19.9978 15.5Z"
stroke="currentColor"
strokeWidth="2"
strokeLinejoin="round"
/>
</styled.svg>
);
}
32 changes: 32 additions & 0 deletions src/app/ui/components/iframe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// __ __ _____ _ _ _____ _ _ _____
// \ \ / /\ | __ \| \ | |_ _| \ | |/ ____|
// \ \ /\ / / \ | |__) | \| | | | | \| | | __
// \ \/ \/ / /\ \ | _ /| . ` | | | | . ` | | |_ |
// \ /\ / ____ \| | \ \| |\ |_| |_| |\ | |__| |
// \/ \/_/ \_\_| \_\_| \_|_____|_| \_|\_____|
//
// The purpose of this iframe is to wrap content from external sources,
// primarily for use with inscriptions. Iframes are dangerous and we
// need to be very careful with our use of them.
//
// Below, we use the sandbox attribute to limit what they can do, as well as
// disabling any interaction with pointer events and user selection.
import { CSSProperties } from 'react';

interface IframeProps {
onError(): void;
src: string;
styles: CSSProperties;
}
export function Iframe({ onError, src, styles }: IframeProps) {
return (
<iframe
loading="lazy"
onError={onError}
sandbox="allow-scripts"
src={src}
style={{ pointerEvents: 'none', userSelect: 'none', ...styles }}
/>
);
}
Loading

0 comments on commit 3bddc81

Please sign in to comment.