Merge pull request #1013 from filecoin-project/@martinalong/single-file-view

@martinalong/single file view
This commit is contained in:
martinalong 2021-11-17 11:53:33 -08:00 committed by GitHub
commit 7319209df8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 250 additions and 148 deletions

View File

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

View File

@ -166,3 +166,7 @@ export const getImageUrlIfExists = (file, sizeLimit = null) => {
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
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";
@ -75,7 +74,6 @@ const SCENES = {
NAV_DIRECTORY: <SceneDirectory />,
NAV_PROFILE: <SceneProfile />,
NAV_DATA: <SceneFilesFolder />,
// NAV_FILE: <SceneFile />,
NAV_SLATE: <SceneSlate />,
NAV_API: <SceneSettingsDeveloper />,
NAV_SETTINGS: <SceneEditAccount />,

View File

@ -4,6 +4,7 @@ import * as Styles from "~/common/styles";
import * as UserBehaviors from "~/common/user-behaviors";
import * as Strings from "~/common/strings";
import * as Environment from "~/common/environment";
import * as Utilities from "~/common/utilities";
import { ButtonPrimaryFull, PopoverNavigation } from "~/components/system";
import { css } from "@emotion/react";
@ -129,7 +130,7 @@ export class ApplicationUserControlsPopup extends React.Component {
render() {
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 { stats } = this.props.viewer;

View File

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

View File

@ -9,13 +9,13 @@ import isEqual from "lodash/isEqual";
import Dismissible from "~/components/core/Dismissible";
const STYLES_AVATAR = css`
display: inline-flex;
background-size: cover;
background-position: 50% 50%;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
position: relative;
border-radius: 4px;
background-color: ${Constants.system.black};
display: block;
`;
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 Styles from "~/common/styles";
import * as Jumpers from "~/components/system/components/GlobalCarousel/jumpers";
import * as Utilities from "~/common/utilities";
import { css } from "@emotion/react";
import { Alert } from "~/components/core/Alert";
@ -19,6 +20,7 @@ import { ModalPortal } from "~/components/core/ModalPortal";
import SlateMediaObject from "~/components/core/SlateMediaObject";
import LinkIcon from "~/components/core/LinkIcon";
import ProfilePhoto from "~/components/core/ProfilePhoto";
/* -------------------------------------------------------------------------------------------------
* Carousel Header
@ -79,6 +81,7 @@ const STYLES_ACTION_BUTTON = css`
`;
function CarouselHeader({
isStandalone,
viewer,
data,
external,
@ -295,11 +298,27 @@ function CarouselHeader({
{file.isLink ? <VisitLinkButton file={file} /> : null}
</div>
</AnimateSharedLayout>
<div style={{ marginLeft: 80 }}>
<button onClick={onClose} css={STYLES_ACTION_BUTTON}>
<SVG.Dismiss />
</button>
</div>
{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 }}>
<button onClick={onClose} css={STYLES_ACTION_BUTTON}>
<SVG.Dismiss />
</button>
</div>
)}
</div>
</motion.nav>
</>
@ -346,27 +365,71 @@ const STYLES_CAROUSEL_MOBILE_SLIDE_COUNT = css`
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 (
<nav css={STYLES_CAROUSEL_MOBILE_HEADER} style={{ justifyContent: "space-between" }}>
<div style={{ width: 76 }}>
<button css={STYLES_ACTION_BUTTON} onClick={onPreviousSlide}>
<SVG.ChevronLeft width={16} height={16} />
</button>
<button style={{ marginLeft: 12 }} css={STYLES_ACTION_BUTTON} onClick={onNextSlide}>
<SVG.ChevronRight width={16} height={16} />
</button>
</div>
{!isStandalone && (
<>
<div style={{ width: 76 }}>
<button
css={STYLES_ACTION_BUTTON}
disabled={isPreviousButtonDisabled}
style={isPreviousButtonDisabled ? { color: Constants.system.grayLight3 } : null}
onClick={onPreviousSlide}
>
<SVG.ChevronLeft width={16} height={16} />
</button>
<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} />
</button>
</div>
<System.H5 color="textGray" as="p" css={STYLES_CAROUSEL_MOBILE_SLIDE_COUNT}>
{current} / {total}
</System.H5>
<System.H5 color="textGray" as="p" css={STYLES_CAROUSEL_MOBILE_SLIDE_COUNT}>
{current} / {total}
</System.H5>
</>
)}
<div style={{ textAlign: "right" }}>
<button onClick={onClose} css={STYLES_ACTION_BUTTON}>
<SVG.Dismiss />
</button>
</div>
{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" }}>
<button onClick={onClose} css={STYLES_ACTION_BUTTON}>
<SVG.Dismiss />
</button>
</div>
)}
</nav>
);
}
@ -617,7 +680,6 @@ const STYLES_PREVIEW_WRAPPER = (theme) => css`
`;
export function CarouselContent({
carouselType,
objects,
index,
data,
@ -629,9 +691,6 @@ export function CarouselContent({
}) {
const file = objects?.[index];
let isRepost = false;
if (carouselType === "SLATE") isRepost = data?.ownerId !== file.ownerId;
useLockScroll();
return (
@ -730,7 +789,7 @@ const getCarouselHandlers = ({ index, objects, params, onChange, onAction }) =>
let { cid } = objects[prevIndex];
onChange(prevIndex);
onAction({ type: "UPDATE_PARAMS", params: { params, cid }, redirect: true });
onAction({ type: "UPDATE_PARAMS", params: { ...params, cid }, redirect: true });
};
const handleClose = (e) => {
@ -782,7 +841,7 @@ const STYLES_ROOT = (theme) => css`
`;
export function GlobalCarousel({
carouselType,
isStandalone,
objects,
index,
params,
@ -797,7 +856,7 @@ export function GlobalCarousel({
style,
}) {
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 });
@ -815,6 +874,8 @@ export function GlobalCarousel({
<div css={STYLES_ROOT}>
{isMobile ? (
<CarouselHeaderMobile
isStandalone={isStandalone}
file={file}
current={index + 1}
total={objects.length}
onPreviousSlide={handlePrevious}
@ -823,6 +884,7 @@ export function GlobalCarousel({
/>
) : (
<CarouselHeader
isStandalone={isStandalone}
viewer={viewer}
external={external}
isOwner={isOwner}
@ -839,7 +901,6 @@ export function GlobalCarousel({
/>
)}
<CarouselContent
carouselType={carouselType}
objects={objects}
index={index}
data={data}

View File

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

View File

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

View File

@ -465,7 +465,6 @@ class SlatePage extends React.Component {
{objects && objects.length ? (
<>
<GlobalCarousel
carouselType="SLATE"
viewer={this.props.viewer}
objects={objects}
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) => {
let isMobile = Window.isMobileBrowser(req.headers["user-agent"]);
let isMac = Window.isMac(req.headers["user-agent"]);