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 15, 2023
1 parent 8dc1d22 commit e19eea4
Show file tree
Hide file tree
Showing 13 changed files with 254 additions and 35 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 https://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 https://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,35 @@
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
aspectRatio="1 / 1"
height="100%"
objectFit="cover"
onError={() => setIsError(true)}
src={src}
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>
);
}
34 changes: 34 additions & 0 deletions src/app/ui/components/iframe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// __ __ _____ _ _ _____ _ _ _____
// \ \ / /\ | __ \| \ | |_ _| \ | |/ ____|
// \ \ /\ / / \ | |__) | \| | | | | \| | | __
// \ \/ \/ / /\ \ | _ /| . ` | | | | . ` | | |_ |
// \ /\ / ____ \| | \ \| |\ |_| |_| |\ | |__| |
// \/ \/_/ \_\_| \_\_| \_|_____|_| \_|\_____|
//
// 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 { HTMLStyledProps, styled } from 'leather-styles/jsx';

interface IframeProps extends HTMLStyledProps<'iframe'> {
onError(): void;
src: string;
}
export function Iframe({ onError, src, ...props }: IframeProps) {
return (
<styled.iframe
loading="lazy"
onError={onError}
overflow="hidden"
pointerEvents="none"
sandbox="allow-scripts"
src={src}
userSelect="none"
{...props}
/>
);
}
Loading

0 comments on commit e19eea4

Please sign in to comment.