Skip to content

Private Sketch feat. #3034

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

Merged
merged 30 commits into from
Aug 9, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
cae1399
backend done, frontend progress
vivekbopaliya Feb 16, 2024
907c057
frontend done
vivekbopaliya Feb 17, 2024
3bf54c4
Merge branch 'develop' of https://github.com/vivekbopaliya/p5.js-web-…
vivekbopaliya Feb 18, 2024
c9da6cc
icons added
vivekbopaliya Feb 18, 2024
ae08e56
UI changes
vivekbopaliya May 4, 2024
02726cb
Merge branch 'develop' of https://github.com/processing/p5.js-web-edi…
vivekbopaliya May 4, 2024
70b8df1
snapshots updated
vivekbopaliya May 29, 2024
dfb7797
small bug fixed
vivekbopaliya Jun 1, 2024
0ba7467
Merge branch 'processing:develop' into feat/privatesketch
vivekbopaliya Jun 1, 2024
64bdb22
Merge branch 'feat/privatesketch' of https://github.com/vivekbopaliya…
vivekbopaliya Jun 1, 2024
89e9716
bug fixed
vivekbopaliya Jun 1, 2024
d2cbc60
fix bugs and added protected route for private sketch
vivekbopaliya Apr 20, 2025
eafb1aa
fixed bug conflict
vivekbopaliya Apr 20, 2025
edc54da
Merge branch 'develop' into feat/privatesketch
raclim May 7, 2025
9b54f58
fix redirect bug
vivekbopaliya Jun 5, 2025
da1c2c8
Merge branch 'develop' into feat/privatesketch
vivekbopaliya Jun 5, 2025
2bb37c5
Merge branch 'develop' into feat/privatesketch
raclim Jun 10, 2025
f5864b1
refactoring code for private sketch feat
vivekbopaliya Jun 24, 2025
374e971
test error fixex
vivekbopaliya Jun 24, 2025
24b3faa
fixed bug
vivekbopaliya Jul 5, 2025
65d3021
merged main
vivekbopaliya Jul 5, 2025
c2cacf7
replaced visibility checkbox with dropdown
vivekbopaliya Jul 19, 2025
ccafec4
test cases fixed
vivekbopaliya Jul 19, 2025
4c5f9c9
test cases snapsho fixed
vivekbopaliya Jul 19, 2025
de05842
test
vivekbopaliya Jul 20, 2025
6901103
test
vivekbopaliya Jul 20, 2025
673457f
lint fixes
vivekbopaliya Jul 20, 2025
5473069
Merge branch 'develop' into feat/privatesketch
vivekbopaliya Jul 20, 2025
205b238
fixed merge conflict
vivek-oppex Jul 31, 2025
a19ae27
Merge branch 'develop' into feat/privatesketch
raclim Aug 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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">
{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