diff --git a/Blaze/Content/chat/Chat.css b/Blaze/Content/chat/Chat.css index 5b955c7..f0ba0e0 100644 --- a/Blaze/Content/chat/Chat.css +++ b/Blaze/Content/chat/Chat.css @@ -107,6 +107,23 @@ margin-left: 5px; } +.search-results { + width: 95%; + border: 1px solid #ccc; + overflow: auto; + margin-left: 0px; + margin-top: 0px; + padding-left: 0px; + position: absolute; + top: 95px; + bottom: 40px; + background-color: #fff; +} + +#earlier_messages { + padding-left: 10px; +} + .messages { width: 70%; border: 1px solid #ccc; @@ -120,6 +137,10 @@ background-color: #fff; } +.messages ul { + padding: 0px; +} + .messages li { list-style-type: none; position: relative; @@ -131,7 +152,7 @@ border-left: 175px solid #f1f1f1; /* background color for left column (user info) */ } -.messages li img.gravatar { +.messages li img.gravatar, #messages-search img.gravatar { height: 16px; width: 16px; display: inline-block; @@ -516,6 +537,10 @@ li.room:hover { background-color: #88b7d6; } +table#search-results { + font-size: 13px; +} + /* This stuff is to support TweetContentProvider, but should be extracted out if other content providers need custom CSS */ .tweet { diff --git a/Blaze/Controllers/HomeController.cs b/Blaze/Controllers/HomeController.cs index 7b98f71..59d916f 100644 --- a/Blaze/Controllers/HomeController.cs +++ b/Blaze/Controllers/HomeController.cs @@ -199,6 +199,62 @@ private ActionResult HandleWebException(string fullUrl, WebException ex) return new FileStreamResult(response.GetResponseStream(), response.ContentType); } + [AuthActionFilter] + public ActionResult Search(string account, string url, string auth) + { + string fullUrl = string.Format("https://{0}.campfirenow.com/{1}?{2}&format=json", account, url, Request["QUERY_STRING"]); + var request = (HttpWebRequest)WebRequest.Create(fullUrl); + request.Method = "GET"; + request.ContentType = "application/json"; + request.Headers["Authorization"] = "Bearer " + auth; + try + { + var response = (HttpWebResponse)request.GetResponse(); + var reader = new StreamReader(response.GetResponseStream()); + var data = reader.ReadToEnd(); + dynamic obj = Newtonsoft.Json.JsonConvert.DeserializeObject(data); + var processor = new MessageProcessor(); + foreach (var msg in obj.messages) + { + msg.parsed_body = processor.ProcessMessage(Convert.ToString(msg.body)); + } + string result = Newtonsoft.Json.JsonConvert.SerializeObject(obj); + return Content(result, "application/json"); + } + catch (WebException ex) + { + return HandleWebException(fullUrl, ex); + } + } + + [AuthActionFilter] + public ActionResult Transcript(string account, string url, string auth) + { + string fullUrl = string.Format("https://{0}.campfirenow.com/room/{1}?format=json", account, url, Request["QUERY_STRING"]); + var request = (HttpWebRequest)WebRequest.Create(fullUrl); + request.Method = "GET"; + request.ContentType = "application/json"; + request.Headers["Authorization"] = "Bearer " + auth; + try + { + var response = (HttpWebResponse)request.GetResponse(); + var reader = new StreamReader(response.GetResponseStream()); + var data = reader.ReadToEnd(); + dynamic obj = Newtonsoft.Json.JsonConvert.DeserializeObject(data); + var processor = new MessageProcessor(); + foreach (var msg in obj.messages) + { + msg.parsed_body = processor.ProcessMessage(Convert.ToString(msg.body)); + } + string result = Newtonsoft.Json.JsonConvert.SerializeObject(obj); + return Content(result, "application/json"); + } + catch (WebException ex) + { + return HandleWebException(fullUrl, ex); + } + } + [AuthActionFilter] public ActionResult GetFile(string account, string auth, string url) { diff --git a/Blaze/Global.asax.cs b/Blaze/Global.asax.cs index e960f22..a22e4bd 100644 --- a/Blaze/Global.asax.cs +++ b/Blaze/Global.asax.cs @@ -60,6 +60,18 @@ public static void RegisterRoutes(RouteCollection routes) new { controller = "Home", action = "Recent" } // Parameter defaults ); + routes.MapRoute( + "search_route", // Route name + "search/{account}/{*url}", // URL with parameters + new { controller = "Home", action = "Search" } // Parameter defaults + ); + + routes.MapRoute( + "transcript_route", // Route name + "transcript/{account}/{*url}", // URL with parameters + new { controller = "Home", action = "Transcript" } // Parameter defaults + ); + routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters diff --git a/Blaze/Scripts/blaze/api.js b/Blaze/Scripts/blaze/api.js index 3fee8da..48c12dd 100644 --- a/Blaze/Scripts/blaze/api.js +++ b/Blaze/Scripts/blaze/api.js @@ -71,6 +71,27 @@ Campfire.prototype.uploadFile = function(roomId, data, callback) { // not completed yet! }; +Campfire.prototype.searchMessages = function (searchTerm, callback) { + var self = this; + var data = {}; + data['q'] = searchTerm; + var base = self.base.replace('/x', '/search'); + $.ajax({ + url: base + '/search', + data: data, + type: 'GET', + beforeSend: $.proxy(self.setAuthHeader, self), + success: function (reply) { + callback(reply.messages); + }, + error: function (xhr, txt, err) { + console && console.log('searchMessages failure: ' + txt + ' (' + err + ')'); + callback([]); + }, + dataType: 'json' + }); +}; + Campfire.prototype.sendMessage = function (roomId, type, message, isPaste, callback) { var self = this; if (message === '') { @@ -108,6 +129,25 @@ Campfire.prototype.starMessage = function (message) { }); }; +Campfire.prototype.transcript = function (room, message, callback) { + var self = this; + var base = self.base.replace('/x', '/transcript'); + var d = new Date(message.created_at()); + $.ajax({ + url: base + '/' + room.id() + '/transcript/' + (d.getYear() + 1900) + '/' + (d.getMonth()+1) + '/' + d.getDate(), + type: 'GET', + beforeSend: $.proxy(self.setAuthHeader, self), + success: function (reply) { + callback(reply.messages, message); + }, + error: function (xhr, txt, err) { + console && console.log('transcript failure: ' + txt + ' (' + err + ')'); + callback([]); + }, + dataType: 'json' + }); +}; + Campfire.prototype.getUsers = function (roomId, callback) { var self = this; $.ajax({ diff --git a/Blaze/Scripts/blaze/chatcontroller.js b/Blaze/Scripts/blaze/chatcontroller.js index 3da4140..3552b6c 100644 --- a/Blaze/Scripts/blaze/chatcontroller.js +++ b/Blaze/Scripts/blaze/chatcontroller.js @@ -171,7 +171,20 @@ ChatController.prototype.getUser = function (id) { } }; -ChatController.prototype.sendMessage = function(room, message, isPaste) { +ChatController.prototype.searchMessages = function (searchTerm) { + var self = this; + self.roomsModel.clearSearchResults(); + self.campfire.searchMessages(searchTerm, function (messages) { + $.each(messages, function (i, o) { + var user = o.user_id ? self.getUser(o.user_id) : new UserModel({ id: 0, name: '' }); + var messageModel = new MessageModel(o, user, self.currentUser, null, self.contentProcessor, self); + self.roomsModel.addSearchResult(messageModel); + }); + }); + self.view.changeRoom('search'); +}; + +ChatController.prototype.sendMessage = function (room, message, isPaste) { var self = this; var type = ''; if (message.indexOf("/play ") === 0) { @@ -209,4 +222,23 @@ ChatController.prototype.signOut = function () { ChatController.prototype.changeTopic = function(room) { var self = this; self.campfire.changeTopic(room.id(), room.topic()); -}; \ No newline at end of file +}; + + +ChatController.prototype.transcript = function (room, message) { + var self = this; + self.roomsModel.clearTranscriptMessages(); + self.view.changeRoom('transcript'); + self.campfire.transcript(room, message, function (messages, selectedMessage) { + var lastMessage = null; + $.each(messages, function (i, o) { + var user = o.user_id ? self.getUser(o.user_id) : new UserModel({ id: 0, name: '' }); + if (o.type !== 'TimestampMessage') { + var messageModel = new MessageModel(o, user, self.currentUser, lastMessage, self.contentProcessor, self); + self.roomsModel.addTranscriptMessage(messageModel); + lastMessage = messageModel; + } + }); + self.view.scrollIntoTranscriptView(selectedMessage); + }); +}; diff --git a/Blaze/Scripts/blaze/chatview.js b/Blaze/Scripts/blaze/chatview.js index c5887f3..c0de4b6 100644 --- a/Blaze/Scripts/blaze/chatview.js +++ b/Blaze/Scripts/blaze/chatview.js @@ -44,7 +44,7 @@ ChatView.prototype.init = function (roomsModel, campfire) { $(this).insertAtCaret('\n'); } }); - $('#tabs-lobby').live('click', function () { + $('#tabs-lobby, #tabs-search, #tabs-transcript').on('click', function () { var name = $(this).data('name'); self.changeRoom(name); }); @@ -231,6 +231,12 @@ ChatView.prototype.changeRoom = function (roomId) { } } }); + $('#send-message').show(); + } else if (roomId == 'search' || roomId == 'transcript') { + $('#tabs-' + roomId).addClass('current'); + $('#messages-' + roomId).addClass('current').show(); + $('#userlist-' + roomId).addClass('current').show(); + $('#send-message').hide(); } else { // lobby $('#tabs-' + roomId).addClass('current'); @@ -241,6 +247,15 @@ ChatView.prototype.changeRoom = function (roomId) { self.scrollToEnd(); }; +ChatView.prototype.scrollIntoTranscriptView = function (message) { + // this check shouldn't be necessary, seems to be a timezone problem in that + // sometimes the message doesn't appear on that day's transcript. + var item = $('#messages-transcript #m-' + message.id()); + if (item.length > 0) { + item[0].scrollIntoView(true); + } +}; + ChatView.prototype.show = function () { $('#page').fadeIn(1000); }; diff --git a/Blaze/Scripts/blaze/models.js b/Blaze/Scripts/blaze/models.js index 8dd69d2..c01f5fa 100644 --- a/Blaze/Scripts/blaze/models.js +++ b/Blaze/Scripts/blaze/models.js @@ -82,6 +82,11 @@ function RoomModel(obj, user, prefs, controller) { } } }; + this.earlierMessages = function () { + if (self.messages().length > 0) { + self.messages()[0].getTranscript(); + } + }; this.toggleSound = function () { prefs.sound(!prefs.sound()); prefs.save(); @@ -121,7 +126,7 @@ function RoomModel(obj, user, prefs, controller) { }, this.editTopic = function () { self.isEditingTopic(true); - }; + }; } function RoomPreferencesModel(parent,pref) { @@ -225,6 +230,7 @@ function RoomsModel(chat) { }; this.inputMessage = ko.observable(''); this.isPaste = ko.observable(false); + this.searchTerm = ko.observable(''); this.sendMessage = function () { if (self.visibleRoom) { var isPaste = self.inputMessage().indexOf('\n') !== -1; @@ -261,6 +267,35 @@ function RoomsModel(chat) { this.signOut = function () { chat.signOut(); }; + this.searchMessages = function () { + chat.searchMessages(self.searchTerm()); + }; + this.searchResults = ko.observableArray([]); + this.transcriptMessages = ko.observableArray([]); + this.collapseTranscriptNotifications = function (element, i, msg) { + var count = 0; + while (i > 0 && self.transcriptMessages()[i].isNotification()) { + count++; + i--; + if (count > 3) { + msg.collapse(); + return; + } + } + }; + this.isVisible = ko.observable(false); + this.addSearchResult = function (searchResult) { + self.searchResults.push(searchResult); + }; + this.clearSearchResults = function () { + self.searchResults.removeAll(); + }; + this.clearTranscriptMessages = function () { + self.transcriptMessages.removeAll(); + }; + this.addTranscriptMessage = function (message) { + self.transcriptMessages.push(message); + }; } function UserModel(obj) { var self = this; @@ -293,6 +328,9 @@ function MessageModel(obj, user, currentUser, prevMsg, emoji, chat) { var self = this; this.chat = chat; this.previousMessage = prevMsg; + this.room = $.grep(this.chat.roomsModel.rooms(), function (room) { + return room.id() == obj.room_id; + })[0]; this.isLastMessage = ko.observable(false); this.id = ko.observable(obj.id); this.parsed_body = ko.observable(obj.parsed_body); @@ -302,17 +340,25 @@ function MessageModel(obj, user, currentUser, prevMsg, emoji, chat) { this.description = ko.observable(obj.description); this.descriptionIsUrl = function () { return self.description().indexOf("http") === 0; - } + }; + this.getTranscript = function () { + self.chat.transcript(self.room, self); + return false; + }; this.url = ko.observable(obj.url); this.when = ko.computed(function () { var d = new Date(self.created_at()); var mins = d.getMinutes() < 10 ? '0' + d.getMinutes() : d.getMinutes(); if (d.getHours() >= 12) { - var hours = d.getHours() === 12 ? 12 : (d.getHours() - 12); + var hours = d.getHours() === 12 ? 12 : (d.getHours() - 12); return hours + ':' + mins + ' PM'; } return d.getHours() + ':' + mins + ' AM'; }); + this.day = ko.computed(function () { + var d = new Date(self.created_at()); + return d.toLocaleDateString(); + }); this.user = user; this.userId = ko.computed(function () { return self.user.id(); @@ -337,7 +383,7 @@ function MessageModel(obj, user, currentUser, prevMsg, emoji, chat) { this.showUser = ko.computed(function () { if (self.isTimestamp()) return false; - if (self.previousMessage === undefined) + if (self.previousMessage === undefined || self.previousMessage === null) return true; if(self.previousMessage.isNotification()) return true; @@ -407,4 +453,7 @@ function MessageModel(obj, user, currentUser, prevMsg, emoji, chat) { self.chat.starMessage(self); return false; }; -} \ No newline at end of file + this.searchUrl = function () { + return "https://scriptrock.campfirenow.com/room/" + self.room.id() + "/transcript/message/" + self.id() + "#message_" + self.id(); + }; +} diff --git a/Blaze/Views/Home/Chat.cshtml b/Blaze/Views/Home/Chat.cshtml index 90e6899..4f7c30b 100644 --- a/Blaze/Views/Home/Chat.cshtml +++ b/Blaze/Views/Home/Chat.cshtml @@ -38,7 +38,7 @@ @@ -98,14 +106,65 @@
- -