Skip to content

Commit 466a28b

Browse files
authored
Live Chat (codesandbox#695)
* Chat * Chat * Fix max height * Only show chat functionality for owner * Chat styling changes * Name change
1 parent 1df3da6 commit 466a28b

File tree

13 files changed

+287
-3
lines changed

13 files changed

+287
-3
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import React from 'react';
2+
import styled from 'styled-components';
3+
import { sortBy, takeRight } from 'lodash';
4+
import { inject, observer } from 'mobx-react';
5+
6+
import AutosizeTextArea from 'common/components/AutosizeTextArea';
7+
8+
const Container = styled.div`
9+
min-height: 200px;
10+
max-height: 300px;
11+
padding: 0 1rem;
12+
color: white;
13+
font-size: 0.875rem;
14+
display: flex;
15+
flex-direction: column;
16+
overflow-y: auto;
17+
`;
18+
19+
const Messages = styled.div`
20+
height: 100%;
21+
flex: 1;
22+
`;
23+
24+
class Chat extends React.Component {
25+
state = {
26+
value: '',
27+
};
28+
29+
handleKeyDown = (e: KeyboardEvent) => {
30+
if (e.keyCode === 13 && !e.shiftKey) {
31+
e.preventDefault();
32+
e.stopPropagation();
33+
// Enter
34+
this.props.signals.live.onSendChat({
35+
message: this.state.value,
36+
});
37+
this.setState({ value: '' });
38+
if (this.messages) {
39+
this.messages.scrollTop = this.messages.scrollHeight;
40+
}
41+
}
42+
};
43+
44+
handleChange = e => {
45+
this.setState({ value: e.target.value });
46+
};
47+
48+
componentDidUpdate() {
49+
if (this.messages) {
50+
this.messages.scrollTop = this.messages.scrollHeight;
51+
}
52+
}
53+
54+
componentDidMount() {
55+
if (this.messages) {
56+
this.messages.scrollTop = this.messages.scrollHeight;
57+
}
58+
}
59+
60+
render() {
61+
const { store } = this.props;
62+
const { messages, users } = store.live.roomInfo.chat;
63+
const currentUserId = store.user.id;
64+
const usersMetadata = store.live.roomInfo.usersMetadata;
65+
66+
return (
67+
<Container
68+
innerRef={el => {
69+
this.messages = el;
70+
}}
71+
>
72+
<Messages>
73+
{messages.length > 0 ? (
74+
sortBy(takeRight(messages, 100), 'date').map((message, i) => {
75+
const metadata = usersMetadata.get(message.userId);
76+
const color = metadata
77+
? `rgb(${metadata.color[0]}, ${metadata.color[1]}, ${
78+
metadata.color[2]
79+
})`
80+
: '#636363';
81+
const name = users.get(message.userId);
82+
return (
83+
<div key={message.date}>
84+
{(i === 0 || messages[i - 1].userId !== message.userId) && (
85+
<div
86+
style={{
87+
color,
88+
fontWeight: 600,
89+
marginBottom: '0.25rem',
90+
marginTop: '0.5rem',
91+
}}
92+
>
93+
{name}
94+
{currentUserId === message.userId && ' (you)'}
95+
{!metadata && ' (left)'}
96+
</div>
97+
)}
98+
<div
99+
style={{
100+
color: 'rgba(255, 255, 255, 0.7)',
101+
fontWeight: 400,
102+
marginBottom: '.25rem',
103+
}}
104+
>
105+
{message.message.split('\n').map(m => (
106+
<span key={m}>
107+
{m}
108+
<br />
109+
</span>
110+
))}
111+
</div>
112+
</div>
113+
);
114+
})
115+
) : (
116+
<div
117+
style={{
118+
fontStyle: 'italic',
119+
color: 'rgba(255, 255, 255, 0.5)',
120+
}}
121+
>
122+
No messages, start sending some!
123+
</div>
124+
)}
125+
</Messages>
126+
<AutosizeTextArea
127+
useCacheForDOMMeasurements
128+
value={this.state.value}
129+
onChange={this.handleChange}
130+
placeholder="Send a message..."
131+
style={{
132+
width: '100%',
133+
minHeight: this.state.height,
134+
marginTop: '0.5rem',
135+
}}
136+
onKeyDown={this.handleKeyDown}
137+
onHeightChange={height => this.setState({ height })}
138+
/>
139+
</Container>
140+
);
141+
}
142+
}
143+
144+
export default inject('signals', 'store')(observer(Chat));

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import NotOwnedSandboxInfo from './items/NotOwnedSandboxInfo';
1616

1717
import ConnectionNotice from './ConnectionNotice';
1818
import Advertisement from './Advertisement';
19+
import WorkspaceItem from './WorkspaceItem';
20+
import Chat from './Chat';
1921
// import DowntimeNotice from './DowntimeNotice';
2022

2123
import { Container, ContactContainer, ItemTitle } from './elements';
@@ -45,9 +47,15 @@ function Workspace({ store }) {
4547
return (
4648
<Container>
4749
{sandbox.owned && <ItemTitle>{item.name}</ItemTitle>}
48-
<div style={{ flex: 1 }}>
50+
<div style={{ flex: 1, overflowY: 'auto' }}>
4951
<Component />
5052
</div>
53+
{store.live.isLive &&
54+
store.live.roomInfo.chatEnabled && (
55+
<WorkspaceItem defaultOpen title="Chat">
56+
<Chat />
57+
</WorkspaceItem>
58+
)}
5159
{!preferences.settings.zenMode && (
5260
<div>
5361
{!store.isPatron && !sandbox.owned && <Advertisement />}

packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ class LiveInfo extends React.Component {
144144
onSessionCloseClicked,
145145
notificationsHidden,
146146
toggleNotificationsHidden,
147+
chatEnabled,
148+
toggleChatEnabled,
147149
} = this.props;
148150

149151
const owner = roomInfo.users.find(u => u.id === ownerId);
@@ -200,6 +202,18 @@ class LiveInfo extends React.Component {
200202
<Margin top={1}>
201203
<SubTitle>Preferences</SubTitle>
202204

205+
{isOwner && (
206+
<PreferencesContainer>
207+
<Preference>Chat enabled</Preference>
208+
<Switch
209+
right={chatEnabled}
210+
onClick={toggleChatEnabled}
211+
small
212+
offMode
213+
secondary
214+
/>
215+
</PreferencesContainer>
216+
)}
203217
<PreferencesContainer>
204218
<Preference>Hide notifications</Preference>
205219
<Switch

packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ const Live = ({ signals, store }) => (
2727
onSessionCloseClicked={signals.live.onSessionCloseClicked}
2828
notificationsHidden={store.live.notificationsHidden}
2929
toggleNotificationsHidden={signals.live.onToggleNotificationsHidden}
30+
chatEnabled={store.live.roomInfo.chatEnabled}
31+
toggleChatEnabled={() => {
32+
signals.live.onChatEnabledChange({
33+
enabled: !store.live.roomInfo.chatEnabled,
34+
});
35+
}}
3036
/>
3137
) : (
3238
<React.Fragment>

packages/app/src/app/store/modules/live/actions.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,14 @@ export function initializeLiveState({ props, state }) {
3939
sandboxId: props.sandboxId,
4040
editorIds: props.editorIds,
4141
mode: props.mode,
42+
chatEnabled: props.chatEnabled,
4243
usersMetadata: {},
4344
users: [],
4445
startTime: Date.now(),
46+
chat: {
47+
messages: [],
48+
users: {},
49+
},
4550
});
4651
state.set('live.isLive', true);
4752
state.set('live.error', null);
@@ -454,3 +459,38 @@ export function removeEditorFromState({ props, state }) {
454459
export function resendOutboundOTTransforms({ ot }) {
455460
ot.serverReconnect();
456461
}
462+
463+
export function receiveChat({ props, state }) {
464+
let name = state.get(`live.roomInfo.chat.users.${props.data.user_id}`);
465+
if (!name) {
466+
const user = state
467+
.get(`live.roomInfo.users`)
468+
.find(u => u.id === props.data.user_id);
469+
470+
if (user) {
471+
state.set(
472+
`live.roomInfo.chat.users.${props.data.user_id}`,
473+
user.username
474+
);
475+
name = user.username;
476+
} else {
477+
name = 'Unknown User';
478+
}
479+
}
480+
481+
state.push('live.roomInfo.chat.messages', {
482+
userId: props.data.user_id,
483+
message: props.data.message,
484+
date: props.data.date,
485+
});
486+
}
487+
488+
export function sendChat({ live, props }) {
489+
live.send('chat', {
490+
message: props.message,
491+
});
492+
}
493+
494+
export function sendChatEnabled({ live, props }) {
495+
live.send('live:chat_enabled', { enabled: props.enabled });
496+
}

packages/app/src/app/store/modules/live/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,7 @@ export default Module({
3737
onRemoveEditorClicked: sequences.removeEditor,
3838
onSessionCloseClicked: sequences.closeSession,
3939
onToggleNotificationsHidden: sequences.toggleNotificationsHidden,
40+
onSendChat: sequences.sendChat,
41+
onChatEnabledChange: sequences.setChatEnabled,
4042
},
4143
});

packages/app/src/app/store/modules/live/model.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export default {
3131
ownerId: types.string,
3232
roomId: types.string,
3333
mode: types.string,
34+
chatEnabled: types.boolean,
3435
sandboxId: types.string,
3536
editorIds: types.array(types.string),
3637
usersMetadata: types.map(UserMetadata),
@@ -41,6 +42,18 @@ export default {
4142
avatarUrl: types.string,
4243
})
4344
),
45+
chat: types.model({
46+
messages: types.array(
47+
types.model({
48+
userId: types.string,
49+
date: types.number,
50+
message: types.string,
51+
})
52+
),
53+
// We keep a separate map if user_id -> username for the case when
54+
// a user disconnects. We still need to keep track of the name.
55+
users: types.map(types.string),
56+
}),
4457
})
4558
),
4659
};

packages/app/src/app/store/modules/live/sequences.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,13 @@ export const handleMessage = [
247247
actions.clearUserSelections,
248248
applySelectionsForModule,
249249
],
250+
'live:chat_enabled': [
251+
isOwnMessage,
252+
{
253+
true: [],
254+
false: [set(state`live.roomInfo.chatEnabled`, props`data.enabled`)],
255+
},
256+
],
250257
'live:add-editor': [
251258
isOwnMessage,
252259
{
@@ -295,6 +302,10 @@ export const handleMessage = [
295302
},
296303
resetLive,
297304
],
305+
chat: [actions.receiveChat],
306+
notification: [
307+
factories.addNotification(props`data.message`, props`data.type`),
308+
],
298309
otherwise: [],
299310
},
300311
];
@@ -344,3 +355,16 @@ export const removeEditor = [
344355
actions.removeEditorFromState,
345356
actions.removeEditor,
346357
];
358+
359+
export const sendChat = [actions.sendChat];
360+
361+
export const setChatEnabled = [
362+
equals(state`live.isOwner`),
363+
{
364+
true: [
365+
set(state`live.roomInfo.chatEnabled`, props`enabled`),
366+
actions.sendChatEnabled,
367+
],
368+
false: [],
369+
},
370+
];
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import styled from 'styled-components';
2+
import AutosizeInput from 'react-input-autosize';
3+
import { styles } from '../Input';
4+
5+
export default styled(AutosizeInput)`
6+
input {
7+
${styles};
8+
}
9+
`;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import AutosizeInput from 'react-textarea-autosize';
2+
import Input from '../Input';
3+
4+
export default Input.withComponent(AutosizeInput);

0 commit comments

Comments
 (0)