slate/components/core/Profile.js

627 lines
19 KiB
JavaScript
Raw Normal View History

import * as React from "react";
import * as Constants from "~/common/constants";
2020-09-27 23:11:04 +03:00
import * as Strings from "~/common/strings";
2021-01-08 03:48:57 +03:00
import * as SVG from "~/common/svg";
2021-01-12 23:30:12 +03:00
import * as Actions from "~/common/actions";
2021-01-21 00:50:29 +03:00
import * as Utilities from "~/common/utilities";
import { GlobalCarousel } from "~/components/system/components/GlobalCarousel";
2020-11-30 08:24:22 +03:00
import { css } from "@emotion/react";
import { ButtonPrimary, ButtonSecondary } from "~/components/system/components/Buttons";
import { TabGroup, SecondaryTabGroup } from "~/components/core/TabGroup";
import { Boundary } from "~/components/system/components/fragments/Boundary";
import { PopoverNavigation } from "~/components/system/components/PopoverNavigation";
import { FileTypeGroup } from "~/components/core/FileTypeIcon";
import ProcessedText from "~/components/core/ProcessedText";
2020-10-02 02:44:22 +03:00
import SlatePreviewBlocks from "~/components/core/SlatePreviewBlock";
2020-11-13 01:27:50 +03:00
import CTATransition from "~/components/core/CTATransition";
2021-01-08 03:48:57 +03:00
import DataView from "~/components/core/DataView";
import EmptyState from "~/components/core/EmptyState";
2021-01-06 02:09:59 +03:00
const STYLES_PROFILE_BACKGROUND = css`
background-color: ${Constants.system.white};
width: 100%;
padding: 104px 56px 24px 56px;
@media (max-width: ${Constants.sizes.mobile}px) {
padding: 80px 24px 16px 24px;
}
`;
2020-11-01 21:40:03 +03:00
const STYLES_PROFILE = css`
width: 100%;
2021-01-15 00:34:41 +03:00
padding: 0px 56px 80px 56px;
2020-11-01 21:40:03 +03:00
overflow-wrap: break-word;
white-space: pre-wrap;
@media (max-width: ${Constants.sizes.mobile}px) {
2021-01-15 00:34:41 +03:00
padding: 0px 24px 16px 24px;
2020-11-01 21:40:03 +03:00
}
`;
const STYLES_PROFILE_INFO = css`
line-height: 1.3;
2020-11-05 00:44:28 +03:00
width: 50%;
2021-01-06 02:09:59 +03:00
max-width: 800px;
2020-11-01 21:40:03 +03:00
overflow-wrap: break-word;
white-space: pre-wrap;
2021-01-06 02:09:59 +03:00
margin: 0 auto;
2020-11-05 21:31:48 +03:00
@media (max-width: ${Constants.sizes.tablet}px) {
2020-11-05 00:44:28 +03:00
width: 100%;
2021-01-06 02:09:59 +03:00
max-width: 100%;
2020-11-05 00:44:28 +03:00
}
2020-11-01 21:40:03 +03:00
`;
2021-01-06 02:09:59 +03:00
const STYLES_INFO = css`
2020-11-01 21:40:03 +03:00
display: block;
width: 100%;
2021-01-06 02:09:59 +03:00
text-align: center;
2020-11-01 21:40:03 +03:00
margin-bottom: 48px;
overflow-wrap: break-word;
white-space: pre-wrap;
`;
const STYLES_PROFILE_IMAGE = css`
background-color: ${Constants.system.foreground};
background-size: cover;
background-position: 50% 50%;
2021-01-06 02:09:59 +03:00
width: 120px;
height: 120px;
2020-11-01 21:40:03 +03:00
flex-shrink: 0;
2020-09-05 04:40:23 +03:00
border-radius: 4px;
2021-01-06 02:09:59 +03:00
margin: 0 auto;
2020-11-01 21:40:03 +03:00
@media (max-width: ${Constants.sizes.mobile}px) {
width: 64px;
height: 64px;
}
`;
2021-01-06 02:09:59 +03:00
const STYLES_NAME = css`
font-size: ${Constants.typescale.lvl4};
2020-11-17 10:12:35 +03:00
font-family: ${Constants.font.semiBold};
2020-11-01 21:40:03 +03:00
max-width: 100%;
2020-11-17 10:12:35 +03:00
font-weight: 400;
2021-01-06 02:09:59 +03:00
margin: 16px auto;
2020-11-01 21:40:03 +03:00
overflow-wrap: break-word;
white-space: pre-wrap;
2020-11-17 10:12:35 +03:00
color: ${Constants.system.black};
@media (max-width: ${Constants.sizes.mobile}px) {
margin-bottom: 8px;
}
2020-09-03 00:08:32 +03:00
`;
const STYLES_DESCRIPTION = css`
2020-11-17 10:12:35 +03:00
font-size: ${Constants.typescale.lvl0};
color: ${Constants.system.darkGray};
2021-01-06 02:09:59 +03:00
max-width: 100%;
2020-11-01 21:40:03 +03:00
overflow-wrap: break-word;
white-space: pre-wrap;
@media (max-width: ${Constants.sizes.mobile}px) {
margin-top: 24px;
}
`;
const STYLES_STATS = css`
font-size: ${Constants.typescale.lvl0};
2021-01-06 02:09:59 +03:00
margin: 16px auto;
2020-11-01 21:40:03 +03:00
display: flex;
2021-01-06 02:09:59 +03:00
justify-content: center;
2020-11-17 10:12:35 +03:00
color: ${Constants.system.grayBlack};
2020-11-01 21:40:03 +03:00
`;
const STYLES_STAT = css`
margin: 0px 12px;
${"" /* width: 112px; */}
2020-11-01 21:40:03 +03:00
flex-shrink: 0;
`;
2020-11-17 10:12:35 +03:00
const STYLES_EXPLORE = css`
margin: 64px auto 64px auto;
2020-11-17 10:12:35 +03:00
height: 1px;
width: 80px;
background-color: ${Constants.system.gray};
`;
2021-01-06 02:09:59 +03:00
const STYLES_BUTTON = css`
margin-bottom: 32px;
@media (max-width: ${Constants.sizes.mobile}px) {
margin-bottom: 16px;
}
`;
2021-01-11 23:21:11 +03:00
const STYLES_ITEM_BOX = css`
position: relative;
justify-self: end;
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
margin-right: 16px;
color: ${Constants.system.darkGray};
@media (max-width: ${Constants.sizes.mobile}px) {
margin-right: 8px;
}
`;
const STYLES_USER_ENTRY = css`
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
font-size: ${Constants.typescale.lvl1};
cursor: pointer;
${"" /* border: 1px solid ${Constants.system.lightBorder}; */}
border-radius: 4px;
margin-bottom: 8px;
background-color: ${Constants.system.white};
`;
const STYLES_USER = css`
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
margin: 16px;
color: ${Constants.system.brand};
font-family: ${Constants.font.medium};
font-size: ${Constants.typescale.lvl1};
@media (max-width: ${Constants.sizes.mobile}px) {
margin: 12px 16px;
}
`;
const STYLES_DIRECTORY_PROFILE_IMAGE = css`
background-color: ${Constants.system.foreground};
background-size: cover;
background-position: 50% 50%;
height: 24px;
width: 24px;
margin-right: 16px;
border-radius: 4px;
`;
const STYLES_MESSAGE = css`
color: ${Constants.system.black};
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@media (max-width: 1000px) {
display: none;
}
`;
const STYLES_DIRECTORY_NAME = css`
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`;
2021-01-15 22:00:33 +03:00
const STYLES_COPY_INPUT = css`
pointer-events: none;
position: absolute;
opacity: 0;
`;
function UserEntry({ user, button, onClick, message, external, url }) {
2021-01-11 23:21:11 +03:00
return (
<div key={user.username} css={STYLES_USER_ENTRY}>
{external ? (
<a css={STYLES_USER} style={{ textDecoration: "none" }} href={url}>
<div
css={STYLES_DIRECTORY_PROFILE_IMAGE}
style={{ backgroundImage: `url(${user.data.photo})` }}
/>
<span css={STYLES_DIRECTORY_NAME}>
{user.data.name || `@${user.username}`}
{message ? <span css={STYLES_MESSAGE}>{message}</span> : null}
</span>
</a>
) : (
<div css={STYLES_USER} onClick={onClick}>
<div
css={STYLES_DIRECTORY_PROFILE_IMAGE}
style={{ backgroundImage: `url(${user.data.photo})` }}
/>
<span css={STYLES_DIRECTORY_NAME}>
{user.data.name || `@${user.username}`}
{message ? <span css={STYLES_MESSAGE}>{message}</span> : null}
</span>
</div>
)}
2021-01-19 00:21:39 +03:00
{external ? null : button}
2021-01-11 23:21:11 +03:00
</div>
);
}
export default class Profile extends React.Component {
2021-01-15 22:00:33 +03:00
_ref = null;
2021-01-11 23:21:11 +03:00
2020-11-13 01:27:50 +03:00
state = {
2021-01-08 03:48:57 +03:00
tab: 1,
view: 0,
slateTab: 0,
peerTab: 0,
2021-01-11 23:21:11 +03:00
copyValue: "",
contextMenu: null,
2021-01-15 00:34:41 +03:00
publicFiles: [],
isFollowing: this.props.external
? false
: !!this.props.viewer.subscriptions.filter((entry) => {
2021-01-19 00:21:39 +03:00
return entry.target_user_id === this.props.creator.id;
}).length,
2020-11-13 01:27:50 +03:00
};
2021-01-21 00:50:29 +03:00
componentDidMount = () => {
this.filterByVisibility();
};
2021-01-21 00:50:29 +03:00
filterByVisibility = () => {
2021-01-15 00:34:41 +03:00
let publicFiles = [];
2021-01-21 00:50:29 +03:00
if (this.props.isOwner) {
const res = Utilities.getPublicAndPrivateFiles({ viewer: this.props.creator });
publicFiles = res.publicFiles;
} else {
publicFiles = this.props.creator.library[0].children;
2021-01-15 00:34:41 +03:00
}
2021-01-21 00:50:29 +03:00
this.setState({ publicFiles: publicFiles });
};
_handleCopy = (e, value) => {
e.stopPropagation();
this.setState({ copyValue: value }, () => {
this._ref.select();
document.execCommand("copy");
this._handleHide();
});
};
_handleHide = (e) => {
this.setState({ contextMenu: null });
};
_handleClick = (e, value) => {
e.stopPropagation();
if (this.state.contextMenu === value) {
this._handleHide();
} else {
this.setState({ contextMenu: value });
}
};
_handleFollow = async (e, id) => {
this._handleHide();
e.stopPropagation();
await Actions.createSubscription({
userId: id,
});
2021-01-15 00:34:41 +03:00
};
render() {
2021-01-21 00:50:29 +03:00
let isOwner = this.props.isOwner;
2021-01-19 00:21:39 +03:00
let creator = this.props.creator;
2021-01-21 00:50:29 +03:00
let username = this.state.slateTab === 0 ? creator.username : null;
let subscriptions = this.props.creator.subscriptions || [];
let subscribers = this.props.creator.subscribers || [];
let exploreSlates = this.props.exploreSlates;
2021-01-21 00:50:29 +03:00
let slates = [];
if (this.state.tab === 1) {
if (this.state.slateTab === 0) {
slates = isOwner
? creator.slates.filter((slate) => slate.data.public === true)
: creator.slates;
} else {
slates = subscriptions
.filter((relation) => {
return !!relation.target_slate_id;
})
.map((relation) => relation.slate);
}
}
2021-01-21 00:50:29 +03:00
let peers = [];
if (this.state.tab === 2) {
if (this.state.peerTab === 0) {
peers = subscriptions
.filter((relation) => {
return !!relation.target_user_id;
})
.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 ? (
<Boundary
captureResize={true}
captureScroll={false}
enabled
onOutsideRectEvent={(e) => this._handleClick(e, relation.id)}
>
2021-01-21 04:07:40 +03:00
<PopoverNavigation
style={{
top: "40px",
right: "0px",
}}
navigation={[
{
text: this.props.viewer?.subscriptions.filter((subscription) => {
return subscription.target_user_id === relation.target_user_id;
}).length
? "Unfollow"
: "Follow",
onClick: this.props.viewer
? (e) => this._handleFollow(e, relation.target_user_id)
: () => this.setState({ visible: true }),
},
]}
/>
2021-01-21 00:50:29 +03:00
</Boundary>
) : null}
</div>
);
return (
<UserEntry
key={relation.id}
user={relation.user}
button={button}
onClick={() => {
this.props.onAction({
type: "NAVIGATE",
value: this.props.sceneId,
scene: "PROFILE",
data: relation.user,
});
}}
external={this.props.external}
url={`/${relation.user.username}`}
/>
);
});
} else {
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 ? (
<Boundary
captureResize={true}
captureScroll={false}
enabled
onOutsideRectEvent={(e) => this._handleClick(e, relation.id)}
>
2021-01-21 04:07:40 +03:00
<PopoverNavigation
style={{
top: "40px",
right: "0px",
}}
navigation={[
{
text: this.props.viewer?.subscriptions.filter((subscription) => {
return subscription.target_user_id === relation.owner_user_id;
}).length
? "Unfollow"
: "Follow",
onClick: this.props.viewer
? (e) => this._handleFollow(e, relation.owner_user_id)
: () => this.setState({ visible: true }),
},
]}
/>
2021-01-21 00:50:29 +03:00
</Boundary>
) : null}
</div>
);
return (
<UserEntry
key={relation.id}
user={relation.owner}
button={button}
onClick={() => {
this.props.onAction({
type: "NAVIGATE",
value: this.props.sceneId,
scene: "PROFILE",
data: relation.owner,
});
}}
external={this.props.external}
url={`https://slate.host/${relation.owner.username}`}
/>
);
});
}
}
2020-11-01 21:40:03 +03:00
2021-01-19 00:21:39 +03:00
let total = creator.slates.reduce((total, slate) => {
return total + slate.data?.objects?.length || 0;
}, 0);
return (
2020-11-01 21:40:03 +03:00
<div>
<GlobalCarousel
2021-01-19 00:21:39 +03:00
carouselType="DATA"
onUpdateViewer={this.props.onUpdateViewer}
resources={this.props.resources}
viewer={this.props.viewer}
2021-01-21 00:50:29 +03:00
objects={this.state.publicFiles}
isOwner={false}
onAction={this.props.onAction}
mobile={this.props.mobile}
external={this.props.external}
/>
2021-01-06 02:09:59 +03:00
<div css={STYLES_PROFILE_BACKGROUND}>
<div css={STYLES_PROFILE_INFO}>
<div
css={STYLES_PROFILE_IMAGE}
2021-01-19 00:21:39 +03:00
style={{ backgroundImage: `url('${creator.data.photo}')` }}
2021-01-06 02:09:59 +03:00
/>
<div css={STYLES_INFO}>
2021-01-19 00:21:39 +03:00
<div css={STYLES_NAME}>{Strings.getPresentationName(creator)}</div>
{!isOwner && !this.props.external && (
<div css={STYLES_BUTTON}>
{this.state.isFollowing ? (
<ButtonSecondary
onClick={(e) => {
this.setState({ isFollowing: false });
this._handleFollow(e, this.props.creator.id);
}}
>
Unfollow
</ButtonSecondary>
) : (
<ButtonPrimary
onClick={(e) => {
this.setState({ isFollowing: true });
this._handleFollow(e, this.props.creator.id);
}}
>
Follow
</ButtonPrimary>
)}
</div>
)}
2021-01-19 00:21:39 +03:00
{creator.data.body ? (
2021-01-06 02:09:59 +03:00
<div css={STYLES_DESCRIPTION}>
2021-01-19 00:21:39 +03:00
<ProcessedText text={creator.data.body} />
2020-11-01 21:40:03 +03:00
</div>
2021-01-06 02:09:59 +03:00
) : null}
<div css={STYLES_STATS}>
<div css={STYLES_STAT}>
<div style={{ fontFamily: `${Constants.font.text}` }}>
{total} <span style={{ color: `${Constants.system.darkGray}` }}>Files</span>
2021-01-06 02:09:59 +03:00
</div>
</div>
<div css={STYLES_STAT}>
<div style={{ fontFamily: `${Constants.font.text}` }}>
2021-01-19 00:21:39 +03:00
{creator.slates.length}{" "}
<span style={{ color: `${Constants.system.darkGray}` }}>Slates</span>
2021-01-06 02:09:59 +03:00
</div>
2020-11-01 21:40:03 +03:00
</div>
</div>
2020-09-03 00:08:32 +03:00
</div>
2020-11-01 21:40:03 +03:00
</div>
2020-12-13 04:16:55 +03:00
</div>
2020-11-13 01:27:50 +03:00
{this.state.visible && (
<div>
<CTATransition
2020-11-17 06:17:56 +03:00
onClose={() => this.setState({ visible: false })}
viewer={this.props.viewer}
open={this.state.visible}
2021-01-19 00:21:39 +03:00
redirectURL={`/_?scene=NAV_PROFILE&user=${creator.username}`}
/>
2020-11-13 01:27:50 +03:00
</div>
)}
2021-01-15 00:34:41 +03:00
<div css={STYLES_PROFILE}>
2021-01-12 23:30:12 +03:00
<TabGroup
tabs={["Files", "Slates", "Peers"]}
value={this.state.tab}
onChange={(value) => this.setState({ tab: value })}
2021-01-15 00:34:41 +03:00
style={{ marginTop: 0, marginBottom: 32 }}
2021-01-12 23:30:12 +03:00
/>
{this.state.tab === 0 ? (
<div>
2021-01-15 00:34:41 +03:00
<div style={{ display: `flex` }}>
<SecondaryTabGroup
tabs={[
<SVG.GridView height="24px" style={{ display: "block" }} />,
<SVG.TableView height="24px" style={{ display: "block" }} />,
]}
value={this.state.view}
onChange={(value) => this.setState({ view: value })}
style={{ margin: "0 0 24px 0", justifyContent: "flex-end" }}
/>
2021-01-15 00:34:41 +03:00
</div>
2021-01-21 00:50:29 +03:00
{this.state.publicFiles.length ? (
<DataView
onAction={this.props.onAction}
viewer={this.props.viewer}
isOwner={isOwner}
2021-01-21 00:50:29 +03:00
items={this.state.publicFiles}
onUpdateViewer={this.props.onUpdateViewer}
view={this.state.view}
/>
2021-01-12 23:30:12 +03:00
) : (
<EmptyState>
<FileTypeGroup />
2021-01-21 00:50:29 +03:00
<div style={{ marginTop: 24 }}>This user does not have any public files yet</div>
</EmptyState>
2021-01-12 23:30:12 +03:00
)}
</div>
) : null}
{this.state.tab === 1 ? (
<div>
<SecondaryTabGroup
tabs={["Slates", "Following"]}
2021-01-12 23:30:12 +03:00
value={this.state.slateTab}
onChange={(value) => this.setState({ slateTab: value })}
style={{ margin: "0 0 24px 0" }}
/>
{slates?.length ? (
<SlatePreviewBlocks
isOwner={!!isOwner ? isOwner : false}
external={this.props.external}
slates={slates}
username={username}
onAction={this.props.onAction}
/>
) : (
<React.Fragment>
<EmptyState>
<SVG.Slate height="24px" style={{ marginBottom: 24 }} />
{this.state.slateTab === 0
? `This user does not have any public slates yet`
: `This user is not following any slates yet`}
</EmptyState>
{this.props.external ? (
<React.Fragment>
<div css={STYLES_EXPLORE} />
<SlatePreviewBlocks
isOwner={false}
slates={exploreSlates}
username={username}
onAction={this.props.onAction}
/>
</React.Fragment>
) : null}
</React.Fragment>
)}
2021-01-12 23:30:12 +03:00
</div>
) : null}
{this.state.tab === 2 ? (
<div>
<SecondaryTabGroup
tabs={["Following", "Followers"]}
value={this.state.peerTab}
onChange={(value) => this.setState({ peerTab: value })}
style={{ margin: "0 0 24px 0" }}
/>
2021-01-21 00:50:29 +03:00
<div>
{peers?.length ? (
peers
) : (
<EmptyState>
<SVG.Users height="24px" style={{ marginBottom: 24 }} />
{this.state.peerTab === 0
? "This user is not following anyone yet"
: "This user does not have any followers yet"}
</EmptyState>
)}
</div>
2021-01-15 22:00:33 +03:00
<input
readOnly
ref={(c) => {
this._ref = c;
}}
value={this.state.copyValue}
tabIndex="-1"
css={STYLES_COPY_INPUT}
/>
2021-01-12 23:30:12 +03:00
</div>
) : null}
</div>
</div>
);
}
}