import { BORDER_RADIUS, COLORS, FlipFlop, IconLoading, ProgressBar } from "@heydovetail/ui-components"; import React from "react"; import { styled } from "typestyle-react"; import { AttachmentPreviewImage, AttachmentService, AttachmentUpload } from "../../AttachmentService"; import { Attachment } from "../../util/attrs"; import { Box, box } from "../../util/box"; import { AttachmentActions } from "../Shared/AttachmentActions"; import { AttachmentPreviewSubscribe } from "../Shared/AttachmentPreviewSubscribe"; import { AttachmentUploadSubscribe } from "../Shared/AttachmentUploadSubscribe"; import { ResponsiveImage } from "../Shared/ResponsiveImage"; import { SelectionMask } from "../Shared/SelectionMask"; const minSizeForIcon = 32; const minSizeForMeatballAlignRight = 160; const minSizeForActionsOutside = 80; export interface Props { attachment: Attachment; attachmentService: AttachmentService; editable: boolean; onHidePreview: () => void; onRemove: () => void; style?: "active" | "selected"; } // When an image is uploaded and doesn't include size information, it defaults // to this. const unavailableUnknownSizeFallback = box(300, 300); interface ContainerProps { attachment: Attachment; attachmentService: AttachmentService; editable: boolean; onHidePreview: () => void; onRemove: () => void; size?: Box; style?: "active" | "selected"; progressStyle?: "light" | "dark"; } class ImageContainer extends React.PureComponent { public render() { const { props } = this; const { progressStyle = "light", size, style } = props; return ( {attachmentUpload => ( {({ set: setMenuOpenTo, active: isMenuOpen }) => ( {({ setOn: setHovered, setOff: setNotHovered, active: isHovered }) => ( {this.props.children} {style === "selected" ? : null} {isMenuOpen || isHovered || style === "active" ? ( ) : null} )} )} )} ); } } export class AttachmentImage extends React.PureComponent { public render() { const { props } = this; const { attachment: { naturalSize } } = props; return ( {(preview): React.ReactNode => { switch (preview.type) { case "image": // The awkwardness here of rendering a when // the size is known is to avoid layout jank while the image is // loading. // // https://stackoverflow.com/questions/45869454/responsive-img-without-reflow // describes the problem, but the solution offered doesn't work // for images that should be smaller than 100%. // // When `naturalSize` is known, the strategy used here is to use // to take up space, rather than the // . The picture is absolutely positioned and renders // over the space preserved by . return ( {naturalSize !== undefined ? : null} {renderSources(preview.sources)} ); case "pending": case "unavailable": return ( {preview.type === "unavailable" ? ( "Preview unavailable" ) : naturalSize !== undefined && naturalSize.width >= minSizeForIcon && naturalSize.height >= minSizeForIcon ? ( ) : null} ); case "audio": return null; } }} ); } } function renderSources(sources: AttachmentPreviewImage["sources"]): React.ReactNode { if (sources === undefined) { return null; } const grouped = new Map>(); for (const source of sources) { const lookup = grouped.get(source.type); const items = lookup !== undefined ? lookup : []; if (lookup === undefined) { grouped.set(source.type, items); } items.push(source); } return ( Array.from(grouped.entries()) // Set image/webp to be first. It's a most sophisticated format and // supports animation, which is good if the user uploaded a .GIF as it // provides an animated preview. .sort(([aType], [bType]) => typeRank(aType) - typeRank(bType)) .map(([type, sources]) => ( `${s.url} ${s.width}w`).join(", ")} sizes="(min-width: 921px) 920px, 100vw" /> )) ); } /** * A function to rank different image types (lower is better). * @param type */ function typeRank(type: string): number { switch (type) { case "image/webp": return 0; default: return 1; } } const HoverContainer = styled("div", { lineHeight: 0, width: "100%" }); const RelativeContainer = styled("div", { display: "inline-block", position: "relative" }); const InnerContainer = styled("div", ({ style }: { style?: "active" | "selected" }) => ({ boxShadow: style === "active" ? `0 0 0 2px ${COLORS.blue}` : undefined, borderRadius: BORDER_RADIUS, display: "inline-block", overflow: "hidden", position: "relative" })); const ActionsButton = styled("div", ({ outside = false }: { outside?: boolean }) => ({ backgroundColor: outside ? undefined : "rgba(36, 18, 77, 0.2)", borderRadius: BORDER_RADIUS, color: outside ? COLORS.i60 : COLORS.white, position: "absolute", right: outside ? -48 : 8, top: outside ? 0 : 8 })); const OpaqueMask = styled("div", { alignItems: "center", backgroundColor: COLORS.i04, borderRadius: BORDER_RADIUS, bottom: 0, color: COLORS.i60, fontSize: "14px", fontWeight: 600, display: "flex", justifyContent: "center", left: 0, position: "absolute", right: 0, top: 0 }); const SemiOpaqueMask = styled("div", { backgroundColor: "rgba(255, 255, 255, 0.6)", bottom: 0, left: 0, position: "absolute", right: 0, top: 0 }); const UploadStateMask: React.SFC<{ progressStyle: "light" | "dark"; attachmentUpload: AttachmentUpload }> = ({ progressStyle, attachmentUpload }) => { switch (attachmentUpload.type) { case "uploading": return ( ); case "queued": return ( ); case "failed": return Upload failed; case "unknown": case "complete": return null; } }; export function UploadProgressBarMask(props: { completed: number; style: "light" | "dark" }) { return ( ); } const ProgressBarContainer = styled("div", { bottom: "8px", left: "8px", position: "absolute", right: "8px", top: `calc(100% - 4px - 8px)` });