@ -0,0 +1,12 @@ | |||
# For more information visit http://editorconfig.org/ | |||
# Top-most EditorConfig file | |||
root = true | |||
# Unix-style newlines with a newline ending every file. | |||
# Also 2 space indentation. | |||
[*] | |||
end_of_line = lf | |||
insert_final_newline = true | |||
trim_trailing_whitespace = true | |||
indent_style = space | |||
indent_size = 2 |
@ -0,0 +1,4 @@ | |||
module.exports = Exim.createActions [ | |||
'recieveMessages' | |||
'createMessage' | |||
] |
@ -0,0 +1,6 @@ | |||
module.exports = Exim.createActions [ | |||
'recieveThreads' | |||
'updateCurrent' | |||
'updateLast' | |||
'updateUnread' | |||
] |
@ -1 +0,0 @@ | |||
Files here will be copied as-is to public directory. |
@ -0,0 +1,10 @@ | |||
ThreadSection = require("./ThreadSection") | |||
MessageSection = require("./MessageSection") | |||
RouteHandler = Exim.Router.RouteHandler | |||
module.exports = Exim.createView | |||
render: -> | |||
<div className="chatapp"> | |||
<ThreadSection /> | |||
<RouteHandler /> | |||
</div> |
@ -0,0 +1,26 @@ | |||
ENTER_KEY_CODE = 13 | |||
actions = require('actions/messages') | |||
module.exports = Exim.createView | |||
getInitialState: -> | |||
text: '' | |||
onChange: (evt, value) -> | |||
@setState({text: evt.target.value}) | |||
onKeyDown: (evt) -> | |||
if evt.keyCode == ENTER_KEY_CODE | |||
evt.preventDefault() | |||
text = @state.text.trim() | |||
if text | |||
actions.createMessage(text) | |||
@setState({text: ''}) | |||
render: -> | |||
<textarea | |||
className="message-composer" | |||
name="message" | |||
value={@state.text} | |||
onChange={@onChange} | |||
onKeyDown={@onKeyDown} | |||
/> |
@ -0,0 +1,13 @@ | |||
module.exports = Exim.createView | |||
propTypes: | |||
message: React.PropTypes.object | |||
render: -> | |||
msg = @props.message | |||
<li className="message-list-item"> | |||
<h5 className="message-author-name">{msg.authorName}</h5> | |||
<div className="message-time"> | |||
{msg.date.toLocaleTimeString()} | |||
</div> | |||
<div className="message-text">{msg.text}</div> | |||
</li> |
@ -0,0 +1,44 @@ | |||
MessageListItem = require('./MessageListItem') | |||
MessageComposer = require('./MessageComposer') | |||
MessageActions = require('actions/messages') | |||
MessageStore = require('stores/messages') | |||
ThreadActions = require('actions/threads') | |||
ThreadStore = require('stores/threads') | |||
module.exports = Exim.createView | |||
mixins: [ | |||
Exim.Router.State | |||
Exim.connect(MessageStore, 'messages') | |||
Exim.connect(ThreadStore, 'currentID') | |||
] | |||
statics: | |||
willTransitionTo: (transition, params) -> | |||
if (params && params.id) | |||
ThreadActions.updateCurrent(params.id) | |||
MessageActions.recieveMessages() | |||
componentDidUpdate: -> | |||
@scrollToBottom() | |||
updateUnread: -> | |||
id = @getParams().id | |||
thread = ThreadStore.get('threads')[id] | |||
if (!thread.lastMessage.isRead) | |||
ThreadActions.updateUnread(id) | |||
scrollToBottom: -> | |||
list = @refs.messageList.getDOMNode() | |||
list.scrollTop = list.scrollHeight | |||
render: -> | |||
messageListItems = @state.messages.map (message) -> | |||
<MessageListItem key={message.id} message={message} /> | |||
<div className="message-section" onMouseMove={@updateUnread}> | |||
<h3 className="message-thread-heading">{@state.current}</h3> | |||
<ul className="message-list" ref="messageList"> | |||
{messageListItems} | |||
</ul> | |||
<MessageComposer /> | |||
</div> |
@ -0,0 +1,37 @@ | |||
Link = Exim.Router.Link | |||
State = Exim.Router.State | |||
ListItem = Exim.createView | |||
mixins: [State] | |||
render: -> | |||
isActive = @isActive(@props.to, @props.params, @props.query) | |||
className = (if isActive then ' active ' else '') | |||
if cls = @props.className | |||
className += cls | |||
@props.className = '' | |||
link = Link(@props) | |||
<li className={className}>{link}</li> | |||
module.exports = Exim.createView | |||
mixins: [State] | |||
propTypes: | |||
thread: React.PropTypes.object | |||
currentThreadID: React.PropTypes.string | |||
render: -> | |||
thread = @props.thread | |||
lastMessage = thread.lastMessage | |||
className = Exim.cx | |||
'thread-list-item': true | |||
unread: thread.unread | |||
<ListItem className={className} to="message" params={{id: thread.id}}> | |||
<h5 className="thread-name">{thread.name}</h5> | |||
<div className="thread-time"> | |||
{lastMessage.date.toLocaleTimeString()} | |||
</div> | |||
<div className="thread-last-message"> | |||
{lastMessage.text} | |||
</div> | |||
</ListItem> |
@ -0,0 +1,26 @@ | |||
ThreadListItem = require('./ThreadListItem') | |||
actions = require('actions/threads') | |||
Store = require('stores/threads') | |||
module.exports = React.createClass | |||
mixins: [Exim.connect(Store, 'threads', 'unread')] | |||
componentWillMount: -> | |||
actions.recieveThreads() | |||
render: -> | |||
threads = @state.threads | |||
items = Object.keys(threads).sort((a, b) -> | |||
threads[b].lastMessage.date - threads[a].lastMessage.date if threads[b].lastMessage and threads[a].lastMessage | |||
).map (threadID) -> | |||
thread = threads[threadID] | |||
<ThreadListItem key={threadID} thread={thread} /> | |||
<div className="thread-section"> | |||
<div className="thread-count"> | |||
<span><b>Unread threads:</b> {this.state.unread}</span> | |||
</div> | |||
<ul className="thread-list"> | |||
{items} | |||
</ul> | |||
</div> |
@ -0,0 +1,7 @@ | |||
routes = require('routes') | |||
utils = require('lib/utils') | |||
document.addEventListener 'DOMContentLoaded', -> | |||
utils.initLocalStorage() | |||
Exim.Router.startRouting routes, document.body | |||
, true |
@ -1 +0,0 @@ | |||
console.log('Yo'); |
@ -0,0 +1,79 @@ | |||
utils = {} | |||
utils.transform = (constants, mappings) -> | |||
Object.keys(mappings).map (key) -> | |||
[constants[key], mappings[key]] | |||
utils.getAndParse = (name) -> | |||
JSON.parse localStorage.getItem name | |||
utils.dateComparator = (first, second) -> | |||
first.date - second.date | |||
utils.dateSetter = (message) -> | |||
message.date = new Date(message.timestamp) | |||
message | |||
utils.getThreads = (messages) -> | |||
threads = {} | |||
for message in messages | |||
threads[message.threadID] = | |||
id: message.threadID | |||
name: message.threadName | |||
lastMessage: message | |||
threads | |||
utils.initLocalStorage = -> | |||
localStorage.clear() | |||
localStorage.setItem "messages", JSON.stringify [ | |||
id: "m_1" | |||
threadID: "t_1" | |||
threadName: "Jack and Jill" | |||
authorName: "Jill" | |||
text: "Hey Jack, want to give a Flux talk at ForwardJS?" | |||
timestamp: Date.now() - 99999 | |||
, | |||
id: "m_2" | |||
threadID: "t_1" | |||
threadName: "Jack and Jill" | |||
authorName: "Jill" | |||
text: "Seems like a pretty cool conference." | |||
timestamp: Date.now() - 89999 | |||
, | |||
id: "m_3" | |||
threadID: "t_1" | |||
threadName: "Jack and Jill" | |||
authorName: "Jack" | |||
text: "Sounds good. Will they be serving dessert?" | |||
timestamp: Date.now() - 79999 | |||
, | |||
id: "m_4" | |||
threadID: "t_2" | |||
threadName: "Paul and Jill" | |||
authorName: "Jill" | |||
text: "Hey Paul, want to get a beer after the conference?" | |||
timestamp: Date.now() - 69999 | |||
, | |||
id: "m_5" | |||
threadID: "t_2" | |||
threadName: "Paul and Jill" | |||
authorName: "Paul" | |||
text: "Totally! Meet you at the hotel bar." | |||
timestamp: Date.now() - 59999 | |||
, | |||
id: "m_6" | |||
threadID: "t_3" | |||
threadName: "Functional Heads" | |||
authorName: "Jill" | |||
text: "Hey Brian, are you going to be talking about functional stuff?" | |||
timestamp: Date.now() - 49999 | |||
, | |||
id: "m_7" | |||
threadID: "t_3" | |||
threadName: "Jill and Brian" | |||
authorName: "Brian" | |||
text: "At ForwardJS? Yeah, of course. See you there!" | |||
timestamp: Date.now() - 39999 | |||
] | |||
module.exports = utils |
@ -0,0 +1,8 @@ | |||
Route = Exim.Router.Route | |||
App = require('./components/App') | |||
MessageSection = require('./components/MessageSection') | |||
module.exports = | |||
<Route handler={App} path="/"> | |||
<Route name="message" handler={MessageSection} path="threads/:id" /> | |||
</Route> |
@ -0,0 +1,45 @@ | |||
utils = require('lib/utils') | |||
actions = require('actions/messages') | |||
ThreadActions = require('actions/threads') | |||
ThreadStore = require('./threads') | |||
module.exports = Exim.createStore | |||
actions: actions, | |||
getInitial: -> | |||
messages: [] | |||
recieveMessages: -> | |||
threadID = ThreadStore.get('currentID') | |||
messages = utils.getAndParse('messages') | |||
filtered = messages | |||
.filter (message) -> | |||
message.threadID == threadID | |||
.map(utils.dateSetter) | |||
.sort(utils.dateComparator) | |||
@update({messages: filtered}) | |||
createMessage: | |||
on: (text) -> | |||
timestamp = Date.now() | |||
message = | |||
id: 'm_' + timestamp, | |||
threadID: ThreadStore.get('currentID'), | |||
text: text, | |||
isRead: true, | |||
authorName: 'Bill', | |||
date: new Date(timestamp), | |||
timestamp: timestamp | |||
localStorageItems = JSON.parse(localStorage.getItem('messages')) | |||
localStorageItems.push(message) | |||
localStorage.setItem('messages', JSON.stringify(localStorageItems)) | |||
storeItems = @get('messages') | |||
storeItems.push(message) | |||
@update('messages', storeItems) | |||
message | |||
did: (message) -> | |||
ThreadActions.updateLast(message) |
@ -0,0 +1,42 @@ | |||
actions = require("actions/threads") | |||
utils = require("lib/utils") | |||
module.exports = Exim.createStore | |||
actions: actions | |||
getInitial: -> | |||
threads: {} | |||
currentID: null | |||
unread: 0 | |||
recieveThreads: -> | |||
messages = utils.getAndParse('messages') | |||
.map(utils.dateSetter) | |||
.sort(utils.dateComparator) | |||
threads = utils.getThreads(messages) | |||
@update 'threads', threads | |||
didRecieveThreads: -> | |||
actions.updateUnread() | |||
updateCurrent: (id) -> | |||
@update 'currentID', id | |||
updateLast: (message) -> | |||
threads = @get('threads') | |||
thread = threads[message.threadID] | |||
thread.lastMessage = message | |||
@update 'threads', threads | |||
updateUnread: (threadId, value) -> | |||
lastMessage = undefined | |||
threads = @get('threads') | |||
if threadId | |||
thread = threads[threadId] | |||
thread.lastMessage.isRead = value or true | |||
@update 'threads', threads | |||
unread = 0 | |||
for key of threads | |||
lastMessage = threads[key].lastMessage | |||
unread++ if lastMessage and not lastMessage.isRead | |||
@update 'unread', unread |
@ -0,0 +1,98 @@ | |||
/** | |||
* This file is provided by Facebook for testing and evaluation purposes | |||
* only. Facebook reserves all rights not expressly granted. | |||
* | |||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |||
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN | |||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | |||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |||
*/ | |||
.chatapp { | |||
font-family: 'Muli', 'Helvetica Neue', helvetica, arial; | |||
max-width: 760px; | |||
margin: 20px auto; | |||
overflow: hidden; | |||
} | |||
.message-list, .thread-list { | |||
border: 1px solid #ccf; | |||
font-size: 16px; | |||
height: 400px; | |||
margin: 0; | |||
overflow-y: auto; | |||
padding: 0; | |||
} | |||
.message-section { | |||
float: right; | |||
width: 65%; | |||
} | |||
.thread-section { | |||
float: left; | |||
width: 32.5%; | |||
} | |||
.message-thread-heading, | |||
.thread-count { | |||
height: 40px; | |||
margin: 0; | |||
} | |||
.message-list-item, .thread-list-item { | |||
list-style: none; | |||
padding: 12px 14px 14px; | |||
} | |||
.thread-list-item { | |||
border-bottom: 1px solid #ccc; | |||
cursor: pointer; | |||
} | |||
.thread-list:hover .thread-list-item:hover { | |||
background-color: #f8f8ff; | |||
} | |||
.thread-list:hover .thread-list-item { | |||
background-color: #fff; | |||
} | |||
.thread-list-item.active, | |||
.thread-list:hover .thread-list-item.active, | |||
.thread-list:hover .thread-list-item.active:hover { | |||
background-color: #efefff; | |||
cursor: default; | |||
} | |||
.message-author-name, | |||
.thread-name { | |||
color: #66c; | |||
float: left; | |||
font-size: 13px; | |||
margin: 0; | |||
} | |||
.message-time, .thread-time { | |||
color: #aad; | |||
float: right; | |||
font-size: 12px; | |||
} | |||
.message-text, .thread-last-message { | |||
clear: both; | |||
font-size: 14px; | |||
padding-top: 10px; | |||
} | |||
.message-composer { | |||
box-sizing: border-box; | |||
font-family: inherit; | |||
font-size: 14px; | |||
height: 5em; | |||
width: 100%; | |||
margin: 20px 0 0; | |||
padding: 10px; | |||
} |
@ -1,11 +1,11 @@ | |||
exports.config = | |||
# See http://brunch.io/#documentation for docs. | |||
# @see http://brunch.io/#documentation for docs | |||
files: | |||
javascripts: | |||
joinTo: | |||
'javascripts/app.js': /^app/ | |||
'javascripts/vendor.js': /^(?!app)/ | |||
'scripts/app.js': /^app/ | |||
'scripts/vendor.js': /^(?!app)/ | |||
stylesheets: | |||
joinTo: 'stylesheets/app.css' | |||
joinTo: 'styles/app.css' | |||
templates: | |||
joinTo: 'javascripts/app.js' | |||
joinTo: 'scripts/app.js' |