added standalone file page and edited share modal to use it

This commit is contained in:
Martina 2021-11-16 16:52:19 -08:00
parent dd386ba452
commit d9e2c89659
15 changed files with 249 additions and 147 deletions

View File

@ -4,14 +4,19 @@ import { css } from "@emotion/react";
/* TYPOGRAPHY */ /* TYPOGRAPHY */
export const OVERFLOW_ELLIPSIS = css`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
export const LINK = css` export const LINK = css`
text-decoration: none; text-decoration: none;
color: ${Constants.system.blue}; color: ${Constants.semantic.textBlack};
cursor: pointer; cursor: pointer;
transition: 200ms ease color;
:visited { :visited {
color: ${Constants.system.blue}; color: ${Constants.semantic.textBlack};
} }
`; `;

View File

@ -166,3 +166,7 @@ export const getImageUrlIfExists = (file, sizeLimit = null) => {
return file.linkImage; return file.linkImage;
} }
}; };
export const getUserDisplayName = (user) => {
return user.name || `@${user.username}`;
};

View File

@ -17,7 +17,6 @@ import * as Environment from "~/common/environment";
// Scenes each have an ID and can be navigated to with _handleAction // Scenes each have an ID and can be navigated to with _handleAction
import SceneError from "~/scenes/SceneError"; import SceneError from "~/scenes/SceneError";
import SceneEditAccount from "~/scenes/SceneEditAccount"; import SceneEditAccount from "~/scenes/SceneEditAccount";
import SceneFile from "~/scenes/SceneFile";
import SceneFilesFolder from "~/scenes/SceneFilesFolder"; import SceneFilesFolder from "~/scenes/SceneFilesFolder";
import SceneSettings from "~/scenes/SceneSettings"; import SceneSettings from "~/scenes/SceneSettings";
import SceneSlates from "~/scenes/SceneSlates"; import SceneSlates from "~/scenes/SceneSlates";
@ -75,7 +74,6 @@ const SCENES = {
NAV_DIRECTORY: <SceneDirectory />, NAV_DIRECTORY: <SceneDirectory />,
NAV_PROFILE: <SceneProfile />, NAV_PROFILE: <SceneProfile />,
NAV_DATA: <SceneFilesFolder />, NAV_DATA: <SceneFilesFolder />,
// NAV_FILE: <SceneFile />,
NAV_SLATE: <SceneSlate />, NAV_SLATE: <SceneSlate />,
NAV_API: <SceneSettingsDeveloper />, NAV_API: <SceneSettingsDeveloper />,
NAV_SETTINGS: <SceneEditAccount />, NAV_SETTINGS: <SceneEditAccount />,

View File

@ -4,6 +4,7 @@ import * as Styles from "~/common/styles";
import * as UserBehaviors from "~/common/user-behaviors"; import * as UserBehaviors from "~/common/user-behaviors";
import * as Strings from "~/common/strings"; import * as Strings from "~/common/strings";
import * as Environment from "~/common/environment"; import * as Environment from "~/common/environment";
import * as Utilities from "~/common/utilities";
import { ButtonPrimaryFull, PopoverNavigation } from "~/components/system"; import { ButtonPrimaryFull, PopoverNavigation } from "~/components/system";
import { css } from "@emotion/react"; import { css } from "@emotion/react";
@ -129,7 +130,7 @@ export class ApplicationUserControlsPopup extends React.Component {
render() { render() {
if (this.props.popup !== "profile") return null; if (this.props.popup !== "profile") return null;
const username = this.props.viewer.name || `@${this.props.viewer.username}`; const username = Utilities.getUserDisplayName(this.props.viewer);
const objectsLength = this.props.viewer.library.length; const objectsLength = this.props.viewer.library.length;
const { stats } = this.props.viewer; const { stats } = this.props.viewer;

View File

@ -67,11 +67,9 @@ export default function LinkTag({ url, ...props }) {
<System.P2 <System.P2
style={{ style={{
paddingRight: 8, paddingRight: 8,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
...props.style, ...props.style,
}} }}
css={Styles.OVERFLOW_ELLIPSIS}
> >
{url} {url}
</System.P2> </System.P2>

View File

@ -9,13 +9,13 @@ import isEqual from "lodash/isEqual";
import Dismissible from "~/components/core/Dismissible"; import Dismissible from "~/components/core/Dismissible";
const STYLES_AVATAR = css` const STYLES_AVATAR = css`
display: inline-flex;
background-size: cover; background-size: cover;
background-position: 50% 50%; background-position: 50% 50%;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
position: relative; position: relative;
border-radius: 4px; border-radius: 4px;
background-color: ${Constants.system.black}; background-color: ${Constants.system.black};
display: block;
`; `;
const STYLES_AVATAR_ONLINE = css` const STYLES_AVATAR_ONLINE = css`

View File

@ -4,6 +4,7 @@ import * as SVG from "~/common/svg";
import * as System from "~/components/system"; import * as System from "~/components/system";
import * as Styles from "~/common/styles"; import * as Styles from "~/common/styles";
import * as Jumpers from "~/components/system/components/GlobalCarousel/jumpers"; import * as Jumpers from "~/components/system/components/GlobalCarousel/jumpers";
import * as Utilities from "~/common/utilities";
import { css } from "@emotion/react"; import { css } from "@emotion/react";
import { Alert } from "~/components/core/Alert"; import { Alert } from "~/components/core/Alert";
@ -19,6 +20,7 @@ import { ModalPortal } from "~/components/core/ModalPortal";
import SlateMediaObject from "~/components/core/SlateMediaObject"; import SlateMediaObject from "~/components/core/SlateMediaObject";
import LinkIcon from "~/components/core/LinkIcon"; import LinkIcon from "~/components/core/LinkIcon";
import ProfilePhoto from "~/components/core/ProfilePhoto";
/* ------------------------------------------------------------------------------------------------- /* -------------------------------------------------------------------------------------------------
* Carousel Header * Carousel Header
@ -79,6 +81,7 @@ const STYLES_ACTION_BUTTON = css`
`; `;
function CarouselHeader({ function CarouselHeader({
isStandalone,
viewer, viewer,
data, data,
external, external,
@ -295,11 +298,27 @@ function CarouselHeader({
{file.isLink ? <VisitLinkButton file={file} /> : null} {file.isLink ? <VisitLinkButton file={file} /> : null}
</div> </div>
</AnimateSharedLayout> </AnimateSharedLayout>
{isStandalone ? (
<a href={`/${file.owner.username}`} css={Styles.LINK} style={{ marginLeft: 80 }}>
<div
style={{ gap: 8, maxWidth: "138px", justifyContent: "flex-end" }}
css={Styles.HORIZONTAL_CONTAINER_CENTERED}
>
<div>
<ProfilePhoto user={file.owner} style={{ borderRadius: "8px" }} size={20} />
</div>
<p css={[Styles.H5, Styles.OVERFLOW_ELLIPSIS]}>
{Utilities.getUserDisplayName(file.owner)}
</p>
</div>
</a>
) : (
<div style={{ marginLeft: 80 }}> <div style={{ marginLeft: 80 }}>
<button onClick={onClose} css={STYLES_ACTION_BUTTON}> <button onClick={onClose} css={STYLES_ACTION_BUTTON}>
<SVG.Dismiss /> <SVG.Dismiss />
</button> </button>
</div> </div>
)}
</div> </div>
</motion.nav> </motion.nav>
</> </>
@ -346,14 +365,42 @@ const STYLES_CAROUSEL_MOBILE_SLIDE_COUNT = css`
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
`; `;
function CarouselHeaderMobile({ current, total, onClose, onNextSlide, onPreviousSlide }) { function CarouselHeaderMobile({
isStandalone,
file,
current,
total,
onClose,
onNextSlide,
onPreviousSlide,
}) {
const isPreviousButtonDisabled = current === 1;
const isNextButtonDisabled = current === total;
return ( return (
<nav css={STYLES_CAROUSEL_MOBILE_HEADER} style={{ justifyContent: "space-between" }}> <nav css={STYLES_CAROUSEL_MOBILE_HEADER} style={{ justifyContent: "space-between" }}>
{!isStandalone && (
<>
<div style={{ width: 76 }}> <div style={{ width: 76 }}>
<button css={STYLES_ACTION_BUTTON} onClick={onPreviousSlide}> <button
css={STYLES_ACTION_BUTTON}
disabled={isPreviousButtonDisabled}
style={isPreviousButtonDisabled ? { color: Constants.system.grayLight3 } : null}
onClick={onPreviousSlide}
>
<SVG.ChevronLeft width={16} height={16} /> <SVG.ChevronLeft width={16} height={16} />
</button> </button>
<button style={{ marginLeft: 12 }} css={STYLES_ACTION_BUTTON} onClick={onNextSlide}> <button
style={
isNextButtonDisabled
? { color: Constants.system.grayLight3, marginLeft: 12 }
: {
marginLeft: 12,
}
}
disabled={isNextButtonDisabled}
css={STYLES_ACTION_BUTTON}
onClick={onNextSlide}
>
<SVG.ChevronRight width={16} height={16} /> <SVG.ChevronRight width={16} height={16} />
</button> </button>
</div> </div>
@ -361,12 +408,28 @@ function CarouselHeaderMobile({ current, total, onClose, onNextSlide, onPrevious
<System.H5 color="textGray" as="p" css={STYLES_CAROUSEL_MOBILE_SLIDE_COUNT}> <System.H5 color="textGray" as="p" css={STYLES_CAROUSEL_MOBILE_SLIDE_COUNT}>
{current} / {total} {current} / {total}
</System.H5> </System.H5>
</>
)}
{isStandalone ? (
<div
style={{ gap: 8, maxWidth: "138px", justifyContent: "flex-end" }}
css={Styles.HORIZONTAL_CONTAINER_CENTERED}
>
<div>
<ProfilePhoto user={file.owner} style={{ borderRadius: "8px" }} size={20} />
</div>
<p css={[Styles.H5, Styles.OVERFLOW_ELLIPSIS]}>
{Utilities.getUserDisplayName(file.owner)}
</p>
</div>
) : (
<div style={{ textAlign: "right" }}> <div style={{ textAlign: "right" }}>
<button onClick={onClose} css={STYLES_ACTION_BUTTON}> <button onClick={onClose} css={STYLES_ACTION_BUTTON}>
<SVG.Dismiss /> <SVG.Dismiss />
</button> </button>
</div> </div>
)}
</nav> </nav>
); );
} }
@ -617,7 +680,6 @@ const STYLES_PREVIEW_WRAPPER = (theme) => css`
`; `;
export function CarouselContent({ export function CarouselContent({
carouselType,
objects, objects,
index, index,
data, data,
@ -629,9 +691,6 @@ export function CarouselContent({
}) { }) {
const file = objects?.[index]; const file = objects?.[index];
let isRepost = false;
if (carouselType === "SLATE") isRepost = data?.ownerId !== file.ownerId;
useLockScroll(); useLockScroll();
return ( return (
@ -782,7 +841,7 @@ const STYLES_ROOT = (theme) => css`
`; `;
export function GlobalCarousel({ export function GlobalCarousel({
carouselType, isStandalone,
objects, objects,
index, index,
params, params,
@ -797,7 +856,7 @@ export function GlobalCarousel({
style, style,
}) { }) {
const file = objects?.[index]; const file = objects?.[index];
const isCarouselOpen = (carouselType || index > 0 || index <= objects.length) && !!file; const isCarouselOpen = (index > 0 || index <= objects.length) && !!file;
useCarouselViaParams({ index, params, objects, onChange }); useCarouselViaParams({ index, params, objects, onChange });
@ -815,6 +874,8 @@ export function GlobalCarousel({
<div css={STYLES_ROOT}> <div css={STYLES_ROOT}>
{isMobile ? ( {isMobile ? (
<CarouselHeaderMobile <CarouselHeaderMobile
isStandalone={isStandalone}
file={file}
current={index + 1} current={index + 1}
total={objects.length} total={objects.length}
onPreviousSlide={handlePrevious} onPreviousSlide={handlePrevious}
@ -823,6 +884,7 @@ export function GlobalCarousel({
/> />
) : ( ) : (
<CarouselHeader <CarouselHeader
isStandalone={isStandalone}
viewer={viewer} viewer={viewer}
external={external} external={external}
isOwner={isOwner} isOwner={isOwner}
@ -839,7 +901,6 @@ export function GlobalCarousel({
/> />
)} )}
<CarouselContent <CarouselContent
carouselType={carouselType}
objects={objects} objects={objects}
index={index} index={index}
data={data} data={data}

View File

@ -36,6 +36,12 @@ const getSlateURLFromViewer = ({ viewer, file }) => {
return `${rootUrl}/${username}/${collection.slatename}?cid=${file.cid}`; return `${rootUrl}/${username}/${collection.slatename}?cid=${file.cid}`;
}; };
const getFileURL = ({ file }) => {
const rootUrl = window?.location?.origin;
return `${rootUrl}/_/object/${file.id}`;
};
const getSlateURLFromData = ({ data, file }) => { const getSlateURLFromData = ({ data, file }) => {
const username = data?.user?.username; const username = data?.user?.username;
const rootUrl = window?.location?.origin; const rootUrl = window?.location?.origin;
@ -46,9 +52,7 @@ const getSlateURLFromData = ({ data, file }) => {
function FileSharingButtons({ file, data, viewer }) { function FileSharingButtons({ file, data, viewer }) {
const fileName = file?.name || file?.filename; const fileName = file?.name || file?.filename;
const username = data?.user?.username || viewer?.username; const username = data?.user?.username || viewer?.username;
const fileLink = data const fileLink = getFileURL({ file });
? getSlateURLFromData({ data, file })
: getSlateURLFromViewer({ viewer, file });
const [copyState, setCopyState] = React.useState({ isCidCopied: false, isLinkCopied: false }); const [copyState, setCopyState] = React.useState({ isCidCopied: false, isLinkCopied: false });
const handleTwitterSharing = () => const handleTwitterSharing = () =>
@ -61,9 +65,8 @@ function FileSharingButtons({ file, data, viewer }) {
window.open(`mailto: ?subject=${fileName} by ${username} on Slate&body=${fileLink}`, "_b"); window.open(`mailto: ?subject=${fileName} by ${username} on Slate&body=${fileLink}`, "_b");
}; };
const cidLink = Strings.getURLfromCID(file.cid);
const handleLinkCopy = () => ( const handleLinkCopy = () => (
Utilities.copyToClipboard(cidLink), setCopyState({ isLinkCopied: true }) Utilities.copyToClipboard(fileLink), setCopyState({ isLinkCopied: true })
); );
const handleCidCopy = () => ( const handleCidCopy = () => (
Utilities.copyToClipboard(file.cid), setCopyState({ isCidCopied: true }) Utilities.copyToClipboard(file.cid), setCopyState({ isCidCopied: true })
@ -82,7 +85,7 @@ function FileSharingButtons({ file, data, viewer }) {
<button css={STYLES_SHARING_BUTTON} onClick={handleLinkCopy}> <button css={STYLES_SHARING_BUTTON} onClick={handleLinkCopy}>
<SVG.Link width={16} /> <SVG.Link width={16} />
<System.P2 style={{ marginLeft: 12 }}> <System.P2 style={{ marginLeft: 12 }}>
{copyState.isLinkCopied ? "Copied" : "Copy CID link"} {copyState.isLinkCopied ? "Copied" : "Copy link"}
</System.P2> </System.P2>
</button> </button>
<button css={STYLES_SHARING_BUTTON} onClick={handleCidCopy}> <button css={STYLES_SHARING_BUTTON} onClick={handleCidCopy}>

99
pages/_/file.js Normal file
View File

@ -0,0 +1,99 @@
import * as React from "react";
import * as Constants from "~/common/constants";
import * as Strings from "~/common/strings";
import * as Events from "~/common/custom-events";
import { css } from "@emotion/react";
import { GlobalCarousel } from "~/components/system/components/GlobalCarousel";
import { ButtonPrimary } from "~/components/system/components/Buttons";
import Profile from "~/components/core/Profile";
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
import WebsitePrototypeHeader from "~/components/core/WebsitePrototypeHeader";
import WebsitePrototypeFooter from "~/components/core/WebsitePrototypeFooter";
import CTATransition from "~/components/core/CTATransition";
const DEFAULT_IMAGE =
"https://slate.textile.io/ipfs/bafkreiaow45dlq5xaydaeqocdxvffudibrzh2c6qandpqkb6t3ahbvh6re";
export const getServerSideProps = async (context) => {
return {
props: { ...context.query },
};
};
const STYLES_ROOT = css`
display: block;
grid-template-rows: auto 1fr auto;
font-size: 1rem;
min-height: 100vh;
background-color: ${Constants.semantic.bgLight};
`;
export default class ProfilePage extends React.Component {
state = {
visible: false,
page: null,
};
componentDidMount = () => {
console.log(this.props.data);
// window.onpopstate = this._handleBackForward;
// if (!Strings.isEmpty(this.props.cid)) {
// let files = this.props.creator.library || [];
// let index = files.findIndex((object) => object.cid === this.props.cid);
// if (index !== -1) {
// Events.dispatchCustomEvent({
// name: "slate-global-open-carousel",
// detail: { index },
// });
// }
// }
};
// _handleBackForward = (e) => {
// let page = window.history.state;
// this.setState({ page });
// Events.dispatchCustomEvent({ name: "slate-global-close-carousel", detail: {} });
// };
render() {
const isMobile = this.props.isMobile;
const viewer = this.props.viewer;
const file = this.props.data;
const { filename: title, body: description } = this.props.data;
// const title = this.props.data
// ? this.props.creator.name
// ? `${this.props.creator.name} on Slate`
// : `@${this.props.creator.username} on Slate`
// : "404";
const url = `https://slate.host/${title}`;
// const description = this.props.creator.body;
const image = DEFAULT_IMAGE; //this.props.creator.photo;
// if (Strings.isEmpty(image)) {
// image = DEFAULT_IMAGE;
// }
return (
<WebsitePrototypeWrapper title={title} description={description} url={url} image={image}>
<WebsitePrototypeHeader />
<div css={STYLES_ROOT}>
<GlobalCarousel
isStandalone
viewer={viewer}
objects={[file]}
onAction={() => {}}
isMobile={isMobile}
// params={page.params}
isOwner={viewer.id === file.ownerId}
index={0}
onChange={() => {}}
/>
</div>
<WebsitePrototypeFooter />
</WebsitePrototypeWrapper>
);
}
}

View File

@ -310,7 +310,6 @@ export default class SlatePage extends React.Component {
<div css={STYLES_SLATE}> <div css={STYLES_SLATE}>
<GlobalCarousel <GlobalCarousel
data={this.props.slate} data={this.props.slate}
carouselType="SLATE"
viewer={this.props.viewer} viewer={this.props.viewer}
objects={objects} objects={objects}
isMobile={this.props.isMobile} isMobile={this.props.isMobile}

View File

@ -96,7 +96,6 @@ export default function SceneActivity({ page, viewer, external, onAction, ...pro
</ScenePage> </ScenePage>
<GlobalCarousel <GlobalCarousel
carouselType="ACTIVITY"
viewer={viewer} viewer={viewer}
objects={globalCarouselState.currentObjects} objects={globalCarouselState.currentObjects}
index={globalCarouselState.currentCarousel} index={globalCarouselState.currentCarousel}

View File

@ -1,98 +0,0 @@
import * as React from "react";
import * as Constants from "~/common/constants";
import * as SVG from "~/common/svg";
import * as Strings from "~/common/strings";
import { css } from "@emotion/react";
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
import SlateMediaObject from "~/components/core/SlateMediaObject";
const STYLES_FLEX = css`
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
flex-direction: column;
height: 100vh;
background-color: ${Constants.system.black};
`;
const STYLES_TOP = css`
background: ${Constants.system.black};
border-bottom: 1px solid ${Constants.system.black};
color: ${Constants.system.white};
width: 100%;
padding: 12px 16px 12px 16px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
`;
const STYLES_LEFT = css`
min-width: 10%;
width: 100%;
padding-right: 16px;
`;
const STYLES_RIGHT = css`
flex-shrink: 0;
cursor: pointer;
height: 100%;
padding-top: 4px;
transition: 200ms ease color;
user-select: none;
:hover {
color: ${Constants.system.blue};
}
`;
const STYLES_BOTTOM = css`
background: ${Constants.system.black};
color: ${Constants.system.white};
padding: 12px 16px 12px 48px;
width: 100%;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
`;
const STYLES_PATH = css`
font-family: "mono";
color: ${Constants.system.white};
font-size: 12px;
text-transform: uppercase;
overflow-wrap: break-word;
user-select: none;
`;
export default class SceneFile extends React.Component {
render() {
const cid = this.props.data.cid;
const fileURL = Strings.getURLfromCID(cid);
return (
<WebsitePrototypeWrapper
title={`${this.props.page.pageTitle} • Slate`}
url={`${Constants.hostname}${this.props.page.pathname}`}
>
<div css={STYLES_FLEX}>
<div css={STYLES_TOP}>
<div css={STYLES_LEFT}>
<a css={STYLES_PATH} href={fileURL} target="_blank">
{fileURL}
</a>
</div>
<div css={STYLES_RIGHT}>
<SVG.Dismiss height="24px" />
</div>
</div>
<SlateMediaObject file={this.props.data} />
</div>
</WebsitePrototypeWrapper>
);
}
}

View File

@ -36,7 +36,6 @@ export default function SceneFilesFolder({ viewer, page, onAction, isMobile }) {
> >
<ScenePage css={STYLES_SCENE_PAGE}> <ScenePage css={STYLES_SCENE_PAGE}>
<GlobalCarousel <GlobalCarousel
carouselType="DATA"
viewer={viewer} viewer={viewer}
objects={objects} objects={objects}
onAction={onAction} onAction={onAction}

View File

@ -465,7 +465,6 @@ class SlatePage extends React.Component {
{objects && objects.length ? ( {objects && objects.length ? (
<> <>
<GlobalCarousel <GlobalCarousel
carouselType="SLATE"
viewer={this.props.viewer} viewer={this.props.viewer}
objects={objects} objects={objects}
data={this.props.data} data={this.props.data}

View File

@ -122,6 +122,41 @@ app.prepare().then(async () => {
// }); // });
}); });
server.get("/_/object/:id", async (req, res) => {
let isMobile = Window.isMobileBrowser(req.headers["user-agent"]);
let isMac = Window.isMac(req.headers["user-agent"]);
const fileId = req.params.id;
const file = await Data.getFileById({ id: fileId });
const id = Utilities.getIdFromCookie(req);
if (!file.isPublic && file.ownerId !== id) {
return res.redirect("/_/404");
}
let viewer = null;
if (id) {
viewer = await ViewerManager.getById({
id,
});
}
if (id && id === file.ownerId) {
file.owner = viewer;
} else {
file.owner = await Data.getUserById({ id: file.ownerId, sanitize: true });
}
return app.render(req, res, "/_/file", {
viewer,
isMobile,
isMac,
data: file,
});
});
server.get("/_/:scene", async (req, res) => { server.get("/_/:scene", async (req, res) => {
let isMobile = Window.isMobileBrowser(req.headers["user-agent"]); let isMobile = Window.isMobileBrowser(req.headers["user-agent"]);
let isMac = Window.isMac(req.headers["user-agent"]); let isMac = Window.isMac(req.headers["user-agent"]);