Skip to content

Commit 7753555

Browse files
siddharthkpSaraVieira
authored andcommitted
Sidebar: Live (codesandbox#3378)
* Fix width of textarea with count * remove noPadding in favor of css= * fix alignment of editable title+description * add hover+focus states for input elements * remove padding from stats + club author and stats together * deduplicate codesandbox black theme * Add link component * fix foreground color in theme * fix focus state for input * fix focus state for swtich * fix privacy setting for pro users * convert text to link * fix outline for color picker button * fix text + switch component * link avatar * use link for pro link * make description muted * disable underline from workbench.css * sigh use important coz old design uses workbench * handle patrons without subscription * turns out my profile is busted, not subscriptionSince * chipping away slowly * so much progress! * remove clutter * redo live timer * only show actions on hover * use the correct variable for user.subscription * make the logic of live and not live simpler * add owner check for controls * move supporting components down * get data from overmind instead of passing * pull decisions up * handle not logged in state
1 parent c765f2e commit 7753555

File tree

12 files changed

+539
-8
lines changed

12 files changed

+539
-8
lines changed

packages/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@
214214
"subworkers": "^1.0.1",
215215
"svg-react-loader": "^0.4.4",
216216
"tern": "^0.21.0",
217+
"use-interval": "^1.2.1",
217218
"util": "0.11.1",
218219
"vue": "^2.5.2",
219220
"vue-eslint-parser": "^2.0.3",

packages/app/src/app/pages/Sandbox/Editor/Workspace/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { Deployment } from './items/Deployment';
2121
import { FilesItem } from './items/Files';
2222
import { GitHub } from './items/GitHub';
2323
import { Live } from './items/Live';
24+
import { Live as LiveNew } from './screens/Live';
2425
import { More } from './items/More';
2526
import { NotOwnedSandboxInfo } from './items/NotOwnedSandboxInfo';
2627
import { ProjectInfo } from './items/ProjectInfo';
@@ -40,7 +41,7 @@ const workspaceTabs = {
4041
github: GitHub,
4142
deploy: REDESIGNED_SIDEBAR ? DeploymentNew : Deployment,
4243
config: REDESIGNED_SIDEBAR ? ConfigurationFilesNew : ConfigurationFiles,
43-
live: Live,
44+
live: REDESIGNED_SIDEBAR ? LiveNew : Live,
4445
server: Server,
4546
more: More,
4647
};
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
import React from 'react';
2+
import css from '@styled-system/css';
3+
import { sortBy } from 'lodash-es';
4+
import useInterval from 'use-interval';
5+
6+
import {
7+
Element,
8+
Collapsible,
9+
Stack,
10+
Avatar,
11+
Text,
12+
Label,
13+
Input,
14+
List,
15+
ListItem,
16+
Select,
17+
Button,
18+
Switch,
19+
} from '@codesandbox/components';
20+
import Tooltip from '@codesandbox/common/lib/components/Tooltip';
21+
22+
import { useOvermind } from 'app/overmind';
23+
24+
import {
25+
AddIcon,
26+
RemoveIcon,
27+
UnfollowIcon,
28+
LiveIcon,
29+
StopIcon,
30+
OpenIcon,
31+
ClassroomIcon,
32+
FollowIcon,
33+
} from './icons';
34+
35+
export const LiveNow = () => {
36+
const {
37+
actions: {
38+
live: {
39+
onSessionCloseClicked,
40+
onChatEnabledChange,
41+
onToggleNotificationsHidden,
42+
onModeChanged,
43+
},
44+
},
45+
state: {
46+
live: {
47+
notificationsHidden,
48+
isOwner,
49+
roomInfo: {
50+
startTime,
51+
roomId,
52+
chatEnabled,
53+
mode,
54+
users,
55+
ownerIds,
56+
editorIds,
57+
},
58+
},
59+
},
60+
} = useOvermind();
61+
62+
const owners = users.filter(user => ownerIds.includes(user.id));
63+
const editors = sortBy(
64+
users.filter(u => editorIds.includes(u.id) && !ownerIds.includes(u.id)),
65+
'username'
66+
);
67+
const spectators = sortBy(
68+
users.filter(u => !ownerIds.includes(u.id) && !editorIds.includes(u.id)),
69+
'username'
70+
);
71+
72+
return (
73+
<>
74+
<Collapsible title="Live" defaultOpen>
75+
<Element css={css({ paddingX: 2 })}>
76+
<Stack justify="space-between" align="center" marginBottom={2}>
77+
<Text variant="danger">
78+
<Stack align="center" gap={2}>
79+
<LiveIcon />
80+
<span>You&apos;re live!</span>
81+
</Stack>
82+
</Text>
83+
<Timer startTime={startTime} />
84+
</Stack>
85+
86+
<Text variant="muted" size={2} block marginBottom={4}>
87+
Share this link with others to invite them to the session
88+
</Text>
89+
90+
<Input
91+
defaultValue={`https://codesandbox.io/live/${roomId}`}
92+
marginBottom={2}
93+
/>
94+
95+
{isOwner && (
96+
<Button variant="danger" onClick={onSessionCloseClicked}>
97+
<StopIcon css={css({ marginRight: 2 })} />{' '}
98+
<span>Stop Session</span>
99+
</Button>
100+
)}
101+
</Element>
102+
</Collapsible>
103+
104+
<Collapsible title="Preferences" defaultOpen>
105+
<List>
106+
{isOwner && (
107+
<ListItem justify="space-between">
108+
<Label htmlFor="chat_enabled">Chat enabled</Label>
109+
<Switch
110+
id="chat_enabled"
111+
on={chatEnabled}
112+
onChange={() => onChatEnabledChange(!chatEnabled)}
113+
/>
114+
</ListItem>
115+
)}
116+
<ListItem justify="space-between">
117+
<Label htmlFor="show_notifications">Show notifications</Label>
118+
<Switch
119+
id="show_notifications"
120+
on={!notificationsHidden}
121+
onChange={() => onToggleNotificationsHidden()}
122+
/>
123+
</ListItem>
124+
</List>
125+
</Collapsible>
126+
127+
<Collapsible title="Live Mode" defaultOpen>
128+
<Stack direction="vertical" gap={2} css={css({ paddingX: 2 })}>
129+
<Select
130+
icon={mode === 'open' ? OpenIcon : ClassroomIcon}
131+
value={mode}
132+
onChange={event => onModeChanged({ mode: event.target.value })}
133+
disabled={!isOwner}
134+
>
135+
<option value="open">Everyone can edit</option>
136+
<option value="classroom">Classroom mode</option>
137+
</Select>
138+
<Text variant="muted" size={2}>
139+
{mode === 'open'
140+
? ''
141+
: 'In Classroom Mode, you have control over who can edit'}
142+
</Text>
143+
</Stack>
144+
</Collapsible>
145+
146+
<Collapsible title="Editors" defaultOpen>
147+
<Element css={css({ paddingX: 2 })}>
148+
{owners.map(user => (
149+
<User key={user.id} user={user} liveRole="owner" />
150+
))}
151+
{editors.map(user => (
152+
<User key={user.id} user={user} liveRole="editor" />
153+
))}
154+
{mode === 'open' &&
155+
spectators.map(user => (
156+
<User key={user.id} user={user} liveRole="editor" />
157+
))}
158+
</Element>
159+
</Collapsible>
160+
161+
{mode === 'classroom' && (
162+
<Collapsible title="Viewers" defaultOpen>
163+
<Element css={css({ paddingX: 2 })}>
164+
{spectators.map(user => (
165+
<User key={user.id} user={user} liveRole="spectator" />
166+
))}
167+
168+
{spectators.length
169+
? null
170+
: 'No other users in session, invite them! '}
171+
</Element>
172+
</Collapsible>
173+
)}
174+
</>
175+
);
176+
};
177+
178+
const User = ({ user, liveRole }) => {
179+
const {
180+
actions: {
181+
live: { onFollow, onRemoveEditorClicked, onAddEditorClicked },
182+
},
183+
state: {
184+
live: {
185+
liveUserId,
186+
followingUserId,
187+
isOwner,
188+
roomInfo: { mode },
189+
},
190+
},
191+
} = useOvermind();
192+
193+
const loggedInUser = user.id === liveUserId;
194+
195+
// only owners can change editors
196+
// and only in classroom mode, in open mode, everyone is an editor
197+
const canChangeEditors = isOwner && mode === 'classroom';
198+
199+
// you can't follow spectators (nothing to follow)
200+
// and you can't follow yourself
201+
const canFollowUser = !(liveRole === 'spectator' || loggedInUser);
202+
203+
return (
204+
<Stack
205+
justify="space-between"
206+
align="center"
207+
css={{
208+
'.live-actions': { opacity: 0 },
209+
':hover': { '.live-actions': { opacity: 1 } },
210+
}}
211+
>
212+
<Stack gap={2} align="center">
213+
<Avatar user={user} />
214+
<span>
215+
<Text size={2} block>
216+
{user.username}
217+
</Text>
218+
<Text size={2} variant="muted" block>
219+
{liveRole === 'owner' ? 'Owner ' : null}
220+
{loggedInUser ? '(you)' : null}
221+
</Text>
222+
</span>
223+
</Stack>
224+
225+
<Stack align="center" gap={2} className="live-actions">
226+
{canChangeEditors && (
227+
<>
228+
{liveRole === 'spectator' && (
229+
<Tooltip content="Make editor">
230+
<AddIcon
231+
css={{ cursor: 'pointer' }}
232+
onClick={() => onAddEditorClicked({ liveUserId: user.id })}
233+
/>
234+
</Tooltip>
235+
)}
236+
{liveRole === 'editor' && (
237+
<Tooltip content="Make spectator">
238+
<RemoveIcon
239+
css={{ cursor: 'pointer' }}
240+
onClick={() => onRemoveEditorClicked({ liveUserId: user.id })}
241+
/>
242+
</Tooltip>
243+
)}
244+
</>
245+
)}
246+
247+
{canFollowUser && (
248+
<>
249+
{followingUserId === user.id ? (
250+
<Tooltip content="Stop following">
251+
<UnfollowIcon
252+
css={{ cursor: 'pointer' }}
253+
onClick={() => onFollow({ liveUserId: null })}
254+
/>
255+
</Tooltip>
256+
) : (
257+
<Tooltip content="Follow along">
258+
<FollowIcon
259+
css={{ cursor: 'pointer' }}
260+
onClick={() => onFollow({ liveUserId: user.id })}
261+
/>
262+
</Tooltip>
263+
)}
264+
</>
265+
)}
266+
</Stack>
267+
</Stack>
268+
);
269+
};
270+
271+
const Timer = props => {
272+
const [since, setSince] = React.useState(Date.now() - props.startTime);
273+
274+
const pad = number => {
275+
if (`${number}`.length === 1) return `0${number}`;
276+
return `${number}`;
277+
};
278+
279+
useInterval(() => {
280+
setSince(Date.now() - props.startTime);
281+
}, 1000);
282+
283+
const hours = Math.floor(since / 1000 / 60 / 60);
284+
const minutes = Math.floor((since - hours * 1000 * 60 * 60) / 1000 / 60);
285+
const seconds = Math.floor(
286+
(since - hours * 1000 * 60 * 60 - minutes * 1000 * 60) / 1000
287+
);
288+
289+
const text = `${hours > 0 ? pad(hours) + ':' : ''}${pad(minutes)}:${pad(
290+
seconds
291+
)}`;
292+
293+
return <Text variant="danger">{text}</Text>;
294+
};

0 commit comments

Comments
 (0)