Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Private Sketch feat. #3034

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
4 changes: 4 additions & 0 deletions client/common/icons.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import CircleInfo from '../images/circle-info.svg';
import Add from '../images/add.svg';
import Filter from '../images/filter.svg';
import Cross from '../images/cross.svg';
import Lock from '../images/lock.svg';
import UnLock from '../images/unlock.svg';

// HOC that adds the right web accessibility props
// https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html
Expand Down Expand Up @@ -102,3 +104,5 @@ export const CircleFolderIcon = withLabel(CircleFolder);
export const CircleInfoIcon = withLabel(CircleInfo);
export const AddIcon = withLabel(Add);
export const FilterIcon = withLabel(Filter);
export const LockIcon = withLabel(Lock);
export const UnlockIcon = withLabel(UnLock);
1 change: 1 addition & 0 deletions client/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const DELETE_COLLECTION = 'DELETE_COLLECTION';
export const ADD_TO_COLLECTION = 'ADD_TO_COLLECTION';
export const REMOVE_FROM_COLLECTION = 'REMOVE_FROM_COLLECTION';
export const EDIT_COLLECTION = 'EDIT_COLLECTION';
export const CHANGE_VISIBILITY = 'CHANGE_VISIBILITY';

export const DELETE_PROJECT = 'DELETE_PROJECT';

Expand Down
2 changes: 2 additions & 0 deletions client/images/lock.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/images/lockgif.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions client/images/unlock.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions client/modules/IDE/actions/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -410,3 +410,26 @@ export function deleteProject(id) {
});
};
}

export function changeVisibility(projectId, visibility) {
return (dispatch) =>
apiClient
.patch('/project/visibility', { projectId, visibility })
.then((response) => {
const { visibility: newVisibility } = response.data;

dispatch({
type: ActionTypes.CHANGE_VISIBILITY,
payload: { visibility: response.data.visibility }
});

dispatch(setToastText(`Sketch is ${newVisibility}`));
dispatch(showToast(2000));
})
.catch((error) => {
dispatch({
type: ActionTypes.ERROR,
error: error?.response?.data
});
});
}
31 changes: 30 additions & 1 deletion client/modules/IDE/components/Header/Toolbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,36 @@ import PlayIcon from '../../../../images/play.svg';
import StopIcon from '../../../../images/stop.svg';
import PreferencesIcon from '../../../../images/preferences.svg';
import ProjectName from './ProjectName';
import { changeVisibility } from '../../actions/project';
import IconButton from '../../../../common/IconButton';
import { LockIcon, UnlockIcon } from '../../../../common/icons';

const Toolbar = (props) => {
const { isPlaying, infiniteLoop, preferencesIsVisible } = useSelector(
(state) => state.ide
);
const project = useSelector((state) => state.project);
const user = useSelector((state) => state.user);
const autorefresh = useSelector((state) => state.preferences.autorefresh);
const dispatch = useDispatch();

const { t } = useTranslation();
const [visibility, setVisibility] = React.useState(
project.visibility || 'Public'
);
const userIsOwner = user?.username === project.owner?.username;
const toggleVisibility = () => {
try {
setVisibility((prev) => (prev === 'Public' ? 'Private' : 'Public'));
dispatch(
changeVisibility(
project.id,
visibility === 'Public' ? 'Private' : 'Public'
)
);
} catch (error) {
console.log(error);
}
};

const playButtonClass = classNames({
'toolbar__play-button': true,
Expand Down Expand Up @@ -111,6 +131,15 @@ const Toolbar = (props) => {
}
return null;
})()}
{userIsOwner && project.owner && (
<section>
{visibility !== 'Private' ? (
<IconButton icon={UnlockIcon} onClick={toggleVisibility} />
) : (
<IconButton icon={LockIcon} onClick={toggleVisibility} />
)}
</section>
)}
</div>
<button
className={preferencesButtonClass}
Expand Down
94 changes: 89 additions & 5 deletions client/modules/IDE/components/SketchList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import getConfig from '../../../utils/getConfig';

import ArrowUpIcon from '../../../images/sort-arrow-up.svg';
import ArrowDownIcon from '../../../images/sort-arrow-down.svg';
import Button from '../../../common/Button';
import { LockIcon } from '../../../common/icons';

const ROOT_URL = getConfig('API_URL');

Expand All @@ -35,11 +37,24 @@ class SketchListRowBase extends React.Component {
super(props);
this.state = {
renameOpen: false,
renameValue: props.sketch.name
renameValue: props.sketch.name,
newVisibility: props.sketch.visibility || 'Public',
visibleDialogOpen: false
};
this.renameInput = React.createRef();
}

toggleVisibility = () => {
this.setState((prev) => ({
newVisibility: prev.newVisibility === 'Public' ? 'Private' : 'Public'
}));
this.props.changeVisibility(
this.props.sketch.id,
this.state.newVisibility === 'Public' ? 'Private' : 'Public'
);
this.setState({ visibleDialogOpen: false });
};

openRename = () => {
this.setState(
{
Expand Down Expand Up @@ -136,6 +151,12 @@ class SketchListRowBase extends React.Component {
>
{this.props.t('SketchList.DropdownDuplicate')}
</MenuItem>
<MenuItem
hideIf={!userIsOwner}
onClick={() => this.setState({ visibleDialogOpen: true })}
>
Change Visibility
</MenuItem>
<MenuItem
hideIf={!this.props.user.authenticated}
onClick={() => {
Expand Down Expand Up @@ -165,6 +186,14 @@ class SketchListRowBase extends React.Component {
if (username === 'p5') {
url = `/${username}/sketches/${slugify(sketch.name, '_')}`;
}
const title = (
<p>
Make {this.props.sketch.name}{' '}
<span className="sketch-visibility__title">
{this.state.newVisibility === 'Private' ? 'Public' : 'Private'}
</span>
</p>
);

const name = (
<React.Fragment>
Expand All @@ -179,6 +208,56 @@ class SketchListRowBase extends React.Component {
ref={this.renameInput}
/>
)}

{this.state.visibleDialogOpen && (
<Overlay
title={title}
closeOverlay={() => this.setState({ visibleDialogOpen: false })}
>
{/* TODO: these <li> should come from translate.json */}
<div className="sketch-visibility">
vivekbopaliya marked this conversation as resolved.
Show resolved Hide resolved
{this.state.newVisibility === 'Public' ? (
<ul className="sketch-visibility_ul">
<li>
Your sketch will stay private and will not be seen by
others.
</li>
<li>
Others will not be able to copy, change, or even see your
sketch.
</li>
<li>This keeps your work safe and private.</li>

<li>
you can focus on being creative without worrying about
others seeing your work.
</li>
<li>
You can always come back and adjust who can see your sketch.
</li>
</ul>
) : (
<ul className="sketch-visibility_ul">
<li>Your sketch will be visible to everyone.</li>
<li>Others can copy, edit, or just check out your sketch.</li>

<li>
This helps everyone share ideas and be more creative
together.
</li>
<li>
You can always change who can see your sketch whenever you
want.
</li>
</ul>
)}

<Button onClick={this.toggleVisibility}>
I have read and understand these effects
</Button>
</div>
</Overlay>
)}
</React.Fragment>
);

Expand All @@ -189,7 +268,9 @@ class SketchListRowBase extends React.Component {
key={sketch.id}
onClick={this.handleRowClick}
>
<th scope="row">{name}</th>
<th scope="row" className="sketches-table_name">
{this.state.newVisibility === 'Private' && <LockIcon />} {name}
</th>
<td>{formatDateCell(sketch.createdAt, mobile)}</td>
<td>{formatDateCell(sketch.updatedAt, mobile)}</td>
{this.renderDropdown()}
Expand All @@ -204,7 +285,8 @@ SketchListRowBase.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired,
updatedAt: PropTypes.string.isRequired
updatedAt: PropTypes.string.isRequired,
visibility: PropTypes.string
}).isRequired,
username: PropTypes.string.isRequired,
user: PropTypes.shape({
Expand All @@ -216,6 +298,7 @@ SketchListRowBase.propTypes = {
cloneProject: PropTypes.func.isRequired,
changeProjectName: PropTypes.func.isRequired,
onAddToCollection: PropTypes.func.isRequired,
changeVisibility: PropTypes.func.isRequired,
mobile: PropTypes.bool,
t: PropTypes.func.isRequired
};
Expand All @@ -241,7 +324,6 @@ class SketchList extends React.Component {
super(props);
this.props.getProjects(this.props.username);
this.props.resetSorting();

this.state = {
isInitialDataLoad: true
};
Expand Down Expand Up @@ -354,6 +436,7 @@ class SketchList extends React.Component {
};

render() {
// const userIsOwner = this.props.user.username === this.props.username;
const username =
this.props.username !== undefined
? this.props.username
Expand Down Expand Up @@ -438,7 +521,8 @@ SketchList.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired,
updatedAt: PropTypes.string.isRequired
updatedAt: PropTypes.string.isRequired,
visibility: PropTypes.string
})
).isRequired,
username: PropTypes.string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,14 @@ exports[`<Sketchlist /> snapshot testing 1`] = `
<tr
class="sketches-table__row"
>
<th
<th
scope="row"
class="sketches-table_name"
>
<a
href="/happydog/sketches/testid1"
>
<LockIcon />
testsketch1
</a>
</th>
Expand Down
12 changes: 9 additions & 3 deletions client/modules/IDE/pages/IDEView.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
import { useLocation, Prompt, useParams } from 'react-router-dom';
import { useLocation, Prompt, useParams, useHistory } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Helmet } from 'react-helmet';
Expand All @@ -11,7 +11,6 @@ import PreviewFrame from '../components/PreviewFrame';
import Console from '../components/Console';
import Toast from '../components/Toast';
import { updateFileContent } from '../actions/files';

import {
autosaveProject,
clearPersistedState,
Expand Down Expand Up @@ -103,7 +102,7 @@ const IDEView = () => {
const [sidebarSize, setSidebarSize] = useState(160);
const [isOverlayVisible, setIsOverlayVisible] = useState(false);
const [MaxSize, setMaxSize] = useState(window.innerWidth);

const history = useHistory();
const cmRef = useRef({});

const autosaveIntervalRef = useRef(null);
Expand All @@ -124,6 +123,13 @@ const IDEView = () => {
}
}, [dispatch, params, project.id]);

useEffect(() => {
if (!isUserOwner && project.visibility === 'Private') {
// TODO: we might want to have a 'Sorry, this sketch is private' page for this
history.push('/');
}
}, [isUserOwner, project.visibility, history]);

const autosaveAllowed = isUserOwner && project.id && preferences.autosave;
const shouldAutosave = autosaveAllowed && ide.unsavedChanges;

Expand Down
9 changes: 6 additions & 3 deletions client/modules/IDE/reducers/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ const initialState = () => {
return {
name: generatedName,
updatedAt: '',
isSaving: false
isSaving: false,
visibility: 'Public'
};
};

Expand All @@ -25,15 +26,17 @@ const project = (state, action) => {
name: action.project.name,
updatedAt: action.project.updatedAt,
owner: action.owner,
isSaving: false
isSaving: false,
visibility: action.project.visibility
};
case ActionTypes.SET_PROJECT:
return {
id: action.project.id,
name: action.project.name,
updatedAt: action.project.updatedAt,
owner: action.owner,
isSaving: false
isSaving: false,
visibility: action.project.visibility
};
case ActionTypes.RESET_PROJECT:
return initialState();
Expand Down
5 changes: 5 additions & 0 deletions client/modules/IDE/reducers/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ const sketches = (state = [], action) => {
return action.projects;
case ActionTypes.DELETE_PROJECT:
return state.filter((sketch) => sketch.id !== action.id);
case ActionTypes.CHANGE_VISIBILITY:
return state.map((sketch) => ({
...sketch,
visibility: action.payload.visibility
}));
case ActionTypes.RENAME_PROJECT: {
return state.map((sketch) => {
if (sketch.id === action.payload.id) {
Expand Down
Loading
Loading