import * as React from "react"; import * as Constants from "~/common/constants"; import * as Strings from "~/common/strings"; import * as Validations from "~/common/validations"; import * as Actions from "~/common/actions"; import * as System from "~/components/system"; import * as UserBehaviors from "~/common/user-behaviors"; import * as SVG from "~/common/svg"; import * as Events from "~/common/custom-events"; import * as Window from "~/common/window"; import { css, withTheme } from "@emotion/react"; import { RadioGroup } from "~/components/system/components/RadioGroup"; import { LoaderSpinner } from "~/components/system/components/Loaders"; import { SlatePicker } from "~/components/core/SlatePicker"; import { Input } from "~/components/system/components/Input"; import { Toggle } from "~/components/system/components/Toggle"; import { Textarea } from "~/components/system/components/Textarea"; import { Tag } from "~/components/system/components/Tag"; import { Link } from "~/components/core/Link"; import LinkTag from "~/components/core/Link/LinkTag"; import isEqual from "lodash/isEqual"; import cloneDeep from "lodash/cloneDeep"; import ProcessedText from "~/components/core/ProcessedText"; import { ConfirmationModal } from "~/components/core/ConfirmationModal"; const DEFAULT_BOOK = "https://slate.textile.io/ipfs/bafkreibk32sw7arspy5kw3p5gkuidfcwjbwqyjdktd5wkqqxahvkm2qlyi"; const DEFAULT_DATA = "https://slate.textile.io/ipfs/bafkreid6bnjxz6fq2deuhehtxkcesjnjsa2itcdgyn754fddc7u72oks2m"; const DEFAULT_DOCUMENT = "https://slate.textile.io/ipfs/bafkreiecdiepww52i5q3luvp4ki2n34o6z3qkjmbk7pfhx4q654a4wxeam"; const DEFAULT_VIDEO = "https://slate.textile.io/ipfs/bafkreibesdtut4j5arclrxd2hmkfrv4js4cile7ajnndn3dcn5va6wzoaa"; const DEFAULT_AUDIO = "https://slate.textile.io/ipfs/bafkreig2hijckpamesp4nawrhd6vlfvrtzt7yau5wad4mzpm3kie5omv4e"; const STYLES_NO_VISIBLE_SCROLL = css` overflow-y: scroll; scrollbar-width: none; -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; ::-webkit-scrollbar { width: 0px; display: none; } ::-webkit-scrollbar-track { background: ${Constants.semantic.bgLight}; } ::-webkit-scrollbar-thumb { background: ${Constants.system.grayLight2}; } `; const STYLES_BODY = css` font-size: 16px; line-height: 1.225; overflow-wrap: break-word; white-space: pre-wrap; margin-bottom: 32px; `; const STYLES_SIDEBAR_INPUT_LABEL = css` font-size: 16px; font-family: ${Constants.font.semiBold}; color: ${Constants.system.grayLight2}; margin-bottom: 8px; `; const STYLES_SIDEBAR = css` width: 420px; padding: 48px 24px 0px 24px; flex-shrink: 0; height: 100vh; display: flex; flex-direction: column; align-items: flex-start; justify-content: space-between; background-color: rgba(20, 20, 20, 0.8); ${STYLES_NO_VISIBLE_SCROLL} @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { -webkit-backdrop-filter: blur(75px); backdrop-filter: blur(75px); background-color: rgba(150, 150, 150, 0.2); } @media (max-width: ${Constants.sizes.mobile}px) { display: none; } `; const STYLES_DISMISS_BOX = css` position: absolute; top: 16px; right: 16px; color: ${Constants.system.grayLight2}; cursor: pointer; :hover { color: ${Constants.system.white}; } `; const STYLES_HEADING = css` font-family: ${Constants.font.semiBold}; font-size: 20px; font-weight: 400; overflow-wrap: break-word; white-space: pre-wrap; margin-bottom: 32px; `; const STYLES_META = css` text-align: start; padding: 14px 0px 8px 0px; overflow-wrap: break-word; `; const STYLES_META_TITLE = css` font-family: ${Constants.font.semiBold}; color: ${Constants.system.white}; font-size: ${Constants.typescale.lvl2}; text-decoration: none; word-break: break-all; overflow-wrap: anywhere; :hover { color: ${Constants.system.blue}; } `; const STYLES_TAG = css` margin-right: 24px; padding: 0px 2px; border-radius: 2px; border: 1px solid ${Constants.system.grayLight2}; `; const STYLES_OPTIONS_SECTION = css` display: flex; flex-direction: row; align-items: center; justify-content: space-between; margin: 16px 0 16px 0; `; const STYLES_META_DETAILS = css` color: ${Constants.system.grayLight2}; text-transform: uppercase; margin: 24px 0px; font-family: ${Constants.font.medium}; font-size: 0.9rem; `; const STYLES_SIDEBAR_SECTION = css` flex-shrink: 0; width: 100%; margin-bottom: 16px; `; const STYLES_ACTIONS = css` color: ${Constants.system.white}; border: 1px solid #3c3c3c; border-radius: 4px; background-color: transparent; margin-bottom: 48px; margin-top: 36px; `; const STYLES_ACTION = css` cursor: pointer; padding: 12px 16px; border-bottom: 1px solid #3c3c3c; display: flex; align-items: center; :hover { color: ${Constants.system.blue}; } :last-child { border: none; } `; const STYLES_SECTION_HEADER = css` font-family: ${Constants.font.semiBold}; font-size: 1.1rem; margin-top: 24px; display: flex; align-items: center; `; const STYLES_HIDDEN = css` position: absolute; opacity: 0; pointer-events: none; `; const STYLES_IMAGE_BOX = css` max-width: 100%; max-height: 368px; display: flex; align-items: center; justify-content: center; background-color: ${Constants.system.black}; overflow: hidden; ${"" /* box-shadow: 0 0 0 1px ${Constants.semantic.borderGrayLight} inset; */} border-radius: 4px; `; const STYLES_FILE_HIDDEN = css` height: 1px; width: 1px; opacity: 0; visibility: hidden; position: fixed; top: -1px; left: -1px; `; const STYLES_TEXT = css` color: ${Constants.system.grayLight2}; line-height: 1.5; `; const STYLES_INPUT = { marginBottom: 8, backgroundColor: "transparent", boxShadow: "0 0 0 1px #3c3c3c inset", color: Constants.system.white, height: 48, }; const STYLES_AUTOSAVE = css` font-size: 12px; line-height: 1.225; display: flex; justify-content: baseline; color: ${Constants.system.yellow}; opacity: 0; ${"" /* margin: 26px 24px; */} position: absolute; top: 24px; left: 16px; @keyframes slate-animations-autosave { 0% { opacity: 0; transform: translateX(0); } 10% { opacity: 1; transform: translateX(12px); } 90% { opacity: 1; transform: translateX(12px); } 100% { opacity: 0; } } animation: slate-animations-autosave 4000ms ease; `; const STYLES_SPINNER = css` width: 24px; height: 24px; `; export const FileTypeDefaultPreview = (props) => { if (props.type) { if (Validations.isVideoType(type)) { return DEFAULT_VIDEO; } else if (Validations.isAudioType(type)) { return DEFAULT_AUDIO; } else if (Validations.isPdfType(type)) { return DEFAULT_DOCUMENT; } else if (Validations.isEpubType(type)) { return DEFAULT_BOOK; } } return DEFAULT_DATA; }; class CarouselSidebar extends React.Component { state = { name: this.props.file.data.name || this.props.file.filename || "", body: this.props.file.data.body || "", source: this.props.file.data.source || "", author: this.props.file.data.author || "", tags: this.props.file.data.tags || [], suggestions: this.props.viewer?.tags || [], selected: {}, isUploading: false, isDownloading: false, showSavedMessage: false, showConnectedSection: false, showFileSection: true, modalShow: false, }; componentDidMount = () => { const editingAllowed = !this.props.external && this.props.isOwner && !this.props.isRepost; if (editingAllowed) { this.debounceInstance = Window.debounce(() => this._handleSave(), 3000); this.calculateSelected(); } }; componentDidUpdate = (prevProps, prevState) => { if (!isEqual(prevState.tags, this.state.tags)) { this.updateSuggestions(); } }; updateSuggestions = () => { let newSuggestions = new Set([...this.state.suggestions, ...this.state.tags]); this.setState({ suggestions: Array.from(newSuggestions) }); }; calculateSelected = () => { if (!this.props.viewer) { this.setState({ selected: {} }); return; } let selected = {}; const id = this.props.file.id; for (let slate of this.props.viewer.slates) { if (slate.objects.some((obj) => obj.id === id)) { selected[slate.id] = true; } } this.setState({ selected }); }; _handleToggleAccordion = (tab) => { this.setState({ [tab]: !this.state[tab] }); }; _handleDarkMode = async (e) => { Events.dispatchCustomEvent({ name: "set-slate-theme", detail: { darkmode: e.target.value }, }); }; _handleChange = (e) => { if (this.props.external || !this.props.isOwner) return; this.debounceInstance(); this.setState( { [e.target.name]: e.target.value, showSavedMessage: false, }, () => { if (e.target.name === "Tags") { this.updateSuggestions(); } } ); }; _handleCapitalization(str) { return str.charAt(0).toUpperCase() + str.slice(1); } _handleSave = async () => { if (this.props.external || !this.props.isOwner) return; this.props.onAction({ type: "UPDATE_VIEWER", viewer: { tags: this.state.suggestions } }); const response = await Actions.updateFile({ id: this.props.file.id, data: { name: this.state.name, body: this.state.body, source: this.state.source, author: this.state.author, tags: this.state.tags, }, }); Events.hasError(response); this.setState({ showSavedMessage: true }); }; _handleSaveCopy = async (data) => { if (!this.props.viewer) { Events.dispatchCustomEvent({ name: "slate-global-open-cta", detail: {} }); return; } this.setState({ loading: "savingCopy" }, async () => { let response = await UserBehaviors.saveCopy({ files: [data] }); Events.hasError(response); this.setState({ loading: false }); }); }; _handleUpload = async (e) => { if (this.props.external || !this.props.isOwner || !this.props.viewer) return; e.persist(); this.setState({ isUploading: true }); let previousCoverId = this.props.file.data.coverImage?.id; if (!e || !e.target) { this.setState({ isUploading: false }); return; } let file = await UserBehaviors.uploadImage(e.target.files[0]); if (!file) { this.setState({ isUploading: false }); return; } let coverImage = file; //TODO(martina): create an endpoint specifically for cover images instead of this, which will delete original cover image etc let updateReponse = await Actions.updateFile({ id: this.props.file.id, data: { coverImage, }, }); if (previousCoverId) { if (!this.props.viewer.library.some((obj) => obj.id === previousCoverId)) { await UserBehaviors.deleteFiles(previousCoverId, true); } } Events.hasError(updateReponse); this.setState({ isUploading: false }); }; _handleDownload = () => { if (!this.props.viewer) { Events.dispatchCustomEvent({ name: "slate-global-open-cta", detail: {} }); return; } this.setState({ isDownloading: true }, async () => { const response = await UserBehaviors.download(this.props.file); this.setState({ isDownloading: false }); Events.hasError(response); }); }; _handleCreateSlate = async () => { if (this.props.external) return; this.props.onClose(); this.props.onAction({ type: "SIDEBAR", value: "SIDEBAR_CREATE_SLATE", data: { files: [this.props.file] }, }); }; _handleDelete = (res) => { if (this.props.external || !this.props.isOwner || !this.props.viewer) return; if (!res) { this.setState({ modalShow: false }); return; } const id = this.props.file.id; let updatedLibrary = this.props.viewer.library.filter((obj) => obj.id !== id); if (this.props.carouselType === "SLATE") { const slateId = this.props.data.id; let slates = this.props.viewer.slates; for (let slate of slates) { if (slate.id === slateId) { slate.objects = slate.objects.filter((obj) => obj.id !== id); break; } } this.props.onAction({ type: "UPDATE_VIEWER", viewer: { library: updatedLibrary, slates } }); } else { this.props.onAction({ type: "UPDATE_VIEWER", viewer: { library: updatedLibrary } }); } UserBehaviors.deleteFiles(id); this.props.onNext(); }; _handleAdd = async (slate) => { if (this.state.selected[slate.id]) { UserBehaviors.removeFromSlate({ slate, ids: [this.props.file.id] }); } else { UserBehaviors.saveCopy({ slate, files: [this.props.file], }); } this.setState({ selected: { ...this.state.selected, [slate.id]: !this.state.selected[slate.id], }, }); }; _handleRemove = async () => { if ( !this.props.carouselType === "SLATE" || this.props.external || !this.props.isOwner || !this.props.viewer ) { return; } const id = this.props.file.id; const slateId = this.props.data.id; let slates = this.props.viewer.slates; for (let slate of slates) { if (slate.id === slateId) { slate.objects = slate.objects.filter((obj) => obj.id !== id); break; } } this.props.onAction({ type: "UPDATE_VIEWER", viewer: { slates } }); UserBehaviors.removeFromSlate({ slate: this.props.data, ids: [this.props.file.id] }); this.props.onNext(); }; render() { const isPublic = this.props.file.isPublic; const file = this.props.file; const { coverImage, type, size } = file.data; const editingAllowed = this.props.isOwner && !this.props.isRepost && !this.props.external; const isUnityGame = Validations.isUnityType(type); const isLink = file.isLink; const showPreviewImageSection = editingAllowed && type && !isLink && !Validations.isPreviewableImage(type); const elements = []; if (editingAllowed && !isUnityGame) { elements.push(