import * as React from "react"; import * as NavigationData from "~/common/navigation-data"; import * as Actions from "~/common/actions"; import * as Strings from "~/common/strings"; import * as Styles from "~/common/styles"; import * as Credentials from "~/common/credentials"; import * as Constants from "~/common/constants"; import * as Validations from "~/common/validations"; import * as FileUtilities from "~/common/file-utilities"; import * as Window from "~/common/window"; import * as Store from "~/common/store"; import * as Websockets from "~/common/browser-websockets"; import * as UserBehaviors from "~/common/user-behaviors"; import * as Events from "~/common/custom-events"; import * as Logging from "~/common/logging"; import * as Environment from "~/common/environment"; // NOTE(jim): // Scenes each have an ID and can be navigated to with _handleAction import SceneError from "~/scenes/SceneError"; import SceneEditAccount from "~/scenes/SceneEditAccount"; import SceneFile from "~/scenes/SceneFile"; import SceneFilesFolder from "~/scenes/SceneFilesFolder"; import SceneSettings from "~/scenes/SceneSettings"; import SceneSlates from "~/scenes/SceneSlates"; import SceneSettingsDeveloper from "~/scenes/SceneSettingsDeveloper"; import SceneAuth from "~/scenes/SceneAuth"; import SceneSlate from "~/scenes/SceneSlate"; import SceneActivity from "~/scenes/SceneActivity"; import SceneDirectory from "~/scenes/SceneDirectory"; import SceneProfile from "~/scenes/SceneProfile"; import SceneArchive from "~/scenes/SceneArchive"; import SceneMakeFilecoinDeal from "~/scenes/SceneMakeFilecoinDeal"; // NOTE(jim): // Sidebars each have a decorator and can be shown to with _handleAction import SidebarCreateSlate from "~/components/sidebars/SidebarCreateSlate"; import SidebarCreateWalletAddress from "~/components/sidebars/SidebarCreateWalletAddress"; import SidebarWalletSendFunds from "~/components/sidebars/SidebarWalletSendFunds"; import SidebarFileStorageDeal from "~/components/sidebars/SidebarFileStorageDeal"; import ModalAddFileToBucket from "~/components/sidebars/ModalAddFileToBucket"; import SidebarAddFileToSlate from "~/components/sidebars/SidebarAddFileToSlate"; import SidebarDragDropNotice from "~/components/sidebars/SidebarDragDropNotice"; import SidebarSingleSlateSettings from "~/components/sidebars/SidebarSingleSlateSettings"; import SidebarFilecoinArchive from "~/components/sidebars/SidebarFilecoinArchive"; import SidebarHelp from "~/components/sidebars/SidebarHelp"; import SidebarFAQ from "~/components/sidebars/SidebarFAQ"; import SidebarEditTags from "~/components/sidebars/SidebarEditTags"; // NOTE(jim): // Core components to the application structure. import ApplicationHeader from "~/components/core/ApplicationHeader"; import ApplicationLayout from "~/components/core/ApplicationLayout"; import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper"; import CTATransition from "~/components/core/CTATransition"; import { GlobalModal } from "~/components/system/components/GlobalModal"; import { OnboardingModal } from "~/components/core/OnboardingModal"; import { SearchModal } from "~/components/core/SearchModal"; import { Alert } from "~/components/core/Alert"; import { announcements } from "~/components/core/OnboardingModal"; import { Logo } from "~/common/logo"; import { LoaderSpinner } from "~/components/system/components/Loaders"; const SIDEBARS = { SIDEBAR_FILECOIN_ARCHIVE: , SIDEBAR_FILE_STORAGE_DEAL: , SIDEBAR_WALLET_SEND_FUNDS: , SIDEBAR_CREATE_WALLET_ADDRESS: , SIDEBAR_ADD_FILE_TO_BUCKET: , SIDEBAR_ADD_FILE_TO_SLATE: , SIDEBAR_CREATE_SLATE: , SIDEBAR_DRAG_DROP_NOTICE: , SIDEBAR_SINGLE_SLATE_SETTINGS: , SIDEBAR_HELP: , SIDEBAR_FAQ: , SIDEBAR_EDIT_TAGS: , }; const SCENES = { NAV_ERROR: , NAV_SIGN_IN: , ...(Environment.ACTIVITY_FEATURE_FLAG ? { NAV_ACTIVITY: } : {}), NAV_DIRECTORY: , NAV_PROFILE: , NAV_DATA: , // NAV_FILE: , NAV_SLATE: , NAV_API: , NAV_SETTINGS: , NAV_SLATES: , NAV_FILECOIN: , NAV_STORAGE_DEAL: , }; let mounted; export default class ApplicationPage extends React.Component { _body; state = { selected: {}, viewer: this.props.viewer, page: this.props.page || {}, data: this.props.data, activePage: this.props.page?.id, sidebar: null, online: null, isMobile: this.props.isMobile, activeUsers: null, loading: false, }; async componentDidMount() { // window.iframely && iframely.load(); this._handleWindowResize(); if (mounted) { return false; } mounted = true; window.addEventListener("dragenter", this._handleDragEnter); window.addEventListener("dragleave", this._handleDragLeave); window.addEventListener("dragover", this._handleDragOver); window.addEventListener("drop", this._handleDrop); window.addEventListener("online", this._handleOnlineStatus); window.addEventListener("offline", this._handleOnlineStatus); window.addEventListener("resize", this._handleWindowResize); window.addEventListener("paste", this._handleUploadFromClipboard); window.onpopstate = this._handleBackForward; if (this.state.viewer) { await this._handleSetupWebsocket(); } } componentWillUnmount() { window.removeEventListener("dragenter", this._handleDragEnter); window.removeEventListener("dragleave", this._handleDragLeave); window.removeEventListener("dragover", this._handleDragOver); window.removeEventListener("drop", this._handleDrop); window.removeEventListener("online", this._handleOnlineStatus); window.removeEventListener("offline", this._handleOnlineStatus); window.removeEventListener("resize", this._handleWindowResize); window.removeEventListener("paste", this._handleUploadFromClipboard); mounted = false; let wsclient = Websockets.getClient(); if (wsclient) { Websockets.deleteClient(); } } _handleUploadFromClipboard = (e) => { const clipboardItems = e.clipboardData.items || []; if (!clipboardItems) return; const { fileLoading, toUpload } = FileUtilities.formatPastedImages({ clipboardItems, }); this._handleRegisterFileLoading({ fileLoading }); const page = this.state.page; let slate = null; if (page?.id === "NAV_SLATE" && this.state.data?.ownerId === this.state.viewer?.id) { slate = this.state.data; } FileUtilities.uploadFiles({ files: toUpload, slate, keys: Object.keys(fileLoading), resources: this.props.resources, context: this, }); }; _handleUpdateViewer = ({ viewer, callback }) => { // _handleUpdateViewer = (newViewerState, callback) => { // let setAsyncState = (newState) => // new Promise((resolve) => // this.setState( // { // viewer: { ...this.state.viewer, ...newState, type: "VIEWER" }, // }, // resolve // ) // ); // await setAsyncState(newViewerState); //NOTE(martina): if updating viewer affects this.state.data (e.g. you're viewing your own slate), update data as well if (viewer?.slates?.length) { const page = this.state.page; if (page?.id === "NAV_SLATE" && this.state.data?.ownerId === this.state.viewer.id) { let data = this.state.data; for (let slate of viewer.slates) { if (slate.id === data.id) { data = slate; break; } } this.setState( { viewer: { ...this.state.viewer, ...viewer }, data, }, () => { if (callback) { callback(); } } ); return; } } this.setState( { viewer: { ...this.state.viewer, ...viewer }, }, () => { if (callback) { callback(); } } ); }; _handleUpdateData = ({ data, callback }) => { // _handleUpdateData = (data, callback) => { //TODO(martina): maybe add a default window.history.replacestate where it pushes the new data to browser? this.setState({ data }, () => { if (callback) { callback(); } }); }; _handleSetupWebsocket = async () => { let wsclient = Websockets.getClient(); if (wsclient) { await Websockets.deleteClient(); wsclient = null; } if (this.props.resources && !Strings.isEmpty(this.props.resources.pubsub)) { if (!this.state.viewer) { Logging.error("WEBSOCKET: NOT AUTHENTICATED"); return; } wsclient = Websockets.init({ resource: this.props.resources.pubsub, viewer: this.state.viewer, onUpdate: this._handleUpdateViewer, onNewActiveUser: this._handleNewActiveUser, }); } if (!wsclient) { Events.dispatchMessage({ message: "We cannot connect to our live update server. You may have to refresh to see updates.", }); } return; }; _handleNewActiveUser = (users) => { this.setState({ activeUsers: users }); }; _handleWindowResize = () => { const { width } = Window.getViewportSize(); // (1) is Window.isMobileBrowser checks, that one holds. // (2) then if the viewport is smaller than the width let isMobile = width > Constants.sizes.mobile ? this.props.isMobile : true; // only change if necessary. if (this.state.isMobile !== isMobile) { Logging.log("changing to mobile?", isMobile); this.setState({ isMobile }); } }; _handleOnlineStatus = async () => { if (navigator.onLine) { Events.dispatchMessage({ message: "Back online!", status: "INFO" }); } else { Events.dispatchMessage({ message: "Offline. Trying to reconnect" }); } this.setState({ online: navigator.onLine }); }; _handleDrop = async (e) => { e.preventDefault(); this.setState({ sidebar: null }); const { fileLoading, files, numFailed, error } = await FileUtilities.formatDroppedFiles({ dataTransfer: e.dataTransfer, }); if (error) { return null; } let page = this.state.page; let slate = null; if (page?.id === "NAV_SLATE" && this.state.data?.ownerId === this.state.viewer?.id) { slate = this.state.data; } this._handleRegisterFileLoading({ fileLoading }); FileUtilities.uploadFiles({ files, slate, keys: Object.keys(fileLoading), numFailed, resources: this.props.resources, context: this, }); }; _handleUploadFiles = async ({ files, slate }) => { const { fileLoading, toUpload, numFailed } = FileUtilities.formatUploadedFiles({ files }); this._handleRegisterFileLoading({ fileLoading }); FileUtilities.uploadFiles({ files: toUpload, slate, keys: Object.keys(fileLoading), numFailed, resources: this.props.resources, context: this, }); }; _handleRegisterFileLoading = ({ fileLoading }) => { if (this.state.fileLoading) { return this.setState({ fileLoading: { ...this.state.fileLoading, ...fileLoading }, }); } return this.setState({ fileLoading, }); }; _handleRegisterFileCancelled = ({ key }) => { let fileLoading = this.state.fileLoading; fileLoading[key].cancelled = true; this.setState({ fileLoading }); }; _handleRegisterLoadingFinished = ({ keys }) => { let fileLoading = this.state.fileLoading; for (let key of keys) { delete fileLoading[key]; } this.setState({ fileLoading }); }; _handleDragEnter = (e) => { e.preventDefault(); if (this.state.sidebar) { return; } // NOTE(jim): Only allow the sidebar to show with file drag and drop. if (e.dataTransfer?.items?.length && e.dataTransfer.items[0].kind !== "file") { return; } this._handleAction({ type: "SIDEBAR", value: "SIDEBAR_ADD_FILE_TO_BUCKET", }); }; _handleDragLeave = (e) => { e.preventDefault(); }; _handleDragOver = (e) => { e.preventDefault(); }; _withAuthenticationBehavior = (authenticate) => async (state, newAccount) => { let response = await authenticate(state); if (Events.hasError(response)) { return response; } if (response.shouldMigrate) { return response; } let viewer = await UserBehaviors.hydrate(); if (Events.hasError(viewer)) { return viewer; } this.setState({ viewer }); await this._handleSetupWebsocket(); let unseenAnnouncements = []; for (let feature of announcements) { if (!viewer.data.onboarding || !Object.keys(viewer.data.onboarding).includes(feature)) { unseenAnnouncements.push(feature); } } if (newAccount || unseenAnnouncements.length) { Events.dispatchCustomEvent({ name: "create-modal", detail: { modal: ( ), noBoundary: true, }, }); } if (newAccount) { Actions.updateSearch("create-user"); } // let redirected = this._handleURLRedirect(); // if (!redirected) { // this._handleAction({ type: "NAVIGATE", value: "NAV_DATA" }); // } this._handleNavigateTo({ href: "/_/data", redirect: true }); return response; }; _handleSelectedChange = (e) => { this.setState({ selected: { ...this.state.selected, [e.target.name]: e.target.value }, }); }; _handleDismissSidebar = () => { this.setState({ sidebar: null, sidebarData: null }); }; _handleAction = (options) => { if (options.type === "NAVIGATE") { return this._handleNavigateTo(options); } if (options.type === "UPDATE_VIEWER") { return this._handleUpdateViewer(options); } if (options.type === "UPDATE_PARAMS") { return this._handleUpdatePageParams(options); } if (options.type === "SIDEBAR") { return this.setState({ sidebar: options.value, sidebarData: options.data, }); } if (options.type === "REGISTER_FILE_CANCELLED") { return this._handleRegisterFileCancelled({ key: options.value }); } if (options.type === "NEW_WINDOW") { return window.open(options.value); } Logging.error("Error: Failed to _handleAction because TYPE did not match any known actions"); }; _handleNavigateTo = async ({ href, redirect = false, popstate = false }) => { const { page, details } = NavigationData.getByHref(href, this.state.viewer); Events.dispatchCustomEvent({ name: "slate-global-close-carousel", detail: {} }); if (redirect || popstate) { window.history.replaceState(null, "Slate", page.pathname); } else { window.history.pushState(null, "Slate", page.pathname); } let state = { data: null, sidebar: null, page }; if (!next.ignore) { state.activePage = page.id; } let body = document.documentElement || document.body; if (page.id === "NAV_SLATE" || page.id === "NAV_PROFILE") { state.loading = true; } this.setState(state, () => { if (!popstate) { body.scrollTo(0, 0); } if (page.id === "NAV_SLATE" || page.id === "NAV_PROFILE") { this.updateDataAndPathname({ page, details }); } }); }; updateDataAndPathname = async ({ page, details }) => { let pathname = page.pathname.split("?")[0]; let search = Strings.getQueryStringFromParams(page.params); let data; if (page?.id === "NAV_SLATE") { let response = await Actions.getSerializedSlate(details); if (!response || response.error) { this.setState({ loading: false }); this._handleNavigateTo({ href: "/_/404", redirect: true }); return; } data = response.data; pathname = `/${data.user.username}/${data.slatename}${search}`; } else if (page?.id === "NAV_PROFILE") { let response = await Actions.getSerializedProfile(details); if (!response || response.error) { this.setState({ loading: false }); this._handleNavigateTo({ href: "/_/404", redirect: true }); return; } data = response.data; pathname = `/${data.username}${search}`; } this.setState({ data, loading: false }); window.history.replaceState(null, "Slate", pathname); }; _handleUpdatePageParams = ({ params, callback, redirect = false }) => { let query = Strings.getQueryStringFromParams(params); const href = window.location.pathname.concat(query); if (redirect) { window.history.replaceState(null, "Slate", href); } else { window.history.pushState(null, "Slate", href); } this.setState({ page: { ...this.state.page, params } }, () => { if (callback) { callback(); } }); }; _handleBackForward = () => { let href = window.location.pathname.concat( window.location.search ? `${window.location.search}` : "" ); this._handleNavigateTo({ href, popstate: true }); }; render() { let page = this.state.page; if (!page?.id) { page = NavigationData.getById(null, this.state.viewer); } let headerElement; if (page.id !== "NAV_SIGN_IN") { headerElement = ( ); } const scene = React.cloneElement(SCENES[page.id], { key: this.state.data?.id, page: page, data: this.state.data, viewer: this.state.viewer, selected: this.state.selected, onSelectedChange: this._handleSelectedChange, onAuthenticate: this._withAuthenticationBehavior(UserBehaviors.authenticate), onTwitterAuthenticate: this._withAuthenticationBehavior(UserBehaviors.authenticateViaTwitter), onAction: this._handleAction, onUpload: this._handleUploadFiles, isMobile: this.state.isMobile, isMac: this.props.isMac, resources: this.props.resources, activeUsers: this.state.activeUsers, userBucketCID: this.state.userBucketCID, external: !!!this.state.viewer, }); let sidebarElement; if (this.state.sidebar) { sidebarElement = React.cloneElement(SIDEBARS[this.state.sidebar], { page: page, selected: this.state.selected, viewer: this.state.viewer, data: this.state.data, sidebarData: this.state.sidebarData, fileLoading: this.state.fileLoading, onSelectedChange: this._handleSelectedChange, onCancel: this._handleDismissSidebar, onUpload: this._handleUploadFiles, onAction: this._handleAction, resources: this.props.resources, }); } const title = `Slate: ${page.pageTitle}`; const description = ""; const url = "https://slate.host/_"; // if (!this.state.loaded) { // return ( // //
// //
//
// ); // } return ( {this.state.loading ? (
) : ( scene )}
{/* {!this.state.loaded ? (
) : null} */}
); } }