reworked profile public files

This commit is contained in:
Martina 2021-01-20 13:50:29 -08:00
parent dcd0f844eb
commit e4124f28d1
10 changed files with 285 additions and 335 deletions

View File

@ -176,7 +176,7 @@ export const bytesToSize = (bytes, decimals = 2) => {
const k = 1024; const k = 1024;
const dm = decimals < 0 ? 0 : decimals; const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));

View File

@ -10,8 +10,13 @@ import * as Events from "~/common/custom-events";
import Cookies from "universal-cookie"; import Cookies from "universal-cookie";
import JSZip from "jszip"; import JSZip from "jszip";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
//NOTE(martina): this file is for utility *API-calling* functions
//For non API related utility functions, see common/utilities.js
//And for uploading related utility functions, see common/file-utilities.js
const cookies = new Cookies(); const cookies = new Cookies();
export const authenticate = async (state) => { export const authenticate = async (state) => {

24
common/utilities.js Normal file
View File

@ -0,0 +1,24 @@
//NOTE(martina): this file is for utility functions that do not involve API calls
//For API related utility functions, see common/user-behaviors.js
//And for uploading related utility functions, see common/file-utilities.js
export const getPublicAndPrivateFiles = ({ viewer }) => {
let publicFileIds = [];
for (let slate of viewer.slates) {
if (slate.data.public) {
publicFileIds.push(...slate.data.objects.map((obj) => obj.id));
}
}
let publicFiles = [];
let privateFiles = [];
let library = viewer.library[0]?.children || [];
for (let file of library) {
if (file.public || publicFileIds.includes(file.id)) {
publicFiles.push(file);
} else {
privateFiles.push(file);
}
}
return { publicFiles, privateFiles };
};

View File

@ -489,7 +489,7 @@ export default class CarouselSidebarData extends React.Component {
<div css={STYLES_ACTION} onClick={() => this._handleCopy(url, "gatewayUrlCopying")}> <div css={STYLES_ACTION} onClick={() => this._handleCopy(url, "gatewayUrlCopying")}>
<SVG.Data height="24px" /> <SVG.Data height="24px" />
<span style={{ marginLeft: 16 }}> <span style={{ marginLeft: 16 }}>
{this.state.loading === "gatewayUrlCopying" ? "Copied!" : "Copy external URL"} {this.state.loading === "gatewayUrlCopying" ? "Copied!" : "Copy file URL"}
</span> </span>
</div> </div>
<div css={STYLES_ACTION} onClick={this._handleDownload}> <div css={STYLES_ACTION} onClick={this._handleDownload}>

View File

@ -3,6 +3,7 @@ import * as Constants from "~/common/constants";
import * as Strings from "~/common/strings"; import * as Strings from "~/common/strings";
import * as SVG from "~/common/svg"; import * as SVG from "~/common/svg";
import * as Actions from "~/common/actions"; import * as Actions from "~/common/actions";
import * as Utilities from "~/common/utilities";
import { GlobalCarousel } from "~/components/system/components/GlobalCarousel"; import { GlobalCarousel } from "~/components/system/components/GlobalCarousel";
import { css } from "@emotion/react"; import { css } from "@emotion/react";
@ -235,19 +236,15 @@ function UserEntry({ user, button, onClick, message, external, url }) {
export default class Profile extends React.Component { export default class Profile extends React.Component {
_ref = null; _ref = null;
lastLength = null;
state = { state = {
tab: 1, tab: 1,
view: 0, view: 0,
fileTab: 0,
slateTab: 0, slateTab: 0,
peerTab: 0, peerTab: 0,
copyValue: "", copyValue: "",
contextMenu: null, contextMenu: null,
publicSlates: [],
publicFiles: [], publicFiles: [],
pseudoPrivateFiles: [],
isFollowing: this.props.external isFollowing: this.props.external
? false ? false
: !!this.props.viewer.subscriptions.filter((entry) => { : !!this.props.viewer.subscriptions.filter((entry) => {
@ -255,70 +252,19 @@ export default class Profile extends React.Component {
}).length, }).length,
}; };
componentDidMount = async () => { componentDidMount = () => {
await this.filterByVisibility(); this.filterByVisibility();
}; };
componentDidUpdate = async (prevProps) => { filterByVisibility = () => {
if (this.props.creator?.id !== prevProps.creator?.id) {
this.setState({
tab: 1,
view: 0,
fileTab: 0,
slateTab: 0,
peerTab: 0,
isFollowing: this.props.external
? false
: !!this.props.viewer.subscriptions.filter((entry) => {
return entry.target_user_id === this.props.creator.id;
}).length,
});
}
if (
this.lastLength != null &&
this.props.creator?.library[0].children.length !== this.lastLength
) {
this.filterByVisibility();
}
};
filterByVisibility = async () => {
let publicFiles = []; let publicFiles = [];
let pseudoPrivateFiles = []; if (this.props.isOwner) {
let files = this.props.creator.library[0].children; const res = Utilities.getPublicAndPrivateFiles({ viewer: this.props.creator });
let publicSlates = publicFiles = res.publicFiles;
this.props.creator.username === this.props.viewer?.username } else {
? this.props.creator.slates.filter((slate) => { publicFiles = this.props.creator.library[0].children;
return slate.data.public === true;
})
: this.props.creator.slates;
let publicSlateFiles = publicSlates
.reduce((acc, curr) => {
return acc.concat(curr.data.objects);
}, [])
.reduce((acc, curr) => {
return acc.concat(curr.cid);
}, [])
.reduce((acc, curr) => {
if (acc.indexOf(curr) === -1) {
acc.push(curr);
}
return acc;
}, []);
for (let file of files) {
if (file.public === true || publicSlateFiles.indexOf(file.cid) != -1) {
publicFiles.push(file);
} else {
pseudoPrivateFiles.push(file);
}
} }
this.setState({ this.setState({ publicFiles: publicFiles });
publicSlates: publicSlates,
publicFiles: publicFiles,
pseudoPrivateFiles: pseudoPrivateFiles,
});
this.lastLength = this.props.creator?.library[0].children.length;
}; };
_handleCopy = (e, value) => { _handleCopy = (e, value) => {
@ -352,182 +298,182 @@ export default class Profile extends React.Component {
}; };
render() { render() {
let isOwner = this.props.creator.username === this.props.viewer?.username; let isOwner = this.props.isOwner;
let creator = this.props.creator; let creator = this.props.creator;
let username = this.state.slateTab === 0 ? creator.username : null;
let subscriptions = this.props.creator.subscriptions || []; let subscriptions = this.props.creator.subscriptions || [];
let subscribers = this.props.creator.subscribers || []; let subscribers = this.props.creator.subscribers || [];
let dataItems = !isOwner
? this.state.publicFiles
: this.state.fileTab === 0
? this.props.creator.library[0].children
: this.state.fileTab === 1
? this.state.publicFiles
: this.state.pseudoPrivateFiles;
let exploreSlates = this.props.exploreSlates; let exploreSlates = this.props.exploreSlates;
let followingSlates = subscriptions let slates = [];
.filter((relation) => { if (this.state.tab === 1) {
return !!relation.target_slate_id; if (this.state.slateTab === 0) {
}) slates = isOwner
.map((relation) => relation.slate); ? creator.slates.filter((slate) => slate.data.public === true)
: creator.slates;
} else {
slates = subscriptions
.filter((relation) => {
return !!relation.target_slate_id;
})
.map((relation) => relation.slate);
}
}
let slates = let peers = [];
this.state.slateTab === 0 if (this.state.tab === 2) {
? this.state.publicSlates if (this.state.peerTab === 0) {
: followingSlates?.length peers = subscriptions
? followingSlates .filter((relation) => {
: null; return !!relation.target_user_id;
let username = this.state.slateTab === 0 ? creator.username : null; })
.map((relation) => {
let following = subscriptions let button = (
.filter((relation) => { <div css={STYLES_ITEM_BOX} onClick={(e) => this._handleClick(e, relation.id)}>
return !!relation.target_user_id; <SVG.MoreHorizontal height="24px" />
}) {this.state.contextMenu === relation.id ? (
.map((relation) => { <Boundary
let button = ( captureResize={true}
<div css={STYLES_ITEM_BOX} onClick={(e) => this._handleClick(e, relation.id)}> captureScroll={false}
<SVG.MoreHorizontal height="24px" /> enabled
{this.state.contextMenu === relation.id ? ( onOutsideRectEvent={(e) => this._handleClick(e, relation.id)}
<Boundary >
captureResize={true} {relation.target_user_id === this.props.viewer?.id ? (
captureScroll={false} <PopoverNavigation
enabled style={{
onOutsideRectEvent={(e) => this._handleClick(e, relation.id)} top: "40px",
> right: "0px",
{relation.target_user_id === this.props.viewer?.id ? ( }}
<PopoverNavigation navigation={[
style={{ {
top: "40px", text: "Copy profile URL",
right: "0px", onClick: (e) =>
}} this._handleCopy(e, `https://slate.host/${relation.user.username}`),
navigation={[ },
{ ]}
text: "Copy profile URL", />
onClick: (e) => ) : (
this._handleCopy(e, `https://slate.host/${relation.user.username}`), <PopoverNavigation
}, style={{
]} top: "40px",
/> right: "0px",
) : ( }}
<PopoverNavigation navigation={[
style={{ {
top: "40px", text: "Copy profile URL",
right: "0px", onClick: (e) =>
}} this._handleCopy(e, `https://slate.host/${relation.user.username}`),
navigation={[ },
{ {
text: "Copy profile URL", text: this.props.viewer?.subscriptions.filter((subscription) => {
onClick: (e) => return subscription.target_user_id === relation.target_user_id;
this._handleCopy(e, `https://slate.host/${relation.user.username}`), }).length
}, ? "Unfollow"
{ : "Follow",
text: this.props.viewer?.subscriptions.filter((subscription) => { onClick: this.props.viewer
return subscription.target_user_id === relation.target_user_id; ? (e) => this._handleFollow(e, relation.target_user_id)
}).length : () => this.setState({ visible: true }),
? "Unfollow" },
: "Follow", ]}
onClick: this.props.viewer />
? (e) => this._handleFollow(e, relation.target_user_id) )}
: () => this.setState({ visible: true }), </Boundary>
}, ) : null}
]} </div>
/> );
)} return (
</Boundary> <UserEntry
) : null} key={relation.id}
</div> user={relation.user}
); button={button}
return ( onClick={() => {
<UserEntry this.props.onAction({
key={relation.id} type: "NAVIGATE",
user={relation.user} value: this.props.sceneId,
button={button} scene: "PROFILE",
onClick={() => { data: relation.user,
this.props.onAction({ });
type: "NAVIGATE", }}
value: this.props.sceneId, external={this.props.external}
scene: "PROFILE", url={`/${relation.user.username}`}
data: relation.user, />
}); );
}} });
external={this.props.external} } else {
url={`/${relation.user.username}`} peers = subscribers.map((relation) => {
/> let button = (
); <div css={STYLES_ITEM_BOX} onClick={(e) => this._handleClick(e, relation.id)}>
}); <SVG.MoreHorizontal height="24px" />
{this.state.contextMenu === relation.id ? (
let followers = subscribers.map((relation) => { <Boundary
let button = ( captureResize={true}
<div css={STYLES_ITEM_BOX} onClick={(e) => this._handleClick(e, relation.id)}> captureScroll={false}
<SVG.MoreHorizontal height="24px" /> enabled
{this.state.contextMenu === relation.id ? ( onOutsideRectEvent={(e) => this._handleClick(e, relation.id)}
<Boundary >
captureResize={true} {relation.owner_user_id === this.props.viewer?.id ? (
captureScroll={false} <PopoverNavigation
enabled style={{
onOutsideRectEvent={(e) => this._handleClick(e, relation.id)} top: "40px",
> right: "0px",
{relation.owner_user_id === this.props.viewer?.id ? ( }}
<PopoverNavigation navigation={[
style={{ {
top: "40px", text: "Copy profile URL",
right: "0px", onClick: (e) =>
}} this._handleCopy(e, `https://slate.host/${relation.owner.username}`),
navigation={[ },
{ ]}
text: "Copy profile URL", />
onClick: (e) => ) : (
this._handleCopy(e, `https://slate.host/${relation.owner.username}`), <PopoverNavigation
}, style={{
]} top: "40px",
/> right: "0px",
) : ( }}
<PopoverNavigation navigation={[
style={{ {
top: "40px", text: "Copy profile URL",
right: "0px", onClick: (e) =>
}} this._handleCopy(e, `https://slate.host/${relation.owner.username}`),
navigation={[ },
{ {
text: "Copy profile URL", text: this.props.viewer?.subscriptions.filter((subscription) => {
onClick: (e) => return subscription.target_user_id === relation.owner_user_id;
this._handleCopy(e, `https://slate.host/${relation.owner.username}`), }).length
}, ? "Unfollow"
{ : "Follow",
text: this.props.viewer?.subscriptions.filter((subscription) => { onClick: this.props.viewer
return subscription.target_user_id === relation.owner_user_id; ? (e) => this._handleFollow(e, relation.owner_user_id)
}).length : () => this.setState({ visible: true }),
? "Unfollow" },
: "Follow", ]}
onClick: this.props.viewer />
? (e) => this._handleFollow(e, relation.owner_user_id) )}
: () => this.setState({ visible: true }), </Boundary>
}, ) : null}
]} </div>
/> );
)} return (
</Boundary> <UserEntry
) : null} key={relation.id}
</div> user={relation.owner}
); button={button}
return ( onClick={() => {
<UserEntry this.props.onAction({
key={relation.id} type: "NAVIGATE",
user={relation.owner} value: this.props.sceneId,
button={button} scene: "PROFILE",
onClick={() => { data: relation.owner,
this.props.onAction({ });
type: "NAVIGATE", }}
value: this.props.sceneId, external={this.props.external}
scene: "PROFILE", url={`https://slate.host/${relation.owner.username}`}
data: relation.owner, />
}); );
}} });
external={this.props.external} }
url={`https://slate.host/${relation.owner.username}`} }
/>
);
});
let total = creator.slates.reduce((total, slate) => { let total = creator.slates.reduce((total, slate) => {
return total + slate.data?.objects?.length || 0; return total + slate.data?.objects?.length || 0;
@ -540,14 +486,8 @@ export default class Profile extends React.Component {
onUpdateViewer={this.props.onUpdateViewer} onUpdateViewer={this.props.onUpdateViewer}
resources={this.props.resources} resources={this.props.resources}
viewer={this.props.viewer} viewer={this.props.viewer}
objects={ objects={this.state.publicFiles}
this.state.fileTab === 0 isOwner={false}
? this.props.creator.library[0].children
: this.state.fileTab === 1
? this.state.publicFiles
: this.state.pseudoPrivateFiles
}
isOwner={isOwner}
onAction={this.props.onAction} onAction={this.props.onAction}
mobile={this.props.mobile} mobile={this.props.mobile}
external={this.props.external} external={this.props.external}
@ -624,14 +564,6 @@ export default class Profile extends React.Component {
{this.state.tab === 0 ? ( {this.state.tab === 0 ? (
<div> <div>
<div style={{ display: `flex` }}> <div style={{ display: `flex` }}>
{isOwner && (
<SecondaryTabGroup
tabs={["All files", "Everyone can view", "Link access only"]}
value={this.state.fileTab}
onChange={(value) => this.setState({ fileTab: value })}
style={{ margin: "0 0 24px 0" }}
/>
)}
<SecondaryTabGroup <SecondaryTabGroup
tabs={[ tabs={[
<SVG.GridView height="24px" style={{ display: "block" }} />, <SVG.GridView height="24px" style={{ display: "block" }} />,
@ -642,23 +574,19 @@ export default class Profile extends React.Component {
style={{ margin: "0 0 24px 0", justifyContent: "flex-end" }} style={{ margin: "0 0 24px 0", justifyContent: "flex-end" }}
/> />
</div> </div>
{!!dataItems.length ? ( {this.state.publicFiles.length ? (
<DataView <DataView
onAction={this.props.onAction} onAction={this.props.onAction}
viewer={this.props.viewer} viewer={this.props.viewer}
isOwner={isOwner} isOwner={isOwner}
items={dataItems} items={this.state.publicFiles}
onUpdateViewer={this.props.onUpdateViewer} onUpdateViewer={this.props.onUpdateViewer}
view={this.state.view} view={this.state.view}
/> />
) : ( ) : (
<EmptyState> <EmptyState>
<FileTypeGroup /> <FileTypeGroup />
<div style={{ marginTop: 24 }}> <div style={{ marginTop: 24 }}>This user does not have any public files yet</div>
{isOwner
? `Drag and drop files into Slate to upload`
: `This user does not have any public files yet`}
</div>
</EmptyState> </EmptyState>
)} )}
</div> </div>
@ -710,30 +638,18 @@ export default class Profile extends React.Component {
onChange={(value) => this.setState({ peerTab: value })} onChange={(value) => this.setState({ peerTab: value })}
style={{ margin: "0 0 24px 0" }} style={{ margin: "0 0 24px 0" }}
/> />
{this.state.peerTab === 0 ? ( <div>
<div> {peers?.length ? (
{following?.length ? ( peers
following ) : (
) : ( <EmptyState>
<EmptyState> <SVG.Users height="24px" style={{ marginBottom: 24 }} />
<SVG.Users height="24px" style={{ marginBottom: 24 }} /> {this.state.peerTab === 0
This user is not following anyone yet ? "This user is not following anyone yet"
</EmptyState> : "This user does not have any followers yet"}
)} </EmptyState>
</div> )}
) : null} </div>
{this.state.peerTab === 1 ? (
<div>
{followers?.length ? (
followers
) : (
<EmptyState>
<SVG.Users height="24px" style={{ marginBottom: 24 }} />
This user does not have any followers yet
</EmptyState>
)}
</div>
) : null}
<input <input
readOnly readOnly
ref={(c) => { ref={(c) => {

View File

@ -374,57 +374,58 @@ export class SlateLayout extends React.Component {
}; };
componentDidUpdate = async (prevProps) => { componentDidUpdate = async (prevProps) => {
if (prevProps.slateId !== this.props.slateId) { // if (prevProps.slateId !== this.props.slateId) {
//NOTE(martina): to handle when you navigate between two slates, so it registers the change properly // //NOTE(martina): to handle when you navigate between two slates, so it registers the change properly
await this.setState({ show: false }); // await this.setState({ show: false });
let defaultLayout = this.props.layout ? this.props.defaultLayout : true; // let defaultLayout = this.props.layout ? this.props.defaultLayout : true;
let fileNames = this.props.fileNames; // let fileNames = this.props.fileNames;
let layout; // let layout;
if (this.props.layout) { // if (this.props.layout) {
layout = await this.repairLayout(this.props.items, { // layout = await this.repairLayout(this.props.items, {
defaultLayout, // defaultLayout,
fileNames, // fileNames,
layout: this.props.layout, // layout: this.props.layout,
}); // });
if (layout) { // if (layout) {
this.props.onSaveLayout( // this.props.onSaveLayout(
{ // {
ver: "2.0", // ver: "2.0",
fileNames, // fileNames,
defaultLayout, // defaultLayout,
layout, // layout,
}, // },
true // true
); // );
} else { // } else {
layout = this.props.layout; // layout = this.props.layout;
} // }
} else { // } else {
layout = generateLayout(this.props.items); // layout = generateLayout(this.props.items);
await this.setState({ layout, items: this.props.items }); // await this.setState({ layout, items: this.props.items });
layout = await this.calculateLayout(layout); // layout = await this.calculateLayout(layout);
this.props.onSaveLayout( // this.props.onSaveLayout(
{ // {
ver: "2.0", // ver: "2.0",
fileNames, // fileNames,
defaultLayout, // defaultLayout,
layout, // layout,
}, // },
true // true
); // );
} // }
await this.setState({ // await this.setState({
items: this.props.items, // items: this.props.items,
layout, // layout,
prevLayouts: [], // prevLayouts: [],
zIndexMax: layout && layout.length ? Math.max(...layout.map((pos) => pos.z)) + 1 : 1, // zIndexMax: layout && layout.length ? Math.max(...layout.map((pos) => pos.z)) + 1 : 1,
fileNames, // fileNames,
defaultLayout, // defaultLayout,
editing: false, // editing: false,
show: true, // show: true,
}); // });
this.calculateContainer(); // this.calculateContainer();
} else if (prevProps.items.length !== this.props.items.length) { // }
if (prevProps.items.length !== this.props.items.length) {
//NOTE(martina): to handle when items are added / deleted from the slate, and recalculate the layout //NOTE(martina): to handle when items are added / deleted from the slate, and recalculate the layout
//NOTE(martina): if there is a case that allows simultaneous add / delete (aka modify but same length), this will not work. //NOTE(martina): if there is a case that allows simultaneous add / delete (aka modify but same length), this will not work.
//would need to replace it with event listener + custom events //would need to replace it with event listener + custom events

View File

@ -51,7 +51,7 @@ export default class ProfilePage extends React.Component {
<WebsitePrototypeWrapper title={title} description={description} url={url} image={image}> <WebsitePrototypeWrapper title={title} description={description} url={url} image={image}>
<WebsitePrototypeHeader /> <WebsitePrototypeHeader />
<div css={STYLES_ROOT}> <div css={STYLES_ROOT}>
<Profile {...this.props} buttons={buttons} external /> <Profile {...this.props} buttons={buttons} isOwner={false} external />
</div> </div>
{this.state.visible && ( {this.state.visible && (
<div> <div>

View File

@ -322,6 +322,7 @@ export default class SlatePage extends React.Component {
<SlateLayout <SlateLayout
external external
slateId={this.props.slate.id} slateId={this.props.slate.id}
key={this.props.slate.id}
layout={layouts && layouts.ver === "2.0" ? layouts.layout : null} layout={layouts && layouts.ver === "2.0" ? layouts.layout : null}
onSaveLayout={this._handleSave} onSaveLayout={this._handleSave}
isOwner={false} isOwner={false}

View File

@ -97,6 +97,8 @@ export default class SceneProfile extends React.Component {
creator={ creator={
this.state.profile.id === this.props.viewer.id ? this.props.viewer : this.state.profile this.state.profile.id === this.props.viewer.id ? this.props.viewer : this.state.profile
} }
isOwner={this.state.profile.id === this.props.viewer.id}
key={this.state.profile.id}
/> />
); );
} }

View File

@ -363,6 +363,7 @@ class SlatePage extends React.Component {
) : ( ) : (
<div style={{ marginTop: isOwner ? 24 : 48 }}> <div style={{ marginTop: isOwner ? 24 : 48 }}>
<SlateLayout <SlateLayout
key={this.props.current.id}
current={this.props.current} current={this.props.current}
onUpdateViewer={this.props.onUpdateViewer} onUpdateViewer={this.props.onUpdateViewer}
viewer={this.props.viewer} viewer={this.props.viewer}