diff --git a/packages/app/src/app/hooks/index.ts b/packages/app/src/app/hooks/index.ts
index 556422fbf84..2e88ceda96b 100644
--- a/packages/app/src/app/hooks/index.ts
+++ b/packages/app/src/app/hooks/index.ts
@@ -1 +1,2 @@
+export { useInterval } from './useInterval';
export { useScript } from './useScript';
diff --git a/packages/app/src/app/hooks/useInterval.ts b/packages/app/src/app/hooks/useInterval.ts
new file mode 100644
index 00000000000..0676f68ec66
--- /dev/null
+++ b/packages/app/src/app/hooks/useInterval.ts
@@ -0,0 +1,18 @@
+// Based on https://overreacted.io/making-setinterval-declarative-with-react-hooks/
+import { useEffect, useRef } from 'react';
+
+const noop = () => undefined;
+export const useInterval = (callback: () => void = noop, delay: number) => {
+ const savedCallback = useRef(null);
+
+ useEffect(() => {
+ savedCallback.current = callback;
+ }, [callback]);
+
+ useEffect(() => {
+ const id =
+ delay !== null ? setInterval(savedCallback.current, delay) : null;
+
+ return () => clearInterval(id);
+ }, [delay]);
+};
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/index.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/index.tsx
index 99987e33db3..4c73c7b35f1 100644
--- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/index.tsx
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/index.tsx
@@ -14,7 +14,7 @@ import ConfigurationFiles from './items/ConfigurationFiles';
import { Deployment } from './items/Deployment';
import Files from './items/Files';
import { GitHub } from './items/GitHub';
-import Live from './items/Live';
+import { Live } from './items/Live';
import { More } from './items/More';
import { NotOwnedSandboxInfo } from './items/NotOwnedSandboxInfo';
import { ProjectInfo } from './items/ProjectInfo';
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/Countdown.js b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/Countdown.js
deleted file mode 100644
index 443c342be07..00000000000
--- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/Countdown.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import React from 'react';
-
-const pad = t => {
- if (`${t}`.length === 1) {
- return `0${t}`;
- }
-
- return `${t}`;
-};
-
-export default class Countdown extends React.PureComponent {
- componentDidMount() {
- this.timer = setTimeout(this.tick, 1000);
- }
-
- tick = () => {
- this.forceUpdate();
-
- this.timer = setTimeout(this.tick, 1000);
- };
-
- componentWillUnmount() {
- clearTimeout(this.timer);
- }
-
- getTimes = () => {
- const delta = Date.now() - this.props.time;
-
- const hours = Math.floor(delta / 1000 / 60 / 60);
- const minutes = Math.floor((delta - hours * 1000 * 60 * 60) / 1000 / 60);
- const seconds = Math.floor(
- (delta - hours * 1000 * 60 * 60 - minutes * 1000 * 60) / 1000
- );
-
- return { hours: pad(hours), minutes: pad(minutes), seconds: pad(seconds) };
- };
-
- render() {
- const { hours, minutes, seconds } = this.getTimes();
-
- return (
-
- {hours > 0 && `${hours}:`}
- {minutes}:{seconds}
-
- );
- }
-}
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/Live.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/Live.tsx
new file mode 100644
index 00000000000..afe4de06482
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/Live.tsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import { observer } from 'mobx-react-lite';
+
+import { useSignals, useStore } from 'app/store';
+
+import {
+ ErrorDescription,
+ WorkspaceInputContainer,
+ WorkspaceSubtitle,
+} from '../../elements';
+
+import { More } from '../More';
+
+import { Description } from './elements';
+import { LiveButton } from './LiveButton';
+import { LiveInfo } from './LiveInfo';
+
+export const Live = observer(() => {
+ const {
+ live: { createLiveClicked },
+ } = useSignals();
+ const {
+ editor: { currentId, currentSandbox, isAllModulesSynced },
+ isLoggedIn,
+ live: { isLive, isLoading },
+ } = useStore();
+
+ const showPlaceHolder = !(isLoggedIn && (isLive || currentSandbox.owned));
+ if (showPlaceHolder) {
+ const message = isLoggedIn ? (
+ <>
+ You need to own this sandbox to open a live session to collaborate with
+ others in real time.{' '}
+ Fork this sandbox to live share it with others!
+ >
+ ) : (
+ `You need to be signed in to open a live session to collaborate with others in real time. Sign in to live share this sandbox!`
+ );
+
+ return ;
+ }
+
+ const hasUnsyncedModules = !isAllModulesSynced;
+ return (
+
+ {isLive ? (
+
+ ) : (
+ <>
+
+ {`Invite others to live edit this sandbox with you. We're doing it live!`}
+
+
+ Create live room
+
+
+ To invite others you need to generate a URL that others can join.
+
+
+ {hasUnsyncedModules && (
+
+ Save all your files before going live
+
+ )}
+
+
+ {
+ createLiveClicked({ sandboxId: currentId });
+ }}
+ />
+
+ >
+ )}
+
+ );
+});
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveButton.js b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveButton.js
deleted file mode 100644
index e769c1a57ba..00000000000
--- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveButton.js
+++ /dev/null
@@ -1,120 +0,0 @@
-import React from 'react';
-import styled, { css } from 'styled-components';
-
-import RecordIcon from 'react-icons/lib/md/fiber-manual-record';
-
-const styles = css`
- display: flex;
- align-items: center;
- justify-content: center;
-
- outline: none;
- border: none;
- padding: 0.5rem;
-
- background-color: #fd2439b8;
-
- width: 100%;
- color: white;
- border-radius: 4px;
- font-weight: 800;
-
- border: 2px solid #fd2439b8;
-`;
-
-const Button = styled.button`
- transition: 0.3s ease all;
- ${styles};
- cursor: pointer;
-
- svg {
- margin-right: 0.25rem;
- }
-
- ${props =>
- props.disable
- ? css`
- pointer-events: none;
- background-color: rgba(0, 0, 0, 0.3);
- border-color: rgba(0, 0, 0, 0.2);
- color: rgba(255, 255, 255, 0.7);
- `
- : css`
- &:hover {
- background-color: #fd2439fa;
- }
- `};
-`;
-
-const LoadingDiv = styled.div`
- ${styles};
-`;
-
-const AnimatedRecordIcon = styled(RecordIcon)`
- transition: 0.3s ease opacity;
-`;
-
-export default class LiveButton extends React.PureComponent {
- state = {
- hovering: false,
- showIcon: true,
- };
-
- timer: ?number;
-
- componentDidUpdate() {
- if (this.state.hovering && !this.timer) {
- this.timer = setInterval(() => {
- this.setState({ showIcon: !this.state.showIcon });
- }, 1000);
- } else if (!this.state.hovering && this.timer) {
- clearInterval(this.timer);
- this.timer = null;
-
- // eslint-disable-next-line
- this.setState({ showIcon: true });
- }
- }
-
- componentWillUnmount() {
- clearInterval(this.timer);
- }
-
- startHovering = () => {
- this.setState({ hovering: true });
- };
-
- stopHovering = () => {
- this.setState({ hovering: false });
- };
-
- render() {
- const {
- onClick,
- isLoading,
- disable,
- showIcon = true,
- message = 'Go Live',
- } = this.props;
-
- if (isLoading) {
- return Creating Session ;
- }
-
- return (
-
- {showIcon && (
-
- )}{' '}
- {message}
-
- );
- }
-}
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveButton/LiveButton.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveButton/LiveButton.tsx
new file mode 100644
index 00000000000..6c153164941
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveButton/LiveButton.tsx
@@ -0,0 +1,47 @@
+import noop from 'lodash/noop';
+import React, { useState } from 'react';
+
+import { useInterval } from 'app/hooks';
+
+import { AnimatedRecordIcon, Button, LoadingDiv } from './elements';
+
+export const LiveButton = ({
+ disable = false,
+ icon = true,
+ isLoading = false,
+ message = 'Go Live',
+ onClick = noop,
+}) => {
+ const [hovering, setHovering] = useState(false);
+ const [showIcon, setShowIcon] = useState(icon);
+
+ useInterval(
+ () => {
+ if (hovering) {
+ setShowIcon(!showIcon);
+ }
+ },
+ hovering ? 1000 : null
+ );
+
+ if (!hovering && !showIcon) {
+ setShowIcon(true);
+ }
+
+ if (isLoading) {
+ return Creating Session ;
+ }
+
+ return (
+ setHovering(true)}
+ onMouseLeave={() => setHovering(false)}
+ onClick={onClick}
+ >
+ {/*
+ // @ts-ignore */}
+ {icon && } {message}
+
+ );
+};
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveButton/elements.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveButton/elements.ts
new file mode 100644
index 00000000000..a4509de93bf
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveButton/elements.ts
@@ -0,0 +1,56 @@
+import styled, { css } from 'styled-components';
+import { HTMLAttributes } from 'react';
+import RecordIcon from 'react-icons/lib/md/fiber-manual-record';
+
+const styles = css`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ outline: none;
+ padding: 0.5rem;
+ background-color: #fd2439b8;
+ width: 100%;
+ color: white;
+ border-radius: 4px;
+ font-weight: 800;
+ border: 2px solid #fd2439b8;
+`;
+
+interface ButtonProps extends HTMLAttributes {
+ disable?: boolean;
+}
+export const Button = styled.button`
+ ${({ disable }) => css`
+ transition: 0.3s ease all;
+ ${styles};
+ cursor: pointer;
+
+ svg {
+ margin-right: 0.25rem;
+ }
+
+ ${disable
+ ? css`
+ pointer-events: none;
+ background-color: rgba(0, 0, 0, 0.3);
+ border-color: rgba(0, 0, 0, 0.2);
+ color: rgba(255, 255, 255, 0.7);
+ `
+ : css`
+ &:hover {
+ background-color: #fd2439fa;
+ }
+ `};
+ `}
+`;
+
+export const LoadingDiv = styled.div`
+ ${styles};
+`;
+
+export const AnimatedRecordIcon = styled(RecordIcon)`
+ ${({ opacity = 1 }) => css`
+ opacity: ${opacity};
+ transition: 0.3s ease opacity;
+ `}
+`;
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveButton/index.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveButton/index.ts
new file mode 100644
index 00000000000..ff524c90831
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveButton/index.ts
@@ -0,0 +1 @@
+export { LiveButton } from './LiveButton';
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo.js b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo.js
deleted file mode 100644
index 12d2d7c0ba1..00000000000
--- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo.js
+++ /dev/null
@@ -1,459 +0,0 @@
-import React from 'react';
-import styled from 'styled-components';
-import { inject, observer } from 'mobx-react';
-import { sortBy } from 'lodash-es';
-
-import RecordIcon from 'react-icons/lib/md/fiber-manual-record';
-import Input from '@codesandbox/common/lib/components/Input';
-import Margin from '@codesandbox/common/lib/components/spacing/Margin';
-import delay from '@codesandbox/common/lib/utils/animation/delay-effect';
-import Switch from '@codesandbox/common/lib/components/Switch';
-
-import Tooltip from '@codesandbox/common/lib/components/Tooltip';
-
-import AddIcon from 'react-icons/lib/md/add';
-import RemoveIcon from 'react-icons/lib/md/remove';
-import FollowIcon from 'react-icons/lib/io/eye';
-import UnFollowIcon from 'react-icons/lib/io/eye-disabled';
-
-import User from './User';
-import Countdown from './Countdown';
-import LiveButton from './LiveButton';
-
-import { Description, WorkspaceInputContainer } from '../../elements';
-
-const Container = styled.div`
- ${delay()};
- color: ${props =>
- props.theme.light ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)'};
- box-sizing: border-box;
-`;
-
-const Title = styled.div`
- color: #fd2439fa;
- font-weight: 800;
- display: flex;
- align-items: center;
- vertical-align: middle;
-
- padding: 0.5rem 1rem;
- padding-top: 0;
-
- svg {
- margin-right: 0.25rem;
- }
-`;
-
-const StyledInput = styled(Input)`
- width: calc(100% - 1.5rem);
- margin: 0 0.75rem;
- font-size: 0.875rem;
-`;
-
-const SubTitle = styled.div`
- text-transform: uppercase;
- font-weight: 700;
- color: rgba(255, 255, 255, 0.5);
-
- padding-left: 1rem;
- font-size: 0.875rem;
-`;
-
-const Users = styled.div`
- padding: 0.25rem 1rem;
- padding-top: 0;
- color: ${props =>
- props.theme.light ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.8)'};
-`;
-
-const ModeSelect = styled.div`
- position: relative;
- margin: 0.5rem 1rem;
-`;
-
-const Mode = styled.button`
- display: block;
- text-align: left;
- transition: 0.3s ease opacity;
- padding: 0.5rem 1rem;
- color: white;
- border-radius: 4px;
- width: 100%;
- font-size: 1rem;
-
- font-weight: 600;
- border: none;
- outline: none;
- background-color: transparent;
- cursor: ${props => (props.onClick ? 'pointer' : 'inherit')};
- color: white;
- opacity: ${props => (props.selected ? 1 : 0.6)};
- margin: 0.25rem 0;
-
- z-index: 3;
-
- ${props =>
- props.onClick &&
- `
- &:hover {
- opacity: 1;
- }`};
-`;
-
-const ModeDetails = styled.div`
- font-size: 0.75rem;
- color: ${props =>
- props.theme.light ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)'};
- margin-top: 0.25rem;
-`;
-
-const ModeSelector = styled.div`
- transition: 0.3s ease transform;
- position: absolute;
- left: 0;
- right: 0;
- top: 0;
- height: 48px;
-
- border: 2px solid rgba(253, 36, 57, 0.6);
- background-color: rgba(253, 36, 57, 0.6);
- border-radius: 4px;
- z-index: -1;
-
- transform: translateY(${props => props.i * 55}px);
-`;
-
-const PreferencesContainer = styled.div`
- margin: 1rem;
- display: flex;
-`;
-
-const Preference = styled.div`
- flex: 1;
- font-weight: 400;
- color: ${props =>
- props.theme.light ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.8)'};
- align-items: center;
- justify-content: center;
- font-size: 0.875rem;
-`;
-
-const IconContainer = styled.div`
- transition: 0.3s ease color;
- color: ${props =>
- props.theme.light ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.8)'};
- cursor: pointer;
-
- &:hover {
- color: white;
- }
-`;
-
-class LiveInfo extends React.Component {
- select = e => {
- e.target.select();
- };
-
- render() {
- const {
- roomInfo,
- isOwner,
- isTeam,
- ownerIds,
- setMode,
- addEditor,
- removeEditor,
- currentUserId,
- reconnecting,
- onSessionCloseClicked,
- notificationsHidden,
- toggleNotificationsHidden,
- chatEnabled,
- toggleChatEnabled,
- setFollowing,
- followingUserId,
- } = this.props;
-
- const owners = roomInfo.users.filter(u => ownerIds.indexOf(u.id) > -1);
-
- const editors = sortBy(
- roomInfo.users.filter(
- u =>
- roomInfo.editorIds.indexOf(u.id) > -1 && ownerIds.indexOf(u.id) === -1
- ),
- 'username'
- );
- const otherUsers = sortBy(
- roomInfo.users.filter(
- u =>
- ownerIds.indexOf(u.id) === -1 &&
- roomInfo.editorIds.indexOf(u.id) === -1
- ),
- 'username'
- );
-
- const liveMessage = (() => {
- if (isTeam) {
- return 'Your team is live!';
- }
-
- if (isOwner) {
- return "You've gone live!";
- }
-
- return 'You are live!';
- })();
-
- return (
-
-
-
- {reconnecting ? (
- 'Reconnecting...'
- ) : (
-
- {liveMessage}
-
- )}
-
-
- {roomInfo.startTime != null && (
-
- )}
-
-
-
- Share this link with others to invite them to the session:
-
-
-
- {isOwner && !isTeam && (
-
-
-
- )}
-
-
- Preferences
-
- {isOwner && (
-
- Chat enabled
-
-
- )}
-
- Hide notifications
-
-
-
-
-
- Live Mode
-
-
- setMode({ mode: 'open' }) : undefined}
- selected={roomInfo.mode === 'open'}
- >
- Open
- Everyone can edit
-
- setMode({ mode: 'classroom' }) : undefined
- }
- selected={roomInfo.mode === 'classroom'}
- >
- Classroom
- Take control over who can edit
-
-
-
-
- {owners && (
-
- Owners
-
- {owners.map(owner => (
-
- {followingUserId === owner.id ? (
-
- setFollowing({ liveUserId: null })}
- />
-
- ) : (
-
-
- setFollowing({ liveUserId: owner.id })
- }
- />
-
- )}
-
- )
- }
- />
- ))}
-
-
- )}
-
- {editors.length > 0 && roomInfo.mode === 'classroom' && (
-
- Editors
-
- {editors.map(user => (
-
- {user.id !== currentUserId && (
-
- {followingUserId === user.id ? (
-
-
- setFollowing({ liveUserId: null })
- }
- />
-
- ) : (
-
-
- setFollowing({ liveUserId: user.id })
- }
- />
-
- )}
-
- )}
- {isOwner && roomInfo.mode === 'classroom' && (
-
-
-
- removeEditor({ liveUserId: user.id })
- }
- />
-
-
- )}
-
- }
- />
- ))}
-
-
- )}
-
-
- Users
-
-
- {otherUsers.length ? (
- otherUsers.map(user => (
-
- {roomInfo.mode !== 'classroom' &&
- user.id !== currentUserId && (
-
- {followingUserId === user.id ? (
-
-
- setFollowing({ liveUserId: null })
- }
- />
-
- ) : (
-
-
- setFollowing({ liveUserId: user.id })
- }
- />
-
- )}
-
- )}
- {isOwner && roomInfo.mode === 'classroom' && (
-
-
- addEditor({ liveUserId: user.id })}
- />
-
-
- )}
-
- }
- />
- ))
- ) : (
-
- No other users in session, invite them!
-
- )}
-
-
-
- );
- }
-}
-
-export default inject('store')(observer(LiveInfo));
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/LiveInfo.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/LiveInfo.tsx
new file mode 100644
index 00000000000..81977bde6d4
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/LiveInfo.tsx
@@ -0,0 +1,86 @@
+import { observer } from 'mobx-react-lite';
+import React from 'react';
+import RecordIcon from 'react-icons/lib/md/fiber-manual-record';
+
+import { useSignals, useStore } from 'app/store';
+
+import { Description, WorkspaceInputContainer } from '../../../elements';
+
+import { LiveButton } from '../LiveButton';
+
+import { ConnectionStatus, Container, StyledInput, Title } from './elements';
+import { LiveMode } from './LiveMode';
+import { Preferences } from './Preferences';
+import { SessionTimer } from './SessionTimer';
+import { Users } from './Users';
+
+export const LiveInfo = observer(() => {
+ const {
+ live: { onSessionCloseClicked },
+ } = useSignals();
+ const {
+ live: {
+ isOwner,
+ isTeam,
+ reconnecting,
+ roomInfo: { roomId, startTime },
+ },
+ } = useStore();
+
+ const liveMessage = (() => {
+ if (isTeam) {
+ return 'Your team is live!';
+ }
+
+ if (isOwner) {
+ return "You've gone live!";
+ }
+
+ return 'You are live!';
+ })();
+
+ return (
+
+
+
+ {reconnecting ? (
+ 'Reconnecting...'
+ ) : (
+ <>
+ {liveMessage}
+ >
+ )}
+
+
+
+ {startTime !== null && }
+
+
+
+
+ Share this link with others to invite them to the session:
+
+
+ e.target.select()}
+ value={`https://codesandbox.io/live/${roomId}`}
+ />
+
+ {isOwner && !isTeam && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ );
+});
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/LiveMode/LiveMode.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/LiveMode/LiveMode.tsx
new file mode 100644
index 00000000000..1473a79c9dc
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/LiveMode/LiveMode.tsx
@@ -0,0 +1,50 @@
+import Margin from '@codesandbox/common/lib/components/spacing/Margin';
+import noop from 'lodash/noop';
+import { observer } from 'mobx-react-lite';
+import React from 'react';
+
+import { useSignals, useStore } from 'app/store';
+
+import { SubTitle } from '../elements';
+
+import { Mode, ModeDetails, ModeSelect, ModeSelector } from './elements';
+
+export const LiveMode = observer(() => {
+ const {
+ live: { onModeChanged },
+ } = useSignals();
+ const {
+ live: {
+ isOwner,
+ roomInfo: { mode },
+ },
+ } = useStore();
+
+ return (
+
+ Live Mode
+
+
+
+
+ onModeChanged({ mode: 'open' }) : noop}
+ selected={mode === 'open'}
+ >
+ Open
+
+ Everyone can edit
+
+
+ onModeChanged({ mode: 'classroom' }) : noop}
+ selected={mode === 'classroom'}
+ >
+ Classroom
+
+ Take control over who can edit
+
+
+
+ );
+});
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/LiveMode/elements.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/LiveMode/elements.ts
new file mode 100644
index 00000000000..f89b618e33b
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/LiveMode/elements.ts
@@ -0,0 +1,66 @@
+import styled, { css } from 'styled-components';
+
+import { Theme } from '../types';
+
+export const Mode = styled.button<{ onClick: () => void; selected: boolean }>`
+ ${({ onClick, selected }) => css`
+ display: block;
+ text-align: left;
+ transition: 0.3s ease opacity;
+ padding: 0.5rem 1rem;
+ color: white;
+ border-radius: 4px;
+ width: 100%;
+ font-size: 1rem;
+
+ font-weight: 600;
+ border: none;
+ outline: none;
+ background-color: transparent;
+ cursor: ${onClick ? 'pointer' : 'inherit'};
+ opacity: ${selected ? 1 : 0.6};
+ margin: 0.25rem 0;
+
+ z-index: 3;
+
+ ${onClick &&
+ css`
+ &:hover {
+ opacity: 1;
+ }
+ `};
+ `}
+`;
+
+export const ModeDetails = styled.div`
+ ${({ theme }: Theme) => css`
+ color: ${theme.light
+ ? css`rgba(0, 0, 0, 0.7)`
+ : css`rgba(255, 255, 255, 0.7)`};
+ font-size: 0.75rem;
+ margin-top: 0.25rem;
+ `}
+`;
+
+export const ModeSelect = styled.div`
+ margin: 0.5rem 1rem;
+ position: relative;
+`;
+
+export const ModeSelector = styled.div<{ i: number }>`
+ ${({ i }) => css`
+ transition: 0.3s ease transform;
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ height: 48px;
+
+ border: 2px solid rgba(253, 36, 57, 0.6);
+ background-color: rgba(253, 36, 57, 0.6);
+ border-radius: 4px;
+ z-index: -1;
+
+ transform: translateY(${i * 55}px);
+ `}
+`;
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/LiveMode/index.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/LiveMode/index.ts
new file mode 100644
index 00000000000..60c33ed3ab6
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/LiveMode/index.ts
@@ -0,0 +1 @@
+export { LiveMode } from './LiveMode';
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Preferences/ChatEnabled.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Preferences/ChatEnabled.tsx
new file mode 100644
index 00000000000..7e82bcdb9be
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Preferences/ChatEnabled.tsx
@@ -0,0 +1,35 @@
+import Switch from '@codesandbox/common/lib/components/Switch';
+import { observer } from 'mobx-react-lite';
+import React from 'react';
+
+import { useSignals, useStore } from 'app/store';
+
+import { Preference, PreferencesContainer } from './elements';
+
+export const ChatEnabled = observer(() => {
+ const {
+ live: { onChatEnabledChange },
+ } = useSignals();
+ const {
+ live: {
+ roomInfo: { chatEnabled },
+ },
+ } = useStore();
+ const toggleChatEnabled = () => {
+ onChatEnabledChange({ enabled: !chatEnabled });
+ };
+
+ return (
+
+ Chat enabled
+
+
+
+ );
+});
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Preferences/HideNotifications.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Preferences/HideNotifications.tsx
new file mode 100644
index 00000000000..782e2cb7d18
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Preferences/HideNotifications.tsx
@@ -0,0 +1,30 @@
+import Switch from '@codesandbox/common/lib/components/Switch';
+import { observer } from 'mobx-react-lite';
+import React from 'react';
+
+import { useSignals, useStore } from 'app/store';
+
+import { Preference, PreferencesContainer } from './elements';
+
+export const HideNotifications = observer(() => {
+ const {
+ live: { onToggleNotificationsHidden },
+ } = useSignals();
+ const {
+ live: { notificationsHidden },
+ } = useStore();
+
+ return (
+
+ Hide notifications
+
+
+
+ );
+});
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Preferences/Preferences.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Preferences/Preferences.tsx
new file mode 100644
index 00000000000..efb8f63278b
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Preferences/Preferences.tsx
@@ -0,0 +1,26 @@
+import Margin from '@codesandbox/common/lib/components/spacing/Margin';
+import { observer } from 'mobx-react-lite';
+import React from 'react';
+
+import { useStore } from 'app/store';
+
+import { SubTitle } from '../elements';
+
+import { ChatEnabled } from './ChatEnabled';
+import { HideNotifications } from './HideNotifications';
+
+export const Preferences = observer(() => {
+ const {
+ live: { isOwner },
+ } = useStore();
+
+ return (
+
+ Preferences
+
+ {isOwner && }
+
+
+
+ );
+});
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Preferences/elements.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Preferences/elements.ts
new file mode 100644
index 00000000000..c2a5b2d8452
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Preferences/elements.ts
@@ -0,0 +1,21 @@
+import styled, { css } from 'styled-components';
+
+import { Theme } from '../types';
+
+export const Preference = styled.div`
+ ${({ theme }: Theme) => css`
+ align-items: center;
+ color: ${theme.light
+ ? css`rgba(0, 0, 0, 0.8)`
+ : css`rgba(255, 255, 255, 0.8)`};
+ flex: 1;
+ font-size: 0.875rem;
+ font-weight: 400;
+ justify-content: center;
+ `}
+`;
+
+export const PreferencesContainer = styled.div`
+ display: flex;
+ margin: 1rem;
+`;
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Preferences/index.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Preferences/index.ts
new file mode 100644
index 00000000000..e7f050692ca
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Preferences/index.ts
@@ -0,0 +1 @@
+export { Preferences } from './Preferences';
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/SessionTimer.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/SessionTimer.tsx
new file mode 100644
index 00000000000..c28ba173da6
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/SessionTimer.tsx
@@ -0,0 +1,31 @@
+import React, { useState } from 'react';
+
+import { useInterval } from 'app/hooks';
+
+export const SessionTimer = ({ startTime }) => {
+ const [elapsed, setElapsed] = useState(`00:00`);
+
+ const pad = (val: number) => (`${val}`.length === 1 ? `0${val}` : `${val}`);
+
+ const getTimes = () => {
+ const delta = Date.now() - startTime;
+
+ const hours = Math.floor(delta / 1000 / 60 / 60);
+ const minutes = Math.floor((delta - hours * 1000 * 60 * 60) / 1000 / 60);
+ const seconds = Math.floor(
+ (delta - hours * 1000 * 60 * 60 - minutes * 1000 * 60) / 1000
+ );
+
+ return { hours: pad(hours), minutes: pad(minutes), seconds: pad(seconds) };
+ };
+
+ const { hours, minutes, seconds } = getTimes();
+
+ useInterval(() => {
+ setElapsed(`${Number(hours) > 0 ? `${hours}:` : ``}${minutes}:${seconds}`);
+ }, 1000);
+
+ // TODO: just return 'elapsed' after this one's fixed
+ // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20544
+ return {elapsed} ;
+};
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Editors/Editors.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Editors/Editors.tsx
new file mode 100644
index 00000000000..55ca36fb7cb
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Editors/Editors.tsx
@@ -0,0 +1,26 @@
+import Margin from '@codesandbox/common/lib/components/spacing/Margin';
+import React from 'react';
+
+import { SubTitle } from '../../elements';
+
+import { Users } from '../elements';
+import { User } from '../User';
+
+import { SideView } from './SideView';
+
+export const Editors = ({ editors }) => (
+
+ Editors
+
+
+ {editors.map(user => (
+ }
+ type="Editor"
+ user={user}
+ />
+ ))}
+
+
+);
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Editors/SideView.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Editors/SideView.tsx
new file mode 100644
index 00000000000..8b11d029922
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Editors/SideView.tsx
@@ -0,0 +1,56 @@
+import Tooltip from '@codesandbox/common/lib/components/Tooltip';
+import { observer } from 'mobx-react-lite';
+import React from 'react';
+import FollowIcon from 'react-icons/lib/io/eye';
+import UnFollowIcon from 'react-icons/lib/io/eye-disabled';
+import RemoveIcon from 'react-icons/lib/md/remove';
+
+import { useSignals, useStore } from 'app/store';
+
+import { IconContainer } from '../elements';
+import { User } from '../types';
+
+type Props = {
+ userId: User['id'];
+};
+export const SideView = observer(({ userId }) => {
+ const {
+ live: { onFollow, onRemoveEditorClicked },
+ } = useSignals();
+ const {
+ live: {
+ followingUserId,
+ isOwner,
+ liveUserId,
+ roomInfo: { mode },
+ },
+ } = useStore();
+
+ return (
+ <>
+ {userId !== liveUserId && (
+
+ {followingUserId === userId ? (
+
+ onFollow({ liveUserId: null })} />
+
+ ) : (
+
+ onFollow({ liveUserId: userId })} />
+
+ )}
+
+ )}
+
+ {isOwner && mode === 'classroom' && (
+
+
+ onRemoveEditorClicked({ liveUserId: userId })}
+ />
+
+
+ )}
+ >
+ );
+});
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Editors/index.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Editors/index.ts
new file mode 100644
index 00000000000..3bdcb3ffc65
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Editors/index.ts
@@ -0,0 +1 @@
+export { Editors } from './Editors';
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/OtherUsers/OtherUsers.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/OtherUsers/OtherUsers.tsx
new file mode 100644
index 00000000000..50ddbcd625f
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/OtherUsers/OtherUsers.tsx
@@ -0,0 +1,30 @@
+import Margin from '@codesandbox/common/lib/components/spacing/Margin';
+import React from 'react';
+
+import { SubTitle } from '../../elements';
+
+import { NoUsers, Users } from '../elements';
+import { User } from '../User';
+
+import { SideView } from './SideView';
+
+export const OtherUsers = ({ otherUsers }) => (
+
+ Users
+
+
+ {otherUsers.length ? (
+ otherUsers.map(user => (
+ }
+ type="Spectator"
+ user={user}
+ />
+ ))
+ ) : (
+ No other users in session, invite them!
+ )}
+
+
+);
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/OtherUsers/SideView.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/OtherUsers/SideView.tsx
new file mode 100644
index 00000000000..b1db610858c
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/OtherUsers/SideView.tsx
@@ -0,0 +1,56 @@
+import Tooltip from '@codesandbox/common/lib/components/Tooltip';
+import { observer } from 'mobx-react-lite';
+import React from 'react';
+import FollowIcon from 'react-icons/lib/io/eye';
+import UnFollowIcon from 'react-icons/lib/io/eye-disabled';
+import AddIcon from 'react-icons/lib/md/add';
+
+import { useSignals, useStore } from 'app/store';
+
+import { IconContainer } from '../elements';
+import { User } from '../types';
+
+type Props = {
+ userId: User['id'];
+};
+export const SideView = observer(({ userId }) => {
+ const {
+ live: { onAddEditorClicked, onFollow },
+ } = useSignals();
+ const {
+ live: {
+ followingUserId,
+ isOwner,
+ liveUserId,
+ roomInfo: { mode },
+ },
+ } = useStore();
+
+ return (
+ <>
+ {mode !== 'classroom' && userId !== liveUserId && (
+
+ {followingUserId === userId ? (
+
+ onFollow({ liveUserId: null })} />
+
+ ) : (
+
+ onFollow({ liveUserId: userId })} />
+
+ )}
+
+ )}
+
+ {isOwner && mode === 'classroom' && (
+
+
+ onAddEditorClicked({ liveUserId: userId })}
+ />
+
+
+ )}
+ >
+ );
+});
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/OtherUsers/index.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/OtherUsers/index.ts
new file mode 100644
index 00000000000..03a50a81fd4
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/OtherUsers/index.ts
@@ -0,0 +1 @@
+export { OtherUsers } from './OtherUsers';
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Owners/Owners.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Owners/Owners.tsx
new file mode 100644
index 00000000000..88c22cdae27
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Owners/Owners.tsx
@@ -0,0 +1,26 @@
+import Margin from '@codesandbox/common/lib/components/spacing/Margin';
+import React from 'react';
+
+import { SubTitle } from '../../elements';
+
+import { Users } from '../elements';
+import { User } from '../User';
+
+import { SideView } from './SideView';
+
+export const Owners = ({ owners }) => (
+
+ Owners
+
+
+ {owners.map(owner => (
+ }
+ type="Owner"
+ user={owner}
+ />
+ ))}
+
+
+);
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Owners/SideView.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Owners/SideView.tsx
new file mode 100644
index 00000000000..14713aa0998
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Owners/SideView.tsx
@@ -0,0 +1,38 @@
+import Tooltip from '@codesandbox/common/lib/components/Tooltip';
+import { observer } from 'mobx-react-lite';
+import React from 'react';
+import FollowIcon from 'react-icons/lib/io/eye';
+import UnFollowIcon from 'react-icons/lib/io/eye-disabled';
+
+import { useSignals, useStore } from 'app/store';
+
+import { IconContainer } from '../elements';
+import { User } from '../types';
+
+type Props = {
+ userId: User['id'];
+};
+export const SideView = observer(({ userId }) => {
+ const {
+ live: { onFollow },
+ } = useSignals();
+ const {
+ live: { followingUserId, liveUserId },
+ } = useStore();
+
+ return (
+ userId !== liveUserId && (
+
+ {followingUserId === userId ? (
+
+ onFollow({ liveUserId: null })} />
+
+ ) : (
+
+ onFollow({ liveUserId: userId })} />
+
+ )}
+
+ )
+ );
+});
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Owners/index.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Owners/index.ts
new file mode 100644
index 00000000000..8f9a448fa66
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Owners/index.ts
@@ -0,0 +1 @@
+export { Owners } from './Owners';
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/User/User.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/User/User.tsx
new file mode 100644
index 00000000000..67879ca9420
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/User/User.tsx
@@ -0,0 +1,50 @@
+import { observer } from 'mobx-react-lite';
+import React, { ReactNode } from 'react';
+
+import { useStore } from 'app/store';
+
+import { User as UserType } from '../types';
+
+import { UserContainer, ProfileImage, UserName, Status } from './elements';
+
+type Props = {
+ sideView?: ReactNode;
+ type: string;
+ user: UserType;
+};
+export const User = observer(({ sideView = null, type, user }) => {
+ const {
+ live: {
+ liveUserId,
+ roomInfo: { users },
+ },
+ } = useStore();
+
+ const metaData = users.find(({ id }) => id === user.id);
+ const [r, g, b] = metaData ? metaData.color : [0, 0, 0];
+ const isCurrentUser = user.id === liveUserId;
+
+ return (
+
+
+
+
+ {user.username}
+
+ {type && (
+
+ {type}
+
+ {isCurrentUser && ' (you)'}
+
+ )}
+
+
+ {sideView}
+
+ );
+});
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/User/elements.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/User/elements.ts
new file mode 100644
index 00000000000..de9ab57fc78
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/User/elements.ts
@@ -0,0 +1,43 @@
+import styled, { css } from 'styled-components';
+import delay from '@codesandbox/common/lib/utils/animation/delay-effect';
+
+export const Status = styled.div`
+ font-size: 0.75rem;
+ color: rgba(255, 255, 255, 0.6);
+`;
+
+export const UserContainer = styled.div<{ isCurrentUser: boolean }>`
+ ${({ isCurrentUser, theme }) => css`
+ ${delay()};
+ display: flex;
+ align-items: center;
+ margin: 0.5rem 0;
+ color: ${theme.light
+ ? css`rgba(0, 0, 0, 0.8)`
+ : css`rgba(255, 255, 255, 0.8)`};
+ ${isCurrentUser &&
+ css`
+ color: white;
+ `};
+
+ &:first-child {
+ margin-top: 0;
+ }
+ `}
+`;
+
+export const ProfileImage = styled.img<{ borderColor: string }>`
+ ${({ borderColor }) => css`
+ width: 26px;
+ height: 26px;
+ border-radius: 2px;
+ border-left: 2px solid ${borderColor};
+
+ margin-right: 0.5rem;
+ `}
+`;
+
+export const UserName = styled.div`
+ font-weight: 600;
+ font-size: 0.875rem;
+`;
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/User/index.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/User/index.ts
new file mode 100644
index 00000000000..403fc945c19
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/User/index.ts
@@ -0,0 +1 @@
+export { User } from './User';
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Users.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Users.tsx
new file mode 100644
index 00000000000..35bba699398
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/Users.tsx
@@ -0,0 +1,39 @@
+import { sortBy } from 'lodash-es';
+import { observer } from 'mobx-react-lite';
+import React from 'react';
+
+import { useStore } from 'app/store';
+
+import { Editors } from './Editors';
+import { OtherUsers } from './OtherUsers';
+import { Owners } from './Owners';
+
+export const Users = observer(() => {
+ const {
+ live: {
+ roomInfo: { editorIds, mode, ownerIds, users },
+ },
+ } = useStore();
+
+ const owners = users.filter(({ id }) => ownerIds.includes(id));
+ const editors = sortBy(
+ users.filter(({ id }) => editorIds.includes(id) && !ownerIds.includes(id)),
+ 'username'
+ );
+ const otherUsers = sortBy(
+ users.filter(({ id }) => !ownerIds.includes(id) && !editorIds.includes(id)),
+ 'username'
+ );
+
+ return (
+ <>
+ {owners && }
+
+ {editors.length > 0 && mode === 'classroom' && (
+
+ )}
+
+
+ >
+ );
+});
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/elements.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/elements.ts
new file mode 100644
index 00000000000..fe99ff38763
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/elements.ts
@@ -0,0 +1,33 @@
+import styled, { css } from 'styled-components';
+
+import { Theme } from '../types';
+
+export const IconContainer = styled.div`
+ ${({ theme }: Theme) => css`
+ color: ${theme.light
+ ? css`rgba(0, 0, 0, 0.8)`
+ : css`rgba(255, 255, 255, 0.8)`};
+ cursor: pointer;
+ transition: 0.3s ease color;
+
+ &:hover {
+ color: white;
+ }
+ `}
+`;
+
+export const NoUsers = styled.div`
+ color: rgba(255, 255, 255, 0.8);
+ font-size: 0.875rem;
+ font-weight: 600;
+ margin-top: 0.25rem;
+`;
+
+export const Users = styled.div`
+ ${({ theme }: Theme) => css`
+ color: ${theme.light
+ ? css`rgba(0, 0, 0, 0.8)`
+ : css`rgba(255, 255, 255, 0.8)`};
+ padding: 0 1rem 0.25rem;
+ `}
+`;
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/index.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/index.ts
new file mode 100644
index 00000000000..a87a5f5b1f1
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/index.ts
@@ -0,0 +1 @@
+export { Users } from './Users';
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/types.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/types.ts
new file mode 100644
index 00000000000..d368afd1870
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/Users/types.ts
@@ -0,0 +1,5 @@
+export type User = {
+ avatarUrl: string;
+ id: number;
+ username: string;
+};
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/elements.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/elements.ts
new file mode 100644
index 00000000000..0e00aab4c4d
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/elements.ts
@@ -0,0 +1,49 @@
+import Input from '@codesandbox/common/lib/components/Input';
+import delay from '@codesandbox/common/lib/utils/animation/delay-effect';
+import styled, { css } from 'styled-components';
+
+import { Theme } from './types';
+
+export const Container = styled.div`
+ ${({ theme }: Theme) => css`
+ ${delay()};
+ color: ${theme.light
+ ? css`rgba(0, 0, 0, 0.7)`
+ : css`rgba(255, 255, 255, 0.7)`};
+ box-sizing: border-box;
+ `}
+`;
+
+export const Title = styled.div`
+ color: #fd2439fa;
+ font-weight: 800;
+ display: flex;
+ align-items: center;
+ vertical-align: middle;
+ padding: 0 1rem 0.5rem;
+
+ svg {
+ margin-right: 0.25rem;
+ }
+`;
+
+export const ConnectionStatus = styled.div`
+ display: flex;
+ flex: 1;
+ align-items: center;
+ font-size: 1rem;
+`;
+
+export const StyledInput = styled(Input)`
+ width: calc(100% - 1.5rem);
+ margin: 0 0.75rem;
+ font-size: 0.875rem;
+`;
+
+export const SubTitle = styled.div`
+ text-transform: uppercase;
+ font-weight: 700;
+ color: rgba(255, 255, 255, 0.5);
+ padding-left: 1rem;
+ font-size: 0.875rem;
+`;
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/index.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/index.ts
new file mode 100644
index 00000000000..4903353342c
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/index.ts
@@ -0,0 +1 @@
+export { LiveInfo } from './LiveInfo';
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/types.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/types.ts
new file mode 100644
index 00000000000..904dc7c650d
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo/types.ts
@@ -0,0 +1,5 @@
+export type Theme = {
+ theme: {
+ light: boolean;
+ };
+};
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/User.js b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/User.js
deleted file mode 100644
index 2e20de7eedb..00000000000
--- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/User.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import React from 'react';
-import styled from 'styled-components';
-import { observer } from 'mobx-react';
-
-import delay from '@codesandbox/common/lib/utils/animation/delay-effect';
-
-const Status = styled.div`
- font-size: 0.75rem;
- color: rgba(255, 255, 255, 0.6);
-`;
-
-const UserContainer = styled.div`
- ${delay()};
- display: flex;
- align-items: center;
- margin: 0.5rem 0;
- color: ${props =>
- props.theme.light ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.8)'};
- ${props =>
- props.isCurrentUser &&
- `
- color: white;
- `};
-
- &:first-child {
- margin-top: 0;
- }
-`;
-
-const ProfileImage = styled.img`
- width: 26px;
- height: 26px;
- border-radius: 2px;
- border-left: 2px solid ${({ borderColor }) => borderColor};
-
- margin-right: 0.5rem;
-`;
-
-const UserName = styled.div`
- font-weight: 600;
- font-size: 0.875rem;
-`;
-
-// eslint-disable-next-line
-class User extends React.Component {
- render() {
- const { user, type, sideView, roomInfo, currentUserId } = this.props;
-
- const metaData = roomInfo.users.find(u => u.id === user.id);
- const [r, g, b] = metaData ? metaData.color : [0, 0, 0];
-
- const isCurrentUser = user.id === currentUserId;
-
- return (
-
-
-
- {user.username}
- {type && (
-
- {type}
- {isCurrentUser && ' (you)'}
-
- )}
-
- {sideView}
-
- );
- }
-}
-
-export default observer(User);
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/elements.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/elements.ts
new file mode 100644
index 00000000000..e1fa9a2ca0a
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/elements.ts
@@ -0,0 +1,7 @@
+import styled from 'styled-components';
+
+import { Description as DescriptionBase } from '../../elements';
+
+export const Description = styled(DescriptionBase)`
+ margin-bottom: 1rem;
+`;
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/index.js b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/index.js
deleted file mode 100644
index 66e46d84502..00000000000
--- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/index.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import React from 'react';
-import { inject, observer } from 'mobx-react';
-
-import LiveInfo from './LiveInfo';
-import LiveButton from './LiveButton';
-
-import {
- Description,
- WorkspaceInputContainer,
- WorkspaceSubtitle,
- ErrorDescription,
-} from '../../elements';
-import { More } from '../More';
-
-const Live = ({ signals, store }) => {
- const hasUnsyncedModules = !store.editor.isAllModulesSynced;
-
- const showPlaceHolder =
- (!store.live.isLive && !store.editor.currentSandbox.owned) ||
- !store.isLoggedIn;
-
- if (showPlaceHolder) {
- const message = store.isLoggedIn ? (
- <>
- You need to own this sandbox to open a live session to collaborate with
- others in real time.{' '}
- Fork this sandbox to live share it with others!
- >
- ) : (
- `You need to be signed in to open a live session to collaborate with others in real time. Sign in to live share this sandbox!`
- );
-
- return ;
- }
-
- return (
-
- {store.live.isLive ? (
- {
- signals.live.onChatEnabledChange({
- enabled: !store.live.roomInfo.chatEnabled,
- });
- }}
- setFollowing={signals.live.onFollow}
- followingUserId={store.live.followingUserId}
- />
- ) : (
-
-
- Invite others to live edit this sandbox with you. We
- {"'"}
- re doing it live!
-
-
-
- Create live room
-
- To invite others you need to generate a URL that others can join.
-
-
- {hasUnsyncedModules && (
-
- Save all your files before going live
-
- )}
-
- {
- signals.live.createLiveClicked({
- sandboxId: store.editor.currentId,
- });
- }}
- isLoading={store.live.isLoading}
- disable={hasUnsyncedModules}
- />
-
-
-
- )}
-
- );
-};
-
-export default inject('signals', 'store')(observer(Live));
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/index.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/index.ts
new file mode 100644
index 00000000000..c5cc09c5182
--- /dev/null
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/index.ts
@@ -0,0 +1 @@
+export { Live } from './Live';