mirror of
https://github.com/filecoin-project/slate.git
synced 2024-12-24 17:44:50 +03:00
Merge pull request #845 from filecoin-project/update/file-privacy
Update: File privacy
This commit is contained in:
commit
d8a51ab42b
@ -404,14 +404,6 @@ export const updateFile = async (data) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const toggleFilePrivacy = async (data) => {
|
||||
await Websockets.checkWebsocket();
|
||||
return await returnJSON(`/api/data/toggle-privacy`, {
|
||||
...DEFAULT_OPTIONS,
|
||||
body: JSON.stringify({ data }),
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteFiles = async (data) => {
|
||||
await Websockets.checkWebsocket();
|
||||
return await returnJSON(`/api/data/delete`, {
|
||||
|
@ -518,51 +518,6 @@ class CarouselSidebar extends React.Component {
|
||||
this.props.onNext();
|
||||
};
|
||||
|
||||
_handleToggleVisibility = async (e) => {
|
||||
if (this.props.external || !this.props.isOwner || !this.props.viewer) return;
|
||||
const isPublic = this.props.file.isPublic;
|
||||
const slateIsPublic = this.props.data?.isPublic;
|
||||
let selected = cloneDeep(this.state.selected);
|
||||
|
||||
const slateIds = Object.entries(this.state.selected)
|
||||
.filter((entry) => entry[1])
|
||||
.map((entry) => entry[0]);
|
||||
const publicSlateIds = [];
|
||||
const publicSlateNames = [];
|
||||
for (let slate of this.props.viewer.slates) {
|
||||
if (slate.isPublic && slateIds.includes(slate.id)) {
|
||||
publicSlateNames.push(slate.data.name);
|
||||
publicSlateIds.push(slate.id);
|
||||
selected[slate.id] = false;
|
||||
}
|
||||
}
|
||||
if (publicSlateNames.length) {
|
||||
const slateNames = publicSlateNames.join(", ");
|
||||
const message = `Making this file link-viewing only will remove it from the following public collections: ${slateNames}. Do you wish to continue?`;
|
||||
if (!window.confirm(message)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.carouselType === "SLATE" && slateIsPublic) {
|
||||
const slateId = this.props.data.id;
|
||||
let slates = cloneDeep(this.props.viewer.slates);
|
||||
for (let slate of slates) {
|
||||
if (slate.id === slateId) {
|
||||
slate.objects = slate.objects.filter((obj) => obj.id !== this.props.file.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.props.onAction({ type: "UPDATE_VIEWER", viewer: { slates } });
|
||||
}
|
||||
|
||||
let response = await Actions.toggleFilePrivacy({ ...this.props.file, isPublic: !isPublic });
|
||||
Events.hasError(response);
|
||||
if (isPublic) {
|
||||
this.setState({ selected });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const isPublic = this.props.file.isPublic;
|
||||
const file = this.props.file;
|
||||
@ -822,74 +777,6 @@ class CarouselSidebar extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
let privacy;
|
||||
if (editingAllowed) {
|
||||
privacy = (
|
||||
<div>
|
||||
<System.P1 css={STYLES_SECTION_HEADER} style={{ marginBottom: 12 }}>
|
||||
Visibility
|
||||
</System.P1>
|
||||
<System.P1
|
||||
css={STYLES_TEXT}
|
||||
style={{
|
||||
marginTop: 12,
|
||||
}}
|
||||
>
|
||||
{isPublic
|
||||
? `This ${
|
||||
isLink ? "link" : "file"
|
||||
} is currently visible to everyone and searchable within Slate. It may appear in activity feeds and explore.`
|
||||
: isLink
|
||||
? "This link is only visible to you"
|
||||
: "This file is only visible to those with the link."}
|
||||
</System.P1>
|
||||
<RadioGroup
|
||||
name="isPublic"
|
||||
options={[
|
||||
{
|
||||
value: true,
|
||||
label: (
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<SVG.Globe height="16px" style={{ marginRight: 8 }} />
|
||||
Public
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: false,
|
||||
label: (
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<SVG.SecurityLock height="16px" style={{ marginRight: 8 }} />
|
||||
Link-viewing only
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
dark={true}
|
||||
style={{ marginTop: 12 }}
|
||||
labelStyle={{ fontFamily: Constants.font.medium }}
|
||||
selected={isPublic}
|
||||
onChange={this._handleToggleVisibility}
|
||||
/>
|
||||
{!isPublic && !isLink && (
|
||||
<Input
|
||||
full
|
||||
value={isLink ? file.data.link.url : Strings.getURLfromCID(file.cid)}
|
||||
name="copyLink"
|
||||
readOnly
|
||||
copyable
|
||||
style={{
|
||||
fontSize: Constants.typescale.lvl1,
|
||||
...STYLES_INPUT,
|
||||
marginTop: 12,
|
||||
}}
|
||||
textStyle={{ color: Constants.system.white }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.state.modalShow && (
|
||||
@ -913,7 +800,6 @@ class CarouselSidebar extends React.Component {
|
||||
</div>
|
||||
{elements}
|
||||
<div css={STYLES_ACTIONS}>{actions}</div>
|
||||
{privacy}
|
||||
{uploadCoverImage}
|
||||
{!this.props.external && this.props.viewer && (
|
||||
<>
|
||||
@ -967,20 +853,3 @@ class CarouselSidebar extends React.Component {
|
||||
}
|
||||
|
||||
export default withTheme(CarouselSidebar);
|
||||
|
||||
{
|
||||
/* <>
|
||||
<div css={STYLES_SECTION_HEADER} style={{ margin: "48px 0px 8px 0px" }}>
|
||||
Visibility
|
||||
</div>
|
||||
<div css={STYLES_OPTIONS_SECTION}>
|
||||
<div css={STYLES_TEXT}>{isVisible ? "Everyone" : "Link only"}</div>
|
||||
<Toggle dark active={isVisible} onChange={this._handleToggleVisibility} />
|
||||
</div>
|
||||
<div style={{ color: Constants.system.grayLight2, marginTop: 8 }}>
|
||||
{isVisible
|
||||
? "This file is currently visible to everyone and searchable within Slate. It may appear in activity feeds and explore."
|
||||
: "This file is currently not visible to others unless they have the link."}
|
||||
</div>
|
||||
</> */
|
||||
}
|
||||
|
@ -3,25 +3,16 @@ import * as Constants from "~/common/constants";
|
||||
import * as Strings from "~/common/strings";
|
||||
import * as SVG from "~/common/svg";
|
||||
import * as Actions from "~/common/actions";
|
||||
import * as Utilities from "~/common/utilities";
|
||||
import * as Events from "~/common/custom-events";
|
||||
import * as Window from "~/common/window";
|
||||
import * as Styles from "~/common/styles";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Link } from "~/components/core/Link";
|
||||
import { GlobalCarousel } from "~/components/system/components/GlobalCarousel";
|
||||
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 { SecondaryTabGroup } from "~/components/core/TabGroup";
|
||||
import { LoaderSpinner } from "~/components/system/components/Loaders";
|
||||
|
||||
import ProcessedText from "~/components/core/ProcessedText";
|
||||
import SlatePreviewBlocks from "~/components/core/SlatePreviewBlock";
|
||||
import DataView from "~/components/core/DataView";
|
||||
import EmptyState from "~/components/core/EmptyState";
|
||||
import ProfilePhoto from "~/components/core/ProfilePhoto";
|
||||
import CollectionPreviewBlock from "~/components/core/CollectionPreviewBlock";
|
||||
@ -139,16 +130,6 @@ const STYLES_STAT = css`
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const STYLES_EXPLORE = css`
|
||||
font-size: ${Constants.typescale.lvl1};
|
||||
font-family: ${Constants.font.text};
|
||||
font-weight: 400;
|
||||
margin: 64px auto 64px auto;
|
||||
width: 120px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid ${Constants.system.black};
|
||||
`;
|
||||
|
||||
const STYLES_BUTTON = css`
|
||||
margin-bottom: 32px;
|
||||
|
||||
@ -157,164 +138,12 @@ const STYLES_BUTTON = css`
|
||||
}
|
||||
`;
|
||||
|
||||
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.grayLight2};
|
||||
|
||||
@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.semantic.borderGrayLight};
|
||||
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.blue};
|
||||
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.semantic.bgLight};
|
||||
background-size: cover;
|
||||
background-position: 50% 50%;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-right: 16px;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const STYLES_DIRECTORY_STATUS_INDICATOR = css`
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
border: 1.2px solid ${Constants.system.green};
|
||||
background-color: ${Constants.system.green};
|
||||
`;
|
||||
|
||||
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;
|
||||
`;
|
||||
|
||||
// const STYLES_COPY_INPUT = css`
|
||||
// pointer-events: none;
|
||||
// position: absolute;
|
||||
// opacity: 0;
|
||||
// `;
|
||||
|
||||
function UserEntry({ user, button, onClick, message, checkStatus }) {
|
||||
const isOnline = checkStatus({ id: user.id });
|
||||
|
||||
return (
|
||||
<div key={user.username} css={STYLES_USER_ENTRY}>
|
||||
<div css={STYLES_USER} onClick={onClick}>
|
||||
<div css={STYLES_DIRECTORY_PROFILE_IMAGE}>
|
||||
<ProfilePhoto user={user} size={24} />
|
||||
{isOnline && <div css={STYLES_DIRECTORY_STATUS_INDICATOR} />}
|
||||
</div>
|
||||
<span css={STYLES_DIRECTORY_NAME}>
|
||||
{user.data.name || `@${user.username}`}
|
||||
{message ? <span css={STYLES_MESSAGE}>{message}</span> : null}
|
||||
</span>
|
||||
</div>
|
||||
{button}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FilesPage({
|
||||
library,
|
||||
user,
|
||||
isOwner,
|
||||
isMobile,
|
||||
viewer,
|
||||
onAction,
|
||||
resources,
|
||||
page,
|
||||
tab = "grid",
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
{isMobile ? null : (
|
||||
<SecondaryTabGroup
|
||||
tabs={[
|
||||
{
|
||||
title: <SVG.GridView height="24px" style={{ display: "block" }} />,
|
||||
value: { tab: "grid", subtab: "files" },
|
||||
},
|
||||
{
|
||||
title: <SVG.TableView height="24px" style={{ display: "block" }} />,
|
||||
value: { tab: "table", subtab: "files" },
|
||||
},
|
||||
]}
|
||||
value={tab}
|
||||
onAction={onAction}
|
||||
style={{ margin: "0 0 24px 0" }}
|
||||
/>
|
||||
)}
|
||||
{library.length ? (
|
||||
<DataView
|
||||
key="scene-profile"
|
||||
user={user}
|
||||
onAction={onAction}
|
||||
viewer={viewer}
|
||||
isOwner={isOwner}
|
||||
items={library}
|
||||
view={tab}
|
||||
resources={resources}
|
||||
page={page}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState>
|
||||
<FileTypeGroup />
|
||||
<div style={{ marginTop: 24 }}>This user does not have any public files yet</div>
|
||||
</EmptyState>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CollectionsPage({
|
||||
user,
|
||||
viewer,
|
||||
@ -339,8 +168,8 @@ function CollectionsPage({
|
||||
<div>
|
||||
<SecondaryTabGroup
|
||||
tabs={[
|
||||
{ title: "Collections", value: { tab: "collections", subtab: "collections" } },
|
||||
{ title: "Subscribed", value: { tab: "subscribed", subtab: "collections" } },
|
||||
{ title: "Collections", value: { tab: "collections" } },
|
||||
{ title: "Subscribed", value: { tab: "subscribed" } },
|
||||
]}
|
||||
value={tab}
|
||||
onAction={onAction}
|
||||
@ -377,125 +206,12 @@ function CollectionsPage({
|
||||
);
|
||||
}
|
||||
|
||||
function PeersPage({
|
||||
checkStatus,
|
||||
viewer,
|
||||
following,
|
||||
followers,
|
||||
fetched,
|
||||
tab = "following",
|
||||
onAction,
|
||||
onLoginModal,
|
||||
}) {
|
||||
const [selectedUser, setSelectedUser] = useState(false);
|
||||
const selectUser = (e, id) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!id || selectedUser === id) {
|
||||
setSelectedUser(null);
|
||||
} else {
|
||||
setSelectedUser(id);
|
||||
}
|
||||
};
|
||||
const followUser = async (e, id) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
selectUser(e, null);
|
||||
if (!viewer) {
|
||||
onLoginModal();
|
||||
return;
|
||||
}
|
||||
await Actions.createSubscription({
|
||||
userId: id,
|
||||
});
|
||||
};
|
||||
|
||||
let peers = tab === "following" ? following : followers;
|
||||
peers = peers.map((relation) => {
|
||||
const following = !!(
|
||||
viewer &&
|
||||
viewer.following.some((subscription) => {
|
||||
return subscription.id === relation.id;
|
||||
}).length
|
||||
);
|
||||
let button =
|
||||
!viewer || relation.id !== viewer?.id ? (
|
||||
<div css={STYLES_ITEM_BOX} onClick={(e) => selectUser(e, relation.id)}>
|
||||
<SVG.MoreHorizontal height="24px" />
|
||||
{selectedUser === relation.id ? (
|
||||
<Boundary
|
||||
captureResize={true}
|
||||
captureScroll={false}
|
||||
enabled
|
||||
onOutsideRectEvent={(e) => selectUser(e)}
|
||||
>
|
||||
<PopoverNavigation
|
||||
style={{
|
||||
top: "40px",
|
||||
right: "0px",
|
||||
}}
|
||||
navigation={[
|
||||
[
|
||||
{
|
||||
text: following ? "Unfollow" : "Follow",
|
||||
onClick: (e) => followUser(e, relation.id),
|
||||
},
|
||||
],
|
||||
]}
|
||||
/>
|
||||
</Boundary>
|
||||
) : null}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Link key={relation.id} href={`/$/user/${relation.id}`} onAction={onAction}>
|
||||
<UserEntry key={relation.id} user={relation} button={button} checkStatus={checkStatus} />
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SecondaryTabGroup
|
||||
tabs={[
|
||||
{ title: "Following", value: { tab: "following", subtab: "peers" } },
|
||||
{ title: "Followers", value: { tab: "followers", subtab: "peers" } },
|
||||
]}
|
||||
value={tab}
|
||||
onAction={onAction}
|
||||
style={{ margin: "0 0 24px 0" }}
|
||||
/>
|
||||
<div>
|
||||
{peers?.length ? (
|
||||
peers
|
||||
) : (
|
||||
<EmptyState>
|
||||
{fetched ? (
|
||||
<React.Fragment>
|
||||
<SVG.Users height="24px" style={{ marginBottom: 24 }} />
|
||||
{tab === "following"
|
||||
? `This user is not following anyone yet`
|
||||
: `This user does not have any followers yet`}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<LoaderSpinner style={{ height: 24, width: 24 }} />
|
||||
)}
|
||||
</EmptyState>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default class Profile extends React.Component {
|
||||
_ref = null;
|
||||
|
||||
state = {
|
||||
contextMenu: null,
|
||||
subscriptions: [],
|
||||
followers: [],
|
||||
following: [],
|
||||
isFollowing:
|
||||
this.props.external || this.props.user.id === this.props.viewer?.id
|
||||
? false
|
||||
@ -506,10 +222,6 @@ export default class Profile extends React.Component {
|
||||
index: -1,
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
this.fetchSocial();
|
||||
};
|
||||
|
||||
componentDidUpdate = (prevProps) => {
|
||||
if (!this.state.fetched && this.props.page.params !== prevProps.page.params) {
|
||||
this.fetchSocial();
|
||||
@ -518,12 +230,9 @@ export default class Profile extends React.Component {
|
||||
|
||||
fetchSocial = async () => {
|
||||
if (this.state.fetched) return;
|
||||
if (this.props.page.params?.subtab !== "peers" && this.props.page.params?.tab !== "subscribed")
|
||||
return;
|
||||
let following, followers, subscriptions;
|
||||
if (this.props.page.params?.tab !== "subscribed") return;
|
||||
let subscriptions;
|
||||
if (this.props.user.id === this.props.viewer?.id) {
|
||||
following = this.props.viewer?.following;
|
||||
followers = this.props.viewer?.followers;
|
||||
subscriptions = this.props.viewer?.subscriptions;
|
||||
} else {
|
||||
const query = { id: this.props.user.id };
|
||||
@ -531,19 +240,15 @@ export default class Profile extends React.Component {
|
||||
if (Events.hasError(response)) {
|
||||
return;
|
||||
}
|
||||
following = response.following;
|
||||
followers = response.followers;
|
||||
subscriptions = response.subscriptions;
|
||||
}
|
||||
this.setState({
|
||||
following: following,
|
||||
followers: followers,
|
||||
subscriptions: subscriptions,
|
||||
fetched: true,
|
||||
});
|
||||
};
|
||||
|
||||
_handleHide = (e) => {
|
||||
_handleHide = () => {
|
||||
this.setState({ contextMenu: null });
|
||||
};
|
||||
|
||||
@ -582,33 +287,14 @@ export default class Profile extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
let subtab = this.props.page.params?.subtab
|
||||
? this.props.page.params?.subtab
|
||||
: this.props.page.params?.cid
|
||||
? "files"
|
||||
: "collections";
|
||||
let tab = this.props.page.params?.tab;
|
||||
let library = this.props.user.library;
|
||||
let isOwner = this.props.isOwner;
|
||||
let user = this.props.user;
|
||||
let { user, isOwner } = this.props;
|
||||
let { fileCount } = user;
|
||||
|
||||
const showStatusIndicator = this.props.isAuthenticated;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<GlobalCarousel
|
||||
carouselType="PROFILE"
|
||||
resources={this.props.resources}
|
||||
viewer={this.props.viewer}
|
||||
objects={library}
|
||||
isOwner={this.props.isOwner}
|
||||
onAction={this.props.onAction}
|
||||
isMobile={this.props.isMobile}
|
||||
external={this.props.external}
|
||||
params={this.props.page.params}
|
||||
index={this.state.index}
|
||||
onChange={(index) => this.setState({ index })}
|
||||
/>
|
||||
<div css={STYLES_PROFILE_BACKGROUND}>
|
||||
<div css={STYLES_PROFILE_INFO}>
|
||||
<div css={STYLES_PROFILE_IMAGE}>
|
||||
@ -650,7 +336,7 @@ export default class Profile extends React.Component {
|
||||
<div css={STYLES_STATS}>
|
||||
<div css={STYLES_STAT}>
|
||||
<div style={{ fontFamily: `${Constants.font.text}` }}>
|
||||
{library.length}{" "}
|
||||
{fileCount}{" "}
|
||||
<span style={{ color: `${Constants.system.grayLight2}` }}>Files</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -665,39 +351,12 @@ export default class Profile extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div css={STYLES_PROFILE}>
|
||||
<TabGroup
|
||||
tabs={[
|
||||
{ title: "Files", value: { subtab: "files" } },
|
||||
{ title: "Collections", value: { subtab: "collections" } },
|
||||
{ title: "Peers", value: { subtab: "peers" } },
|
||||
]}
|
||||
value={subtab}
|
||||
onAction={this.props.onAction}
|
||||
style={{ marginTop: 0, marginBottom: 32 }}
|
||||
itemStyle={{ margin: "0px 16px" }}
|
||||
/>
|
||||
{subtab === "files" ? (
|
||||
<FilesPage {...this.props} user={user} library={library} tab={tab} />
|
||||
) : null}
|
||||
{subtab === "collections" ? (
|
||||
<CollectionsPage
|
||||
{...this.props}
|
||||
tab={tab}
|
||||
fetched={this.state.fetched}
|
||||
subscriptions={this.state.subscriptions}
|
||||
/>
|
||||
) : null}
|
||||
{subtab === "peers" ? (
|
||||
<PeersPage
|
||||
{...this.props}
|
||||
tab={tab}
|
||||
onLoginModal={this._handleLoginModal}
|
||||
checkStatus={this.checkStatus}
|
||||
following={this.state.following}
|
||||
followers={this.state.followers}
|
||||
fetched={this.state.fetched}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -210,8 +210,8 @@ export default class SidebarCreateSlate extends React.Component {
|
||||
marginTop: 12,
|
||||
}}
|
||||
>
|
||||
All collections are public by default. This means they can be discovered and seen by
|
||||
anyone on the internet. If you make it private, only you will be able to see it.
|
||||
Public collections can be discovered and seen by anyone on the internet. If you make it
|
||||
private, only you will be able to see it.
|
||||
</System.P1>
|
||||
<RadioGroup
|
||||
name="isPublic"
|
||||
|
@ -25,9 +25,8 @@ import getFilesByUserId from "~/node_common/data/methods/get-files-by-user-id";
|
||||
import deleteFilesByIds from "~/node_common/data/methods/delete-files-by-ids";
|
||||
import deleteFilesByUserId from "~/node_common/data/methods/delete-files-by-user-id";
|
||||
import updateFileById from "~/node_common/data/methods/update-file-by-id";
|
||||
import updateFilePrivacy from "~/node_common/data/methods/update-file-privacy";
|
||||
import updateFilesPublic from "~/node_common/data/methods/update-files-public";
|
||||
import incrementFileSavecount from "~/node_common/data/methods/increment-file-savecount";
|
||||
import recalcFilePrivacy from "~/node_common/data/methods/recalc-file-privacy";
|
||||
|
||||
// NOTE(martina):
|
||||
// Like postgres queries
|
||||
@ -121,9 +120,8 @@ export {
|
||||
deleteFilesByIds,
|
||||
deleteFilesByUserId,
|
||||
updateFileById,
|
||||
updateFilePrivacy,
|
||||
updateFilesPublic,
|
||||
incrementFileSavecount,
|
||||
recalcFilePrivacy,
|
||||
// NOTE(martina): Like postgres queries
|
||||
createLike,
|
||||
deleteLikeByFile,
|
||||
|
42
node_common/data/methods/recalc-file-privacy.js
Normal file
42
node_common/data/methods/recalc-file-privacy.js
Normal file
@ -0,0 +1,42 @@
|
||||
import { runQuery } from "~/node_common/data/utilities";
|
||||
|
||||
export default async ({ fileId }) => {
|
||||
return await runQuery({
|
||||
label: "RECALC_FILE_PRIVACY",
|
||||
queryFn: async (DB) => {
|
||||
const slateIds = `(SELECT ?? FROM ?? WHERE ?? = ?)`;
|
||||
const slateIdsFields = ["slateId", "slate_files", "fileId", fileId];
|
||||
|
||||
const filePrivacy = `(SELECT EXISTS (SELECT * FROM ?? JOIN ?? ON ?? = ?? WHERE ?? = ?))`;
|
||||
const filePrivacyFields = [
|
||||
"slates",
|
||||
"slate_ids",
|
||||
"slates.id",
|
||||
"slate_ids.slateId",
|
||||
"isPublic",
|
||||
true,
|
||||
];
|
||||
|
||||
const update = `UPDATE ?? SET ?? = ${filePrivacy} WHERE ?? = ?`;
|
||||
const updateFields = ["files", "isPublic", ...filePrivacyFields, "id", fileId];
|
||||
|
||||
const updatedFile = await DB.raw(`WITH ?? AS ${slateIds} ${update} RETURNING *`, [
|
||||
"slate_ids",
|
||||
...slateIdsFields,
|
||||
...updateFields,
|
||||
]);
|
||||
let rows = updatedFile.rows;
|
||||
if (rows?.length) {
|
||||
return rows.first();
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
errorFn: async (e) => {
|
||||
return {
|
||||
error: true,
|
||||
decorator: "RECALC_FILE_PRIVACY",
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
@ -11,8 +11,6 @@ export const sanitizeUser = (entity) => {
|
||||
username: entity.username,
|
||||
slates: entity.slates, //NOTE(martina): this is not in the database. It is added after
|
||||
library: entity.library, //NOTE(martina): this is not in the database. It is added after
|
||||
twitterId: entity.twitterId,
|
||||
email: entity.email,
|
||||
data: {
|
||||
name: entity.data?.name,
|
||||
photo: entity.data?.photo,
|
||||
|
@ -6,6 +6,8 @@ import * as Social from "~/node_common/social";
|
||||
import * as Logging from "~/common/logging";
|
||||
import * as ArrayUtilities from "~/node_common/array-utilities";
|
||||
import * as Monitor from "~/node_common/monitor";
|
||||
import * as Arrays from "~/common/arrays";
|
||||
import * as SearchManager from "~/node_common/managers/search";
|
||||
|
||||
import crypto from "crypto";
|
||||
import JWT from "jsonwebtoken";
|
||||
@ -285,3 +287,35 @@ export const addToSlate = async ({ slate, files, user, saveCopy = false }) => {
|
||||
|
||||
return { added: response.length };
|
||||
};
|
||||
|
||||
export const removeFromPublicCollectionUpdatePrivacy = async ({ files }) => {
|
||||
let targetFiles = Arrays.filterPublic(files);
|
||||
let madePrivate = [];
|
||||
for (let file of targetFiles) {
|
||||
let updatedFile = await Data.recalcFilePrivacy({ fileId: file.id });
|
||||
if (!updatedFile) continue;
|
||||
if (file.isPublic && !updatedFile.isPublic) {
|
||||
madePrivate.push(updatedFile);
|
||||
}
|
||||
}
|
||||
if (madePrivate.length) {
|
||||
SearchManager.updateFile(madePrivate, "REMOVE");
|
||||
}
|
||||
return madePrivate;
|
||||
};
|
||||
|
||||
export const addToPublicCollectionUpdatePrivacy = async ({ files }) => {
|
||||
let targetFiles = Arrays.filterPrivate(files);
|
||||
let madePublic = [];
|
||||
for (let file of targetFiles) {
|
||||
let updatedFile = await Data.recalcFilePrivacy({ fileId: file.id });
|
||||
if (!updatedFile) continue;
|
||||
if (!file.isPublic && updatedFile.isPublic) {
|
||||
madePublic.push(updatedFile);
|
||||
}
|
||||
}
|
||||
if (madePublic.length) {
|
||||
SearchManager.updateFile(madePublic, "ADD");
|
||||
}
|
||||
return madePublic;
|
||||
};
|
||||
|
@ -108,6 +108,7 @@ export default async (req, res) => {
|
||||
}
|
||||
|
||||
if (slate?.isPublic) {
|
||||
Utilities.addToPublicCollectionUpdatePrivacy({ files: filesToAddToSlate });
|
||||
SearchManager.updateFile(createdFiles, "ADD");
|
||||
}
|
||||
ViewerManager.hydratePartial(id, { library: true, slates: slate ? true : false });
|
||||
|
@ -35,20 +35,7 @@ export default async (req, res) => {
|
||||
SearchManager.updateSlate(slate, "REMOVE");
|
||||
|
||||
if (slate.isPublic) {
|
||||
//NOTE(martina): if any of the files in it are now private (because they are no longer in any public slates) remove them from search
|
||||
const files = slate.objects;
|
||||
|
||||
const publicFiles = await Data.getFilesByIds({
|
||||
ids: files.map((file) => file.id),
|
||||
publicOnly: true,
|
||||
});
|
||||
const publicIds = publicFiles.map((file) => file.id);
|
||||
|
||||
let privateFiles = files.filter((file) => !publicIds.includes(file.id));
|
||||
|
||||
if (privateFiles.length) {
|
||||
SearchManager.updateFile(privateFiles, "REMOVE");
|
||||
}
|
||||
Utilities.removeFromPublicCollectionUpdatePrivacy({ files: slate.objects });
|
||||
}
|
||||
|
||||
return res.status(200).send({ decorator: "SERVER_DELETE_SLATE", error: false });
|
||||
|
@ -20,7 +20,7 @@ export default async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const slate = await Data.getSlateById({ id: req.body.data.slateId });
|
||||
const slate = await Data.getSlateById({ id: req.body.data.slateId, includeFiles: true });
|
||||
|
||||
if (!slate) {
|
||||
return res.status(404).send({
|
||||
@ -46,18 +46,7 @@ export default async (req, res) => {
|
||||
}
|
||||
|
||||
if (slate.isPublic) {
|
||||
const publicFiles = await Data.getFilesByIds({ ids: fileIds, publicOnly: true });
|
||||
const publicIds = publicFiles.map((file) => file.id);
|
||||
|
||||
let privateFiles = fileIds
|
||||
.filter((id) => !publicIds.includes(id))
|
||||
.map((id) => {
|
||||
return { id };
|
||||
});
|
||||
|
||||
if (privateFiles.length) {
|
||||
SearchManager.updateFile(privateFiles, "REMOVE");
|
||||
}
|
||||
Utilities.removeFromPublicCollectionUpdatePrivacy({ files: slate.objects });
|
||||
}
|
||||
|
||||
ViewerManager.hydratePartial(id, { slates: true });
|
||||
|
@ -48,28 +48,6 @@ export default async (req, res) => {
|
||||
.status(500)
|
||||
.send({ decorator: "SERVER_UPDATE_SLATE_UPDATE_PRIVACY_FAILED", error: true });
|
||||
}
|
||||
|
||||
if (!updates.isPublic) {
|
||||
//NOTE(martina): if any of the files in it are now private (because they are no longer in any public slates) remove them from search
|
||||
const files = slate.objects;
|
||||
|
||||
const publicFiles = await Data.getFilesByIds({
|
||||
ids: files.map((file) => file.id),
|
||||
publicOnly: true,
|
||||
});
|
||||
const publicIds = publicFiles.map((file) => file.id);
|
||||
|
||||
let privateFiles = files.filter((file) => !publicIds.includes(file.id));
|
||||
|
||||
if (privateFiles.length) {
|
||||
SearchManager.updateFile(privateFiles, "REMOVE");
|
||||
}
|
||||
} else {
|
||||
//NOTE(martina): make sure all the now-public files are in search if they weren't already
|
||||
const files = slate.objects;
|
||||
|
||||
SearchManager.updateFile(files, "ADD");
|
||||
}
|
||||
}
|
||||
|
||||
if (updates.data.name && updates.data.name !== slate.data.name) {
|
||||
@ -111,8 +89,12 @@ export default async (req, res) => {
|
||||
|
||||
if (slate.isPublic && !updates.isPublic) {
|
||||
SearchManager.updateSlate(response, "REMOVE");
|
||||
|
||||
Utilities.removeFromPublicCollectionUpdatePrivacy({ files: slate.objects });
|
||||
} else if (!slate.isPublic && updates.isPublic) {
|
||||
SearchManager.updateSlate(response, "ADD");
|
||||
|
||||
Utilities.addToPublicCollectionUpdatePrivacy({ files: slate.objects });
|
||||
} else {
|
||||
SearchManager.updateSlate(response, "EDIT");
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import * as Data from "~/node_common/data";
|
||||
import * as Serializers from "~/node_common/serializers";
|
||||
|
||||
export default async (req, res) => {
|
||||
let id = req.body.data.id;
|
||||
let { id } = req.body.data;
|
||||
if (!id) {
|
||||
return res.status(404).send({ decorator: "SERVER_USER_SOCIAL_NO_USER_ID", error: true });
|
||||
}
|
||||
@ -21,38 +20,8 @@ export default async (req, res) => {
|
||||
.send({ decorator: "SERVER_USER_SOCIAL_SUBSCRIPTIONS_NOT_FOUND", error: true });
|
||||
}
|
||||
|
||||
const following = await Data.getFollowingByUserId({ ownerId: id });
|
||||
|
||||
if (!following) {
|
||||
return res
|
||||
.status(404)
|
||||
.send({ decorator: "SERVER_USER_SOCIAL_FOLLOWING_NOT_FOUND", error: true });
|
||||
}
|
||||
|
||||
if (following.error) {
|
||||
return res
|
||||
.status(500)
|
||||
.send({ decorator: "SERVER_USER_SOCIAL_FOLLOWING_NOT_FOUND", error: true });
|
||||
}
|
||||
|
||||
const followers = await Data.getFollowersByUserId({ userId: id });
|
||||
|
||||
if (!followers) {
|
||||
return res
|
||||
.status(404)
|
||||
.send({ decorator: "SERVER_USER_SOCIAL_FOLLOWERS_NOT_FOUND", error: true });
|
||||
}
|
||||
|
||||
if (followers.error) {
|
||||
return res
|
||||
.status(500)
|
||||
.send({ decorator: "SERVER_USER_SOCIAL_FOLLOWERS_NOT_FOUND", error: true });
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
decorator: "SERVER_USER_SOCIAL",
|
||||
following,
|
||||
followers,
|
||||
subscriptions,
|
||||
});
|
||||
};
|
||||
|
@ -59,28 +59,6 @@ export default async (req, res) => {
|
||||
if (privacyResponse.error) {
|
||||
return res.status(500).send({ decorator: "UPDATE_COLLECTION_PRIVACY_FAILED", error: true });
|
||||
}
|
||||
|
||||
if (!updates.isPublic) {
|
||||
//NOTE(martina): if any of the files in it are now private (because they are no longer in any public slates) remove them from search
|
||||
const files = slate.objects;
|
||||
|
||||
const publicFiles = await Data.getFilesByIds({
|
||||
ids: files.map((file) => file.id),
|
||||
publicOnly: true,
|
||||
});
|
||||
const publicIds = publicFiles.map((file) => file.id);
|
||||
|
||||
let privateFiles = files.filter((file) => !publicIds.includes(file.id));
|
||||
|
||||
if (privateFiles.length) {
|
||||
SearchManager.updateFile(privateFiles, "REMOVE");
|
||||
}
|
||||
} else {
|
||||
//NOTE(martina): make sure all the now-public files are in search if they weren't already
|
||||
const files = slate.objects;
|
||||
|
||||
SearchManager.updateFile(files, "ADD");
|
||||
}
|
||||
}
|
||||
|
||||
if (updates.data.name && updates.data.name !== slate.data.name) {
|
||||
@ -118,8 +96,10 @@ export default async (req, res) => {
|
||||
|
||||
if (slate.isPublic && !updates.isPublic) {
|
||||
SearchManager.updateSlate(updatedSlate, "REMOVE");
|
||||
Utilities.removeFromPublicCollectionUpdatePrivacy({ files: slate.objects });
|
||||
} else if (!slate.isPublic && updates.isPublic) {
|
||||
SearchManager.updateSlate(updatedSlate, "ADD");
|
||||
Utilities.addToPublicCollectionUpdatePrivacy({ files: slate.objects });
|
||||
} else {
|
||||
SearchManager.updateSlate(updatedSlate, "EDIT");
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ export default async (req, res) => {
|
||||
//NOTE(martina): cleans the input to remove fields they should not be changing like ownerId, createdAt, filename, size, type etc.
|
||||
let updates = {
|
||||
id: req.body.data.id,
|
||||
isPublic: req.body.data.isPublic,
|
||||
data: {
|
||||
name: req.body.data.data?.name,
|
||||
body: req.body.data.data?.body,
|
||||
@ -34,24 +33,6 @@ export default async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof updates.isPublic !== "undefined" && updates.isPublic !== file.isPublic) {
|
||||
let response = await Data.updateFilePrivacy({
|
||||
ownerId: file.ownerId,
|
||||
id: updates.id,
|
||||
isPublic: updates.isPublic,
|
||||
});
|
||||
|
||||
if (!response || response.error) {
|
||||
return res.status(500).send({ decorator: "UPDATE_FILE_PRIVACY_FAILED", error: true });
|
||||
}
|
||||
|
||||
if (response.isPublic) {
|
||||
SearchManager.updateFile(response, "ADD");
|
||||
} else {
|
||||
SearchManager.updateFile(response, "REMOVE");
|
||||
}
|
||||
}
|
||||
|
||||
let response = await Data.updateFileById(updates);
|
||||
|
||||
if (!response || response.error) {
|
||||
|
Loading…
Reference in New Issue
Block a user