-
Notifications
You must be signed in to change notification settings - Fork 11
/
chat.coffee
258 lines (213 loc) · 8.7 KB
/
chat.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# This should only change if the room changes, else chat box will be re-rendering a lot
Handlebars.registerHelper "currentRoom", ->
return unless Meteor.userId()?
# Return room only if it exists in the collection (not deleted)
return ChatRooms.findOne(Session.get("room"), {fields: _id: 1})?._id
Template.chat.rendered = ->
# Send room changes to server
# TODO this incurs a high traffic/rendering cost when switching between rooms
# TODO only subscribe to the room if found in the ChatRooms collection
this.autorun ->
roomId = Session.get("room")
# in replay mode, messages will be automatically pushed over by the server
if Router.current().route.getName() is "replay"
console.log "Room change in replay; no action taken."
return
# request the contents of the chat room otherwise
Session.set("chatRoomReady", false)
Meteor.subscribe("chatstate", roomId, -> Session.set("chatRoomReady", true))
Template.chat.events
"click .action-room-create": (e) ->
e.preventDefault()
bootbox.prompt "Name the room", (roomName) ->
return unless !!roomName
Meteor.call "createChat", roomName, (err, id) ->
return unless id
Session.set "room", id
Template.currentChatroom.events
"click .action-room-leave": ->
# TODO convert this to a method call ... !
# bootbox.confirm "Leave this room?", (value) ->
Session.set("room", undefined) # if value
Template.currentChatroom.helpers
nameDoc: -> ChatRooms.findOne(""+@, {fields: name: 1})
Template.rooms.helpers
loaded: -> Session.equals("chatSubReady", true)
availableRooms: ->
selector = if TurkServer.isAdmin() and Session.equals("adminShowDeleted", true) then {}
else { deleted: {$exists: false} }
ChatRooms.find(selector, {sort: {name: 1}}) # For a consistent ordering
Template.roomItem.helpers
active: -> if Session.equals("room", @_id) then "active" else ""
deleted: -> if @deleted then "deleted" else ""
empty: -> @users is 0
Template.roomItem.events =
"click .action-room-enter": (e) ->
e.preventDefault()
unless Meteor.userId()
bootbox.alert "You must be logged in to join chat rooms."
return
Session.set "room", @_id
Mapper.events.emit("chat-join")
"click .action-room-delete": (e) ->
e.preventDefault()
roomId = @_id
bootbox.confirm "This will delete the chat room and its messages. Are you sure?", (res) ->
return unless res # Only if "yes" clicked
Meteor.call("deleteChat", roomId) if roomId
# don't select chatroom (above function) - http://stackoverflow.com/questions/10407783/stop-event-propagation-in-meteor
e.stopImmediatePropagation()
Template.roomUsers.helpers
users: -> ChatUsers.find {}
findUser: -> Meteor.users.findOne @userId
Template.roomHeader.rendered = ->
tmplInst = this
this.autorun ->
# Trigger this whenever title changes - note only name is reactively depended on
Blaze.getData()
# Destroy old editable if it exists
tmplInst.$(".editable").editable("destroy").editable
display: ->
success: (response, newValue) ->
roomId = Session.get("room")
return unless roomId
Meteor.call "renameChat", roomId, newValue
showEvent = (eventId) ->
Mapper.switchTab 'events' # Make sure we are on the event page
# Set up a scroll event, then trigger a re-render
Mapper.selectEvent(eventId)
Mapper.scrollToEvent(eventId)
Template.messageBox.events =
"click .tweet-icon.clickme": (e) ->
tweetId = $(e.target).data("tweetid") + "" # Ensure string, not integer
data = Datastream.findOne(tweetId)
return unless data
# Error message if tweet is hidden, or went on a deleted event
if data.hidden or Events.findOne(data.events?[0])?.deleted
bootbox.alert("That data has been deleted.")
return
if data.events? and data.events.length > 0
showEvent data.events[0] # Scroll to event
else
Mapper.selectData(tweetId)
Mapper.scrollToData(tweetId)
"click .event-icon.clickme": (e) ->
eventId = $(e.target).data("eventid") + ""
event = Events.findOne(eventId)
return unless event
if event.deleted
bootbox.alert("That event has been deleted.")
return
showEvent(eventId)
Template.messageBox.helpers
loaded: -> Session.equals("chatRoomReady", true)
messages: ->
# Multiple chatrooms may be loaded, to save on traffic or for the replay.
# Since we're sorting anyway, filter by room.
ChatMessages.find { room: Session.get("room") },
sort: {timestamp: 1}
# These usernames are nonreactive because find does not use any reactive variables
Template.messageItem.helpers
username: -> Meteor.users.findOne(@userId)?.username || @userId
# If updating the user, also update server notification generations.
userRegex = new RegExp('(^|\\b|\\s)(@[\\w.]+)($|\\b|\\s)','g')
tweetRegex = new RegExp('(^|\\b|\\s)(~[\\d]+)($|\\b|\\s)','g')
eventRegex = new RegExp('(^|\\b|\\s)(#[\\d]+)($|\\b|\\s)','g')
###
Blaze.toHTML registers reactive dependencies, so chat messages can get
re-rendered with state. However, this can cause excessive CPU usage.
As a result, we use Blaze.toHTML with static data, and use very specific
reactive dependencies below. It has to be reactive, or if the chat loads
before the events/tweets then messages will be empty.
Moving the findOne functions inside the Blaze.With won't make any difference
below as the entire chat message has to be re-rendered anyway.
###
# TODO: remove ugly spaces added below
userFunc = (_, p1, p2) ->
username = p2.substring(1)
# userPill uses _id, username, and status
user = Meteor.users.findOne(username: username, {fields: {username: 1, status: 1}})
return " " + if user then Blaze.toHTMLWithData(Template.userPill, user) else p2
tweetFunc = (_, p1, p2) ->
tweetNum = parseInt( p2.substring(1) )
# tweetIconClickable only uses _id and num
tweet = Datastream.findOne( {num: tweetNum}, {fields: num: 1} )
return " " + if tweet then Blaze.toHTMLWithData(Template.tweetIconClickable, tweet) else p2
eventFunc = (_, p1, p2) ->
eventNum = parseInt( p2.substring(1) )
# eventIconClickable only uses _id and num
event = Events.findOne( {num: eventNum}, {fields: num: 1} )
return " " + if event then Blaze.toHTMLWithData(Template.eventIconClickable, event) else p2
# Because messages only render when inserted, we can use this to scroll the chat window
Template.messageItem.rendered = ->
# Scroll down whenever anything happens
$messages = $(".messages-body")
$messages.scrollTop $messages[0].scrollHeight
# Replace any matched users, tweets, or events with links
Template.messageItem.helpers
renderText: ->
text = Handlebars._escape(@text)
# No SafeString needed here as long as renderText is unescaped
text = text.replace userRegex, userFunc
text = text.replace tweetRegex, tweetFunc
text = text.replace eventRegex, eventFunc
eventText: ->
username = Meteor.users.findOne(@userId).username
return username + " has " + (if @event is "enter" then "entered" else "left" ) + " the room."
Template.chatInput.rendered = ->
$(@find(".chat-help")).popover
html: true
placement: "top"
trigger: "hover"
content: Blaze.toHTML Template.chatPopover
Template.chatInput.events =
submit: (e, tmpl) ->
e.preventDefault()
$msg = $( tmpl.find(".chat-input") )
return unless $msg.val()
Meteor.call "sendChat", Session.get("room"), $msg.val() # Server only method
$msg.val("")
$msg.focus()
Meteor.flush()
# Auto scroll happens on messageBox render now..
Mapper.events.emit("chat-message")
# RegExp syntax below taken from
# https://github.com/meteor/meteor/blob/devel/packages/minimongo/selector.js
# We use $where because we need the regex to match on a number!
# This worked before but was removed in 0.7.1:
# https://github.com/meteor/meteor/pull/1874#issuecomment-37074734
# However, since it's all on the client, this will result in the same performance.
numericMatcher = (filter) ->
re = new RegExp("^" + filter)
return { $where: -> re.test(@num) }
Template.chatInput.helpers
settings: -> {
position: "top"
limit: 5
rules: [
{
token: '@'
collection: Meteor.users
field: "username"
template: Template.userPill
},
{
token: '~'
collection: Datastream
field: "num"
template: Template.tweetNumbered
# TODO this can select tweets attached to deleted events, but error
# shows up when they are clicked
filter: { hidden: $exists: false }
selector: numericMatcher
},
{
token: '#'
collection: Events
field: "num"
template: Template.eventShort
filter: { deleted: $exists: false }
selector: numericMatcher
}
]
}