From 783e69dfd3c2a44da22374f1d0976f17c042598c Mon Sep 17 00:00:00 2001 From: Logan Buckley Date: Sat, 6 May 2017 10:59:42 -0400 Subject: [PATCH 1/3] Add chat drafts to conversation state. This allows drafts to be stored per conversation instead of keeping the same draft for all conversations. However, it doesn't handle drafts in game chats the same way, because those conversations are not stored in the same way. --- src/model/AppActions.js | 8 ++++++++ src/model/conversation.js | 15 +++++++++++++++ src/model/types.js | 1 + src/ui/chat/ChatMessageBar.js | 15 +++++++++++++++ src/ui/chat/ChatScreen.js | 10 ++++++++++ src/ui/chat/RoomChat.js | 3 +++ src/ui/chat/UserChat.js | 3 +++ src/ui/game/GameScreen.js | 5 ++++- 8 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/model/AppActions.js b/src/model/AppActions.js index 13f1e02..9579bb8 100644 --- a/src/model/AppActions.js +++ b/src/model/AppActions.js @@ -426,6 +426,14 @@ export class AppActions { this._client.sendMessage({type: 'CHAT', text: body, channelId: conversationId}); } + onDraftChat = (body: string, conversationId: number) => { + this._store.dispatch({ + type: 'DRAFT_CHAT', + conversationId: conversationId, + text: body + }); + } + onSendGameChat = (body: string, gameId: number) => { if (this._isOffline()) { return; diff --git a/src/model/conversation.js b/src/model/conversation.js index c79fb4f..401a5a8 100644 --- a/src/model/conversation.js +++ b/src/model/conversation.js @@ -18,6 +18,7 @@ function createConversation(msg: KgsMessage) { let convo: Conversation = { id: msg.channelId, messages: [], + draft: '', status: isTempId(msg.channelId) ? 'pending' : 'created' }; if (msg.callbackKey) { @@ -170,6 +171,20 @@ export function handleConversationMessage( }; return {...prevState, conversationsById}; } + } else if (msg.type === 'DRAFT_CHAT') { + console.log('Drafting chat'); + console.log(msg); + let convoId = msg.conversationId; + let conversationsById: Index = {...prevState.conversationsById}; + if (conversationsById[convoId]) { + console.log('Found conversation!'); + conversationsById[convoId] = { + ...conversationsById[convoId], + draft: msg.text + }; + return {...prevState, conversationsById}; + } } + return prevState; } diff --git a/src/model/types.js b/src/model/types.js index 0f13e5a..7bdabbe 100644 --- a/src/model/types.js +++ b/src/model/types.js @@ -97,6 +97,7 @@ export type Conversation = { lastSeen?: number, unseenCount?: number, chatsDisabled?: boolean, + draft: string, messages: Array, callbackKey?: ?number, status: 'pending' | 'created' | 'userNotFound' | 'closed' diff --git a/src/ui/chat/ChatMessageBar.js b/src/ui/chat/ChatMessageBar.js index b1de4c6..a815df3 100644 --- a/src/ui/chat/ChatMessageBar.js +++ b/src/ui/chat/ChatMessageBar.js @@ -6,6 +6,7 @@ import type {Conversation} from '../../model'; type Props = { conversation: ?Conversation, onSubmit: string => any; + onDraft: string => any; }; export default class ChatMessageBar extends Component { @@ -22,6 +23,13 @@ export default class ChatMessageBar extends Component { } } + _onChange(event: SyntheticEvent) { + let target: EventTarget = event.target; + if (target instanceof HTMLInputElement) { + this.props.onDraft(target.value); + } + } + render() { let {conversation} = this.props; let placeholder; @@ -30,6 +38,11 @@ export default class ChatMessageBar extends Component { } else { placeholder = 'Type a message...'; } + // FIXME: This draft handling is a workaround for game chat + // storing conversation state differently from user and room chat. + // Ideally they'd work the same way and game chat drafts would be + // saved as well. + let draft = conversation ? conversation.draft : ''; return (
@@ -39,6 +52,8 @@ export default class ChatMessageBar extends Component { className='ChatMessageBar-input' type='text' placeholder={placeholder} + value={draft === '' ? null : draft} + onChange={this._onChange.bind(this)} autoFocus={!isTouchDevice()} />
diff --git a/src/ui/chat/ChatScreen.js b/src/ui/chat/ChatScreen.js index 4d21a7d..58769ab 100644 --- a/src/ui/chat/ChatScreen.js +++ b/src/ui/chat/ChatScreen.js @@ -400,6 +400,7 @@ export default class ChatScreen extends Component { usersByName={usersByName} onUserDetail={actions.onUserDetail} onSendChat={this._onSendChat} + onDraftChat={this._onDraftChat} setMessagesDivRef={this._setMessagesDivRef} setMessageInputRef={this._setMessageInputRef} /> : null} {activeConv && activeRoom ? @@ -420,6 +421,7 @@ export default class ChatScreen extends Component { onJoinGame={actions.onJoinGame} onSelectChallenge={actions.onSelectChallenge} onSendChat={this._onSendChat} + onDraftChat={this._onDraftChat} setMessagesDivRef={this._setMessagesDivRef} setMessageInputRef={this._setMessageInputRef} />) : null}
@@ -456,6 +458,14 @@ export default class ChatScreen extends Component { this.props.actions.onSendChat(body, activeConversationId); } + _onDraftChat = (body: string) => { + let {activeConversationId} = this.state; + if (!activeConversationId) { + return; + } + this.props.actions.onDraftChat(body, activeConversationId); + } + _onShowList = () => { this.setState({showingList: true}, () => { window.scrollTo(0, 0); diff --git a/src/ui/chat/RoomChat.js b/src/ui/chat/RoomChat.js index 2a2696b..7ca49e8 100644 --- a/src/ui/chat/RoomChat.js +++ b/src/ui/chat/RoomChat.js @@ -29,6 +29,7 @@ export default class RoomChat extends Component { onSelectChallenge: number => any, onShowGames: (filter: GameFilter) => any, onSendChat: string => any, + onDraftChat: string => any, setMessagesDivRef: HTMLElement => any, setMessageInputRef: HTMLElement => any }; @@ -41,6 +42,7 @@ export default class RoomChat extends Component { usersByName, games, onUserDetail, + onDraftChat, onSendChat, onJoinGame, onSelectChallenge, @@ -81,6 +83,7 @@ export default class RoomChat extends Component {
{!isMobileScreen() ? diff --git a/src/ui/chat/UserChat.js b/src/ui/chat/UserChat.js index 1cb1096..81a2757 100644 --- a/src/ui/chat/UserChat.js +++ b/src/ui/chat/UserChat.js @@ -20,6 +20,7 @@ export default class UserChat extends Component { usersByName: Index, onUserDetail: string => any, onSendChat: string => any, + onDraftChat: string => any, setMessagesDivRef: HTMLElement => any, setMessageInputRef: HTMLElement => any }; @@ -32,6 +33,7 @@ export default class UserChat extends Component { usersByName, onUserDetail, onSendChat, + onDraftChat, setMessagesDivRef, setMessageInputRef } = this.props; @@ -68,6 +70,7 @@ export default class UserChat extends Component {
diff --git a/src/ui/game/GameScreen.js b/src/ui/game/GameScreen.js index 01ab379..57c0182 100644 --- a/src/ui/game/GameScreen.js +++ b/src/ui/game/GameScreen.js @@ -225,9 +225,12 @@ export default class GameScreen extends Component { id: 0, messages: [], status: 'created', + draft: '', chatsDisabled: !game.tree }} - onSubmit={this._onChat} /> + onSubmit={this._onChat} + onDraft={()=>{}} + /> From ee3dcba6667df3ff822b520528cf069c1426bc92 Mon Sep 17 00:00:00 2001 From: Logan Buckley Date: Sat, 6 May 2017 16:06:46 -0400 Subject: [PATCH 2/3] Make `onDraft` prop to ChatMessageBar optional. --- src/model/conversation.js | 3 --- src/ui/chat/ChatMessageBar.js | 8 ++++---- src/ui/game/GameScreen.js | 1 - 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/model/conversation.js b/src/model/conversation.js index 401a5a8..e293722 100644 --- a/src/model/conversation.js +++ b/src/model/conversation.js @@ -172,12 +172,9 @@ export function handleConversationMessage( return {...prevState, conversationsById}; } } else if (msg.type === 'DRAFT_CHAT') { - console.log('Drafting chat'); - console.log(msg); let convoId = msg.conversationId; let conversationsById: Index = {...prevState.conversationsById}; if (conversationsById[convoId]) { - console.log('Found conversation!'); conversationsById[convoId] = { ...conversationsById[convoId], draft: msg.text diff --git a/src/ui/chat/ChatMessageBar.js b/src/ui/chat/ChatMessageBar.js index a815df3..dedea7c 100644 --- a/src/ui/chat/ChatMessageBar.js +++ b/src/ui/chat/ChatMessageBar.js @@ -6,7 +6,7 @@ import type {Conversation} from '../../model'; type Props = { conversation: ?Conversation, onSubmit: string => any; - onDraft: string => any; + onDraft?: string => any; }; export default class ChatMessageBar extends Component { @@ -23,9 +23,9 @@ export default class ChatMessageBar extends Component { } } - _onChange(event: SyntheticEvent) { + _onChange = (event: SyntheticEvent) => { let target: EventTarget = event.target; - if (target instanceof HTMLInputElement) { + if (target instanceof HTMLInputElement && this.props.onDraft) { this.props.onDraft(target.value); } } @@ -53,7 +53,7 @@ export default class ChatMessageBar extends Component { type='text' placeholder={placeholder} value={draft === '' ? null : draft} - onChange={this._onChange.bind(this)} + onChange={this._onChange} autoFocus={!isTouchDevice()} /> diff --git a/src/ui/game/GameScreen.js b/src/ui/game/GameScreen.js index 57c0182..d8e9759 100644 --- a/src/ui/game/GameScreen.js +++ b/src/ui/game/GameScreen.js @@ -229,7 +229,6 @@ export default class GameScreen extends Component { chatsDisabled: !game.tree }} onSubmit={this._onChat} - onDraft={()=>{}} /> From 50fabcd966c5d048d77499178b70dd40542c022d Mon Sep 17 00:00:00 2001 From: Logan Buckley Date: Wed, 10 May 2017 15:49:42 -0500 Subject: [PATCH 3/3] Implement debouncing and properly clear draft on room change. --- src/model/AppActions.js | 7 ++++--- src/model/conversation.js | 2 +- src/ui/chat/ChatMessageBar.js | 21 +++++++++++++-------- src/util/debounce.js | 14 ++++++++++++++ 4 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 src/util/debounce.js diff --git a/src/model/AppActions.js b/src/model/AppActions.js index 9579bb8..cab4ffa 100644 --- a/src/model/AppActions.js +++ b/src/model/AppActions.js @@ -18,6 +18,7 @@ import type { PlayerColor } from './types'; import {distinct} from '../util/collection'; +import {debounce} from '../util/debounce'; const APP_STATE_SAVE_KEY = 'savedAppState'; @@ -426,13 +427,13 @@ export class AppActions { this._client.sendMessage({type: 'CHAT', text: body, channelId: conversationId}); } - onDraftChat = (body: string, conversationId: number) => { + onDraftChat = debounce((body: string, conversationId: number) => { this._store.dispatch({ - type: 'DRAFT_CHAT', + type: 'SAVE_CHAT_DRAFT', conversationId: conversationId, text: body }); - } + }, 250) onSendGameChat = (body: string, gameId: number) => { if (this._isOffline()) { diff --git a/src/model/conversation.js b/src/model/conversation.js index e293722..583383d 100644 --- a/src/model/conversation.js +++ b/src/model/conversation.js @@ -171,7 +171,7 @@ export function handleConversationMessage( }; return {...prevState, conversationsById}; } - } else if (msg.type === 'DRAFT_CHAT') { + } else if (msg.type === 'SAVE_CHAT_DRAFT') { let convoId = msg.conversationId; let conversationsById: Index = {...prevState.conversationsById}; if (conversationsById[convoId]) { diff --git a/src/ui/chat/ChatMessageBar.js b/src/ui/chat/ChatMessageBar.js index dedea7c..541c5d5 100644 --- a/src/ui/chat/ChatMessageBar.js +++ b/src/ui/chat/ChatMessageBar.js @@ -9,24 +9,34 @@ type Props = { onDraft?: string => any; }; +type State = { + draft: string +}; + export default class ChatMessageBar extends Component { props: Props; + state: State = {draft: ''}; _input: ?HTMLInputElement; componentDidUpdate(prevProps: Props) { let prevConvoId = prevProps.conversation && prevProps.conversation.id; let convoId = this.props.conversation && this.props.conversation.id; + let newDraft = this.props.conversation && this.props.conversation.draft; if (convoId !== prevConvoId && this._input && !isTouchDevice()) { this._input.focus(); + this.setState({draft: newDraft || ''}); } } _onChange = (event: SyntheticEvent) => { let target: EventTarget = event.target; - if (target instanceof HTMLInputElement && this.props.onDraft) { - this.props.onDraft(target.value); + if (target instanceof HTMLInputElement) { + this.setState({draft: target.value}); + if (this.props.onDraft) { + this.props.onDraft(target.value); + } } } @@ -38,11 +48,6 @@ export default class ChatMessageBar extends Component { } else { placeholder = 'Type a message...'; } - // FIXME: This draft handling is a workaround for game chat - // storing conversation state differently from user and room chat. - // Ideally they'd work the same way and game chat drafts would be - // saved as well. - let draft = conversation ? conversation.draft : ''; return (
@@ -52,7 +57,7 @@ export default class ChatMessageBar extends Component { className='ChatMessageBar-input' type='text' placeholder={placeholder} - value={draft === '' ? null : draft} + value={this.state.draft} onChange={this._onChange} autoFocus={!isTouchDevice()} /> diff --git a/src/util/debounce.js b/src/util/debounce.js new file mode 100644 index 0000000..ffbd23d --- /dev/null +++ b/src/util/debounce.js @@ -0,0 +1,14 @@ +// @flow + +export function debounce(callback: Function, delay: number): Function { + let timeout: number; + return function() { + let context: Object = this; + let args: arguments = arguments; + let debounced: Function = () => { + callback.apply(context, args); + }; + clearTimeout(timeout); + timeout = setTimeout(debounced, delay); + }; +}