Commit 8d1e10bd by E. Kolpakov

Fixes and improvements from solutions fork:

* Removing jQuery.ajax.beforeSend - it might be set globally and this code overrides it (part of 62fa253a)
* Avoid resetting Backbone history if already started (part of 9591b526)
* goHome no longer triggers thread:removed event - already triggered up the call stack (part of 9591b526)
* Avoid using beforeSend as it might be set globally (adapted from 2dfe104f)
* Refactor triggering navigation to use constant/method instead of magic strings (part of 4843facd)
* Switching to using jQueyr.prop instead of jQuery.attr
* Fixed email notifications enable/disable (adapted from 8a7f0224)
* Load thread if requested thread isn't loaded yet (adapted from e28dde32)
* Added ability to set custom css classes on search messages (adapted from b2e4c189)
* Actually rerendering view with new model data when it is loaded/changed
parent e1dbfe3a
/* globals DiscussionThreadListView, DiscussionThreadView, DiscussionUtil, NewPostView */ /* globals DiscussionThreadListView, DiscussionThreadView, DiscussionUtil, NewPostView, Thread */
(function() { (function() {
'use strict'; 'use strict';
var __hasProp = {}.hasOwnProperty, var __hasProp = {}.hasOwnProperty,
...@@ -18,9 +18,20 @@ ...@@ -18,9 +18,20 @@
return child; return child;
}; };
function getSingleThreadRoute(commentable_id, thread_id) {
return commentable_id + "/threads/" + thread_id;
}
if (typeof Backbone !== "undefined" && Backbone !== null) { if (typeof Backbone !== "undefined" && Backbone !== null) {
this.DiscussionRouter = (function(_super) { this.DiscussionRouter = (function(_super) {
var allThreadsRoute = "",
singleThreadRoute = getSingleThreadRoute(":forum_name", ":thread_id"), // :forum_name/threads/:thread_id
routes = {};
routes[allThreadsRoute] = "allThreads";
routes[singleThreadRoute] = "showThread";
__extends(DiscussionRouter, _super); __extends(DiscussionRouter, _super);
function DiscussionRouter() { function DiscussionRouter() {
...@@ -40,16 +51,16 @@ ...@@ -40,16 +51,16 @@
this.showMain = function() { this.showMain = function() {
return DiscussionRouter.prototype.showMain.apply(self, arguments); return DiscussionRouter.prototype.showMain.apply(self, arguments);
}; };
this.renderThreadView = function() {
return DiscussionRouter.prototype.renderThreadView.apply(self, arguments);
};
this.setActiveThread = function() { this.setActiveThread = function() {
return DiscussionRouter.prototype.setActiveThread.apply(self, arguments); return DiscussionRouter.prototype.setActiveThread.apply(self, arguments);
}; };
return DiscussionRouter.__super__.constructor.apply(this, arguments); return DiscussionRouter.__super__.constructor.apply(this, arguments);
} }
DiscussionRouter.prototype.routes = { DiscussionRouter.prototype.routes = routes;
"": "allThreads",
":forum_name/threads/:thread_id": "showThread"
};
DiscussionRouter.prototype.initialize = function(options) { DiscussionRouter.prototype.initialize = function(options) {
var self = this; var self = this;
...@@ -89,16 +100,44 @@ ...@@ -89,16 +100,44 @@
if (this.thread) { if (this.thread) {
return this.nav.setActiveThread(this.thread.get("id")); return this.nav.setActiveThread(this.thread.get("id"));
} else { } else {
return this.nav.goHome; return this.nav.goHome();
} }
}; };
DiscussionRouter.prototype.showThread = function(forum_name, thread_id) { DiscussionRouter.prototype.showThread = function(forum_name, thread_id) {
var self;
self = this;
this.thread = this.discussion.get(thread_id); this.thread = this.discussion.get(thread_id);
if (this.thread) {
this.renderThreadView();
return;
}
// if thread is not loaded yet for some reason - try loading it
DiscussionUtil.safeAjax({
url: DiscussionUtil.urlFor('retrieve_single_thread', forum_name, thread_id)
}).done(function(data) {
// if succeded - proceed normally
self.thread = new Thread(data.content);
self.discussion.add(self.thread);
self.renderThreadView();
}).fail(function(xhr) {
// otherwise display error message and navigate to all threads view
var errorMessage = (xhr.status === 404) ?
gettext("The thread you selected has been deleted. Please select another thread.") :
gettext("We had some trouble loading more responses. Please try again.");
DiscussionUtil.discussionAlert(gettext("Sorry"), errorMessage);
this.allThreads();
});
};
DiscussionRouter.prototype.renderThreadView = function() {
this.thread.set("unread_comments_count", 0); this.thread.set("unread_comments_count", 0);
this.thread.set("read", true); this.thread.set("read", true);
this.setActiveThread(); this.setActiveThread();
return this.showMain(); this.showMain();
}; };
DiscussionRouter.prototype.showMain = function() { DiscussionRouter.prototype.showMain = function() {
...@@ -127,17 +166,14 @@ ...@@ -127,17 +166,14 @@
}; };
DiscussionRouter.prototype.navigateToThread = function(thread_id) { DiscussionRouter.prototype.navigateToThread = function(thread_id) {
var thread; var thread, targetThreadRoute;
thread = this.discussion.get(thread_id); thread = this.discussion.get(thread_id);
return this.navigate("" + (thread.get("commentable_id")) + "/threads/" + thread_id, { targetThreadRoute = getSingleThreadRoute(thread.get("commentable_id"), thread_id);
trigger: true this.navigate(targetThreadRoute, { trigger: true});
});
}; };
DiscussionRouter.prototype.navigateToAllThreads = function() { DiscussionRouter.prototype.navigateToAllThreads = function() {
return this.navigate("", { this.navigate(allThreadsRoute, { trigger: true });
trigger: true
});
}; };
DiscussionRouter.prototype.showNewPost = function() { DiscussionRouter.prototype.showNewPost = function() {
......
...@@ -34,10 +34,11 @@ ...@@ -34,10 +34,11 @@
course_settings: course_settings course_settings: course_settings
}); });
/* jshint +W031*/ /* jshint +W031*/
return Backbone.history.start({ if (!Backbone.History.started) {
pushState: true, Backbone.history.start({pushState: true, root: "/courses/" + $$course_id + "/discussion/forum/"});
root: "/courses/" + $$course_id + "/discussion/forum/" } else {
}); Backbone.history.loadUrl(window.location.pathname);
}
} }
}; };
DiscussionProfileApp = { DiscussionProfileApp = {
......
...@@ -184,9 +184,9 @@ ...@@ -184,9 +184,9 @@
DiscussionUtil.safeAjax = function(params) { DiscussionUtil.safeAjax = function(params) {
var $elem, deferred, request, var $elem, deferred, request,
self = this; self = this, deferred;
$elem = params.$elem; $elem = params.$elem;
if ($elem && $elem.attr("disabled")) { if ($elem && $elem.prop("disabled")) {
deferred = $.Deferred(); deferred = $.Deferred();
deferred.reject(); deferred.reject();
return deferred.promise(); return deferred.promise();
...@@ -194,18 +194,6 @@ ...@@ -194,18 +194,6 @@
params.url = URI(params.url).addSearch({ params.url = URI(params.url).addSearch({
ajax: 1 ajax: 1
}); });
params.beforeSend = function() {
if ($elem) {
$elem.attr("disabled", "disabled");
}
if (params.$loading) {
if (params.loadingCallback) {
return params.loadingCallback.apply(params.$loading);
} else {
return self.showLoadingIndicator($(params.$loading), params.takeFocus);
}
}
};
if (!params.error) { if (!params.error) {
params.error = function() { params.error = function() {
self.discussionAlert( self.discussionAlert(
...@@ -216,9 +204,21 @@ ...@@ -216,9 +204,21 @@
); );
}; };
} }
if ($elem) {
$elem.prop("disabled", true);
}
if (params.$loading) {
if (params.loadingCallback) {
params.loadingCallback.apply(params.$loading);
} else {
self.showLoadingIndicator($(params.$loading), params.takeFocus);
}
}
request = $.ajax(params).always(function() { request = $.ajax(params).always(function() {
if ($elem) { if ($elem) {
$elem.removeAttr("disabled"); $elem.prop("disabled", false);
} }
if (params.$loading) { if (params.$loading) {
if (params.loadedCallback) { if (params.loadedCallback) {
...@@ -231,7 +231,7 @@ ...@@ -231,7 +231,7 @@
return request; return request;
}; };
DiscussionUtil.updateWithUndo = function(model, updates, safeAjaxParams, errorMsg) { DiscussionUtil.updateWithUndo = function(model, updates, safeAjaxParams, errorMsg, beforeSend) {
var undo, var undo,
self = this; self = this;
if (errorMsg) { if (errorMsg) {
...@@ -241,6 +241,9 @@ ...@@ -241,6 +241,9 @@
} }
undo = _.pick(model.attributes, _.keys(updates)); undo = _.pick(model.attributes, _.keys(updates));
model.set(updates); model.set(updates);
if (typeof beforeSend === 'function') {
beforeSend();
}
return this.safeAjax(safeAjaxParams).fail(function() { return this.safeAjax(safeAjaxParams).fail(function() {
return model.set(undo); return model.set(undo);
}); });
......
...@@ -389,18 +389,18 @@ ...@@ -389,18 +389,18 @@
msg = gettext("We had some trouble removing this endorsement. Please try again."); msg = gettext("We had some trouble removing this endorsement. Please try again.");
} }
} }
beforeFunc = function() { return DiscussionUtil.updateWithUndo(
return self.trigger("comment:endorse"); this.model,
}; updates,
return DiscussionUtil.updateWithUndo(this.model, updates, { {
url: url, url: url,
type: "POST", type: "POST",
data: { data: { endorsed: is_endorsing },
endorsed: is_endorsing $elem: $(event.currentTarget)
}, },
beforeSend: beforeFunc, msg,
$elem: $(event.currentTarget) function() { return self.trigger("comment:endorse"); }
}, msg).always(this.trigger("comment:endorse")); ).always(this.trigger("comment:endorse"));
}; };
DiscussionContentShowView.prototype.toggleVote = function(event) { DiscussionContentShowView.prototype.toggleVote = function(event) {
......
...@@ -123,6 +123,7 @@ ...@@ -123,6 +123,7 @@
return self.displayedCollection.reset(discussion.models); return self.displayedCollection.reset(discussion.models);
}); });
this.collection.on("add", this.addAndSelectThread); this.collection.on("add", this.addAndSelectThread);
this.collection.on("thread:remove", this.threadRemoved);
this.sidebar_padding = 10; this.sidebar_padding = 10;
this.boardName = null; this.boardName = null;
this.template = _.template($("#thread-list-template").html()); this.template = _.template($("#thread-list-template").html());
...@@ -135,7 +136,8 @@ ...@@ -135,7 +136,8 @@
var content; var content;
content = _.template($("#search-alert-template").html())({ content = _.template($("#search-alert-template").html())({
'message': searchAlert.attributes.message, 'message': searchAlert.attributes.message,
'cid': searchAlert.cid 'cid': searchAlert.cid,
'css_class': searchAlert.attributes.css_class
}); });
self.$(".search-alerts").append(content); self.$(".search-alerts").append(content);
return self.$("#search-alert-" + searchAlert.cid + " a.dismiss") return self.$("#search-alert-" + searchAlert.cid + " a.dismiss")
...@@ -151,11 +153,19 @@ ...@@ -151,11 +153,19 @@
}); });
}; };
DiscussionThreadListView.prototype.addSearchAlert = function(message) { /**
* Creates search alert model and adds it to collection
* @param message - alert message
* @param css_class - Allows setting custom css class for a message. This can be used to style messages
* of different types differently (i.e. other background, completely hide, etc.)
* @returns {Backbone.Model}
*/
DiscussionThreadListView.prototype.addSearchAlert = function(message, css_class) {
var m; var m;
m = new Backbone.Model({ if (typeof css_class === 'undefined' || css_class === null) {
"message": message css_class = "";
}); }
m = new Backbone.Model({"message": message, "css_class": css_class});
this.searchAlertCollection.add(m); this.searchAlertCollection.add(m);
return m; return m;
}; };
...@@ -392,8 +402,8 @@ ...@@ -392,8 +402,8 @@
return false; return false;
}; };
DiscussionThreadListView.prototype.threadRemoved = function(thread_id) { DiscussionThreadListView.prototype.threadRemoved = function(thread) {
return this.trigger("thread:removed", thread_id); this.trigger("thread:removed", thread);
}; };
DiscussionThreadListView.prototype.setActiveThread = function(thread_id) { DiscussionThreadListView.prototype.setActiveThread = function(thread_id) {
...@@ -406,7 +416,7 @@ ...@@ -406,7 +416,7 @@
}; };
DiscussionThreadListView.prototype.goHome = function() { DiscussionThreadListView.prototype.goHome = function() {
var thread_id, url; var url;
this.template = _.template($("#discussion-home-template").html()); this.template = _.template($("#discussion-home-template").html());
$(".forum-content").html(this.template); $(".forum-content").html(this.template);
$(".forum-nav-thread-list a").removeClass("is-active").find(".sr").remove(); $(".forum-nav-thread-list a").removeClass("is-active").find(".sr").remove();
...@@ -416,19 +426,9 @@ ...@@ -416,19 +426,9 @@
url: url, url: url,
type: "GET", type: "GET",
success: function(response) { success: function(response) {
if (response.status) { $('input.email-setting').prop('checked', response.status);
return $('input.email-setting').attr('checked', 'checked');
} else {
return $('input.email-setting').removeAttr('checked');
}
} }
}); });
thread_id = null;
return this.trigger("thread:removed");
/*
select all threads
*/
}; };
DiscussionThreadListView.prototype.isBrowseMenuVisible = function() { DiscussionThreadListView.prototype.isBrowseMenuVisible = function() {
...@@ -731,8 +731,7 @@ ...@@ -731,8 +731,7 @@
url: DiscussionUtil.urlFor("users"), url: DiscussionUtil.urlFor("users"),
type: "GET", type: "GET",
dataType: 'json', dataType: 'json',
error: function() { error: function() {},
},
success: function(response) { success: function(response) {
var message; var message;
if (response.users.length > 0) { if (response.users.length > 0) {
...@@ -742,7 +741,7 @@ ...@@ -742,7 +741,7 @@
username: response.users[0].username username: response.users[0].username
}) })
}, true); }, true);
return self.addSearchAlert(message); return self.addSearchAlert(message, 'search-by-user');
} }
} }
}); });
...@@ -765,23 +764,17 @@ ...@@ -765,23 +764,17 @@
}; };
DiscussionThreadListView.prototype.updateEmailNotifications = function() { DiscussionThreadListView.prototype.updateEmailNotifications = function() {
if ($('input.email-setting').attr('checked')) { var checkbox, checked, urlName;
return DiscussionUtil.safeAjax({ checkbox = $('input.email-setting');
url: DiscussionUtil.urlFor("enable_notifications"), checked = checkbox.prop('checked');
type: "POST", urlName = (checked) ? "enable_notifications" : "disable_notifications";
error: function() { DiscussionUtil.safeAjax({
return $('input.email-setting').removeAttr('checked'); url: DiscussionUtil.urlFor(urlName),
} type: "POST",
}); error: function() {
} else { checkbox.prop('checked', !checked);
return DiscussionUtil.safeAjax({ }
url: DiscussionUtil.urlFor("disable_notifications"), });
type: "POST",
error: function() {
return $('input.email-setting').attr('checked', 'checked');
}
});
}
}; };
return DiscussionThreadListView; return DiscussionThreadListView;
......
...@@ -91,6 +91,7 @@ ...@@ -91,6 +91,7 @@
id = self.model.get("id"); id = self.model.get("id");
if (collection.get(id)) { if (collection.get(id)) {
self.model = collection.get(id); self.model = collection.get(id);
self.rerender();
} }
}); });
this.createShowView(); this.createShowView();
......
...@@ -3,9 +3,10 @@ ...@@ -3,9 +3,10 @@
'use strict'; 'use strict';
describe('DiscussionUtil', function() { describe('DiscussionUtil', function() {
beforeEach(function() { beforeEach(function() {
return DiscussionSpecHelper.setUpGlobals(); DiscussionSpecHelper.setUpGlobals();
}); });
return describe("updateWithUndo", function() {
describe("updateWithUndo", function() {
it("calls through to safeAjax with correct params, and reverts the model in case of failure", function() { it("calls through to safeAjax with correct params, and reverts the model in case of failure", function() {
var deferred, model, res, updates; var deferred, model, res, updates;
deferred = $.Deferred(); deferred = $.Deferred();
...@@ -45,13 +46,13 @@ ...@@ -45,13 +46,13 @@
updates = { updates = {
hello: "world" hello: "world"
}; };
$elem = jasmine.createSpyObj('$elem', ['attr']); $elem = jasmine.createSpyObj('$elem', ['prop']);
$elem.attr.and.returnValue(true); $elem.prop.and.returnValue(true);
res = DiscussionUtil.updateWithUndo(model, updates, { res = DiscussionUtil.updateWithUndo(model, updates, {
foo: "bar", foo: "bar",
$elem: $elem $elem: $elem
}, "error message"); }, "error message");
expect($elem.attr).toHaveBeenCalledWith("disabled"); expect($elem.prop).toHaveBeenCalledWith("disabled");
expect(DiscussionUtil.safeAjax).toHaveBeenCalled(); expect(DiscussionUtil.safeAjax).toHaveBeenCalled();
expect(model.attributes).toEqual({ expect(model.attributes).toEqual({
hello: false, hello: false,
...@@ -64,6 +65,30 @@ ...@@ -64,6 +65,30 @@
return expect(failed).toBe(true); return expect(failed).toBe(true);
}); });
}); });
describe('safeAjax', function() {
function dismissAlert() {
$(".modal#discussion-alert").remove();
}
it('respects global beforeSend', function() {
var beforeSendSpy = jasmine.createSpy();
$.ajaxSetup({beforeSend: beforeSendSpy});
var $elem = jasmine.createSpyObj('$elem', ['prop']);
DiscussionUtil.safeAjax({
$elem: $elem,
url: "/",
type: "GET",
dataType: "json"
}).always(function() {
dismissAlert();
});
expect($elem.prop).toHaveBeenCalledWith("disabled", true);
expect(beforeSendSpy).toHaveBeenCalled();
});
});
}); });
}).call(this); }).call(this);
...@@ -261,7 +261,7 @@ ...@@ -261,7 +261,7 @@
}); });
}); });
describe("search alerts", function() { describe("search alerts", function() {
var testAlertMessages; var testAlertMessages, getAlertMessagesAndClasses;
testAlertMessages = function(expectedMessages) { testAlertMessages = function(expectedMessages) {
return expect($(".search-alert .message").map(function() { return expect($(".search-alert .message").map(function() {
...@@ -269,6 +269,15 @@ ...@@ -269,6 +269,15 @@
}).get()).toEqual(expectedMessages); }).get()).toEqual(expectedMessages);
}; };
getAlertMessagesAndClasses = function () {
return $(".search-alert").map(function() {
return {
text: $('.message', this).html(),
css_class: $(this).attr('class')
};
}).get();
};
it("renders and removes search alerts", function() { it("renders and removes search alerts", function() {
var bar, foo; var bar, foo;
testAlertMessages([]); testAlertMessages([]);
...@@ -282,6 +291,27 @@ ...@@ -282,6 +291,27 @@
return testAlertMessages([]); return testAlertMessages([]);
}); });
it("renders search alert with custom class", function() {
var foo, messages;
testAlertMessages([]);
this.view.addSearchAlert("foo", "custom-class");
messages = getAlertMessagesAndClasses();
expect(messages.length).toEqual(1);
expect(messages[0].text).toEqual("foo");
expect(messages[0].css_class).toEqual("search-alert custom-class");
foo = this.view.addSearchAlert("bar", "other-class");
messages = getAlertMessagesAndClasses();
expect(messages.length).toEqual(2);
expect(messages[0].text).toEqual("foo");
expect(messages[0].css_class).toEqual("search-alert custom-class");
expect(messages[1].text).toEqual("bar");
expect(messages[1].css_class).toEqual("search-alert other-class");
});
it("clears all search alerts", function() { it("clears all search alerts", function() {
this.view.addSearchAlert("foo"); this.view.addSearchAlert("foo");
this.view.addSearchAlert("bar"); this.view.addSearchAlert("bar");
......
...@@ -94,7 +94,7 @@ ...@@ -94,7 +94,7 @@
this.view.render(); this.view.render();
expectedGroupId = null; expectedGroupId = null;
DiscussionSpecHelper.makeAjaxSpy(function(params) { DiscussionSpecHelper.makeAjaxSpy(function(params) {
return expect(params.data.group_id).toEqual(expectedGroupId); expect(params.data.group_id).toEqual(expectedGroupId);
}); });
return _.each(["1", "2", ""], function(groupIdStr) { return _.each(["1", "2", ""], function(groupIdStr) {
expectedGroupId = groupIdStr; expectedGroupId = groupIdStr;
...@@ -103,6 +103,7 @@ ...@@ -103,6 +103,7 @@
self.view.$(".js-post-body textarea").val("dummy body"); self.view.$(".js-post-body textarea").val("dummy body");
self.view.$(".forum-new-post-form").submit(); self.view.$(".forum-new-post-form").submit();
expect($.ajax).toHaveBeenCalled(); expect($.ajax).toHaveBeenCalled();
self.view.$(".forum-new-post-form").prop("disabled", false);
return $.ajax.calls.reset(); return $.ajax.calls.reset();
}); });
}); });
......
...@@ -33,8 +33,10 @@ ...@@ -33,8 +33,10 @@
} }
}); });
this.event = DiscussionSpecHelper.makeEventSpy(); this.event = DiscussionSpecHelper.makeEventSpy();
this.event.target = $("body");
spyOn(this.comment, "remove"); spyOn(this.comment, "remove");
return spyOn(this.view.$el, "remove"); spyOn(this.view.$el, "remove");
$(this.event.target).prop("disabled", false);
}); });
setAjaxResult = function(isSuccess) { setAjaxResult = function(isSuccess) {
return spyOn($, "ajax").and.callFake(function(params) { return spyOn($, "ajax").and.callFake(function(params) {
...@@ -151,7 +153,7 @@ ...@@ -151,7 +153,7 @@
this.view.$el.find(".edit-comment-body").html($("<textarea></textarea>")); this.view.$el.find(".edit-comment-body").html($("<textarea></textarea>"));
this.view.$el.find(".edit-comment-body textarea").val(this.updatedBody); this.view.$el.find(".edit-comment-body textarea").val(this.updatedBody);
spyOn(this.view, 'cancelEdit'); spyOn(this.view, 'cancelEdit');
return spyOn($, "ajax").and.callFake(function(params) { spyOn($, "ajax").and.callFake(function(params) {
if (self.ajaxSucceed) { if (self.ajaxSucceed) {
params.success(); params.success();
} else { } else {
...@@ -164,10 +166,17 @@ ...@@ -164,10 +166,17 @@
} }
}; };
}); });
this.event = DiscussionSpecHelper.makeEventSpy();
// All the way down in discussion/utils.js there's this line
// element.after(...);
// element is event.target in this case. This causes a JS exception, so we override the target
this.event.target = $("body");
$(this.event.target).prop("disabled", false);
}); });
it('calls the update endpoint correctly and displays the show view on success', function() { it('calls the update endpoint correctly and displays the show view on success', function() {
this.ajaxSucceed = true; this.ajaxSucceed = true;
this.view.update(DiscussionSpecHelper.makeEventSpy()); this.view.update(this.event);
expect($.ajax).toHaveBeenCalled(); expect($.ajax).toHaveBeenCalled();
expect($.ajax.calls.mostRecent().args[0].url._parts.path) expect($.ajax.calls.mostRecent().args[0].url._parts.path)
.toEqual('/courses/edX/999/test/discussion/comments/01234567/update'); .toEqual('/courses/edX/999/test/discussion/comments/01234567/update');
...@@ -179,7 +188,7 @@ ...@@ -179,7 +188,7 @@
var originalBody; var originalBody;
originalBody = this.comment.get("body"); originalBody = this.comment.get("body");
this.ajaxSucceed = false; this.ajaxSucceed = false;
this.view.update(DiscussionSpecHelper.makeEventSpy()); this.view.update(this.event);
expect($.ajax).toHaveBeenCalled(); expect($.ajax).toHaveBeenCalled();
expect($.ajax.calls.mostRecent().args[0].url._parts.path) expect($.ajax.calls.mostRecent().args[0].url._parts.path)
.toEqual('/courses/edX/999/test/discussion/comments/01234567/update'); .toEqual('/courses/edX/999/test/discussion/comments/01234567/update');
......
<div class="search-alert" id="search-alert-<%- cid %>"> <div class="search-alert <%= css_class %>" id="search-alert-<%- cid %>">
<div class="search-alert-content"> <div class="search-alert-content">
<p class="message"><%= message %></p> <p class="message"><%= message %></p>
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment