Path: blob/master/src/packages/next/components/path/path.tsx
1448 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import {6Alert,7Avatar as AntdAvatar,8Button,9Divider,10Flex,11Space,12Tooltip,13QRCode,14} from "antd";15import Link from "next/link";16import { useRouter } from "next/router";17import { join } from "path";18import { useEffect, useState } from "react";19import basePath from "lib/base-path";20import { Icon } from "@cocalc/frontend/components/icon";21import {22SHARE_AUTHENTICATED_EXPLANATION,23SHARE_AUTHENTICATED_ICON,24} from "@cocalc/util/consts/ui";25import InPlaceSignInOrUp from "components/auth/in-place-sign-in-or-up";26import A from "components/misc/A";27import Badge from "components/misc/badge";28import SanitizedMarkdown from "components/misc/sanitized-markdown";29import { Layout } from "components/share/layout";30import License from "components/share/license";31import LinkedPath from "components/share/linked-path";32import Loading from "components/share/loading";33import PathActions from "components/share/path-actions";34import PathContents from "components/share/path-contents";35import ProjectLink from "components/share/project-link";36import Avatar from "components/share/proxy/avatar";37import apiPost from "lib/api/post";38import type { CustomizeType } from "lib/customize";39import useCounter from "lib/share/counter";40import { Customize } from "lib/share/customize";41import type { PathContents as PathContentsType } from "lib/share/get-contents";42import { getTitle } from "lib/share/util";4344import { SocialMediaShareLinks } from "components/landing/social-media-share-links";4546export interface PublicPathProps {47id: string;48path: string;49url: string;50project_id: string;51projectTitle?: string;52relativePath?: string;53description?: string;54counter?: number;55compute_image?: string;56license?: string;57contents?: PathContentsType;58error?: string;59customize: CustomizeType;60disabled?: boolean;61has_site_license?: boolean;62unlisted?: boolean;63authenticated?: boolean;64stars?: number;65isStarred?: boolean;66githubOrg?: string; // if given, this is being mirrored from this github org67githubRepo?: string; // if given, mirrored from this github repo.68projectAvatarImage?: string; // optional 320x320 image representing the project from which this was shared69// Do a redirect to here; this is due to names versus id and is needed when70// visiting this by following a link from within the share server that71// doesn't use the names. See https://github.com/sagemathinc/cocalc/issues/611572redirect?: string;73jupyter_api: boolean;74created: string | null; // ISO 8601 string75last_edited: string | null; // ISO 8601 string76ogUrl?: string; // Open Graph URL for social media sharing77ogImage?: string; // Open Graph image for social media sharing78}7980export default function PublicPath({81id,82path,83url,84project_id,85projectTitle,86relativePath = "",87description,88counter,89compute_image,90license,91contents,92error,93customize,94disabled,95has_site_license,96unlisted,97authenticated,98stars = 0,99isStarred: isStarred0,100githubOrg,101githubRepo,102projectAvatarImage,103redirect,104jupyter_api,105ogUrl,106}: PublicPathProps) {107useCounter(id);108const [numStars, setNumStars] = useState<number>(stars);109110const [isStarred, setIsStarred] = useState<boolean | null | undefined>(111isStarred0 ?? null,112);113useEffect(() => {114setIsStarred(isStarred0);115}, [isStarred0]);116117const [qrcode, setQrcode] = useState<string>("");118useEffect(() => {119setQrcode(location.href);120}, []);121122const [signingUp, setSigningUp] = useState<boolean>(false);123const router = useRouter();124const [invalidRedirect, setInvalidRedirect] = useState<boolean>(false);125126useEffect(() => {127if (redirect) {128// User can in theory pass in an arbitrary redirect, which could probably be dangerous (e.g., to an external129// spam/hack site!?). So we only automatically redirect to the SAME site we're on right now.130if (redirect) {131const site = siteName(redirect);132if (!site) {133// no site specified -- path relative to our own site134router.replace(redirect);135} else if (site == siteName(location.href)) {136// site specified and it is our own site.137router.replace(redirect);138} else {139// user can manually inspect url and click140setInvalidRedirect(true);141}142}143}144}, [redirect]);145146if (id == null || (redirect && !invalidRedirect)) {147return (148<div style={{ margin: "30px", textAlign: "center" }}>149<Loading style={{ fontSize: "30px" }} />150</div>151);152}153154function visibility_explanation() {155if (disabled) {156return (157<>158<Icon name="lock" /> private159</>160);161}162if (unlisted) {163return (164<>165<Icon name="eye-slash" /> unlisted166</>167);168}169if (authenticated) {170return (171<>172<Icon name={SHARE_AUTHENTICATED_ICON} /> authenticated (173{SHARE_AUTHENTICATED_EXPLANATION})174</>175);176}177}178179function visibility() {180if (unlisted || disabled || authenticated) {181return <div>{visibility_explanation()}</div>;182}183}184185async function star() {186setIsStarred(true);187setNumStars(numStars + 1);188// Actually do the api call after changing state, so it is189// maximally snappy. Also, being absolutely certain that star/unstar190// actually worked is not important.191await apiPost("/public-paths/star", { id });192}193194async function unstar() {195setIsStarred(false);196setNumStars(numStars - 1);197await apiPost("/public-paths/unstar", { id });198}199200function renderStar() {201const badge = (202<Badge203count={numStars}204style={{205marginLeft: "10px",206marginTop: "-2.5px",207}}208/>209);210if (isStarred == null) {211// not signed in ==> isStarred is null or undefined.212return (213<Button214onClick={() => {215setSigningUp(!signingUp);216}}217title={"Sign in to star"}218>219<Icon name="star" /> Star {badge}220</Button>221);222}223// Signed in so isStarred is true or false.224let btn;225if (isStarred == true) {226btn = (227<Button onClick={unstar}>228<Icon name="star-filled" style={{ color: "#eac54f" }} /> Starred{" "}229{badge}230</Button>231);232} else {233btn = (234<Button onClick={star}>235<Icon name="star" /> Star {badge}236</Button>237);238}239return (240<div>241<Space.Compact>242{btn}243<Button href={join(basePath, "stars")}>...</Button>244</Space.Compact>245</div>246);247}248249function renderProjectLink() {250if (githubOrg && githubRepo) {251return (252<Tooltip253title="Go to the top level of the repository."254placement="right"255>256<b>257<Icon name="home" /> GitHub Repository:{" "}258</b>259<A href={`/github/${githubOrg}/${githubRepo}`}>260{githubOrg}/{githubRepo}261</A>262<br />263</Tooltip>264);265}266if (url) {267let name, target;268const i = url.indexOf("/");269if (url.startsWith("gist")) {270target = `https://gist.github.com/${url.slice(i + 1)}`;271name = "GitHub Gist";272} else {273target = "https://" + url.slice(i + 1);274name = "URL";275}276// NOTE: it could conceivable only be http:// display will work, but this277// link will be wrong. I'm not going to worry about that.278return (279<Tooltip280placement="right"281title={`This file is hosted at ${target}. Click to open in a new tab.`}282>283<b>284<Icon name="external-link" /> {name}:{" "}285</b>286<A href={target}>{target}</A>287<br />288</Tooltip>289);290}291return (292<div>293<ProjectLink project_id={project_id} title={projectTitle} />294<br />295</div>296);297}298299function renderPathLink() {300if (githubRepo) {301const segments = url.split("/");302return (303<Tooltip304placement="right"305title="This is hosted on GitHub. Click to open GitHub in a new tab."306>307<b>308<Icon name="external-link" /> Path:{" "}309</b>310<A href={`https://github.com/${join(...segments.slice(1))}`}>311{segments.length > 3312? join(...segments.slice(3))313: join(...segments.slice(1))}314</A>315<br />316</Tooltip>317);318}319320if (url) return;321322return (323<div>324<LinkedPath325path={path}326relativePath={relativePath}327id={id}328isDir={contents?.isdir}329/>330<br />331</div>332);333}334335return (336<Customize value={customize}>337<Layout338title={getTitle({ path, relativePath })}339top={340projectAvatarImage ? (341<AntdAvatar342shape="square"343size={160}344icon={345<img346src={projectAvatarImage}347alt={`Avatar for ${projectTitle}.`}348/>349}350style={{ float: "left", margin: "20px" }}351/>352) : undefined353}354>355{githubOrg && (356<Avatar357size={96}358name={githubOrg}359style={{ float: "right", marginLeft: "15px" }}360/>361)}362<div>363{invalidRedirect && (364<Alert365type="warning"366message={367<>368<Icon name="external-link" /> External Redirect369</>370}371description={372<div>373The author has configured a redirect to:{" "}374<div style={{ fontSize: "13pt", textAlign: "center" }}>375<A href={redirect}>{redirect}</A>376</div>377</div>378}379style={{ margin: "15px 0" }}380/>381)}382<PathActions383id={id}384path={path}385url={url}386relativePath={relativePath}387isDir={contents?.isdir}388exclude={new Set(["hosted"])}389project_id={project_id}390image={compute_image}391description={description}392has_site_license={has_site_license}393/>394<Space395style={{396marginTop: "-30px",397float: "right",398justifyContent: "flex-end",399}}400direction="vertical"401>402<Flex>403<div style={{ flex: 1 }} />404{qrcode && <QRCode value={qrcode} size={110} color="#5a687d" />}405</Flex>406<div style={{ float: "right" }}>{renderStar()}</div>407</Space>408{signingUp && (409<Alert410closable411onClick={() => setSigningUp(false)}412style={{ margin: "0 auto", maxWidth: "400px" }}413type="warning"414message={415<InPlaceSignInOrUp416title="Star Shared Files"417why="to star this"418onSuccess={() => {419star();420setSigningUp(false);421router.reload();422}}423/>424}425/>426)}427{description?.trim() && (428<SanitizedMarkdown429style={430{ marginBottom: "-1em" } /* -1em to undo it being a paragraph */431}432value={description}433/>434)}435436{renderProjectLink()}437{renderPathLink()}438{counter && (439<>440<Badge count={counter} /> views441<br />442</>443)}444{license && (445<>446<b>License:</b> <License license={license} />447<br />448</>449)}450{visibility()}451{compute_image && (452<>453{compute_image}454<br />455</>456)}457</div>458{ogUrl && (459<SocialMediaShareLinks460title={getTitle({ path, relativePath })}461url={ogUrl}462showText463/>464)}465<Divider />466{error != null && (467<Alert468showIcon469type="error"470style={{ maxWidth: "700px", margin: "30px auto" }}471message="Error loading file"472description={473<div>474There was a problem loading{" "}475{relativePath ? relativePath : "this file"} in{" "}476<Link href={`/share/public_paths/${id}`}>{path}.</Link>477<br />478<br />479{error}480</div>481}482/>483)}484{contents != null && (485<PathContents486id={id}487relativePath={relativePath}488path={path}489jupyter_api={jupyter_api}490{...contents}491/>492)}493</Layout>494</Customize>495);496}497498function siteName(url) {499const i = url.indexOf("://");500if (i == -1) {501return "";502}503const j = url.indexOf("/", i + 3);504if (j == -1) {505return url;506}507return url.slice(0, j);508}509510511