summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIvan Sein <ivan@struktur.de>2017-11-03 13:03:39 +0100
committerGitHub <noreply@github.com>2017-11-03 13:03:39 +0100
commitd83c3d250eb249f3308b7fc7925272ddf756e8ef (patch)
treee79fef5ab96fdd00ad634ba96c38b0d430b5971d
parent23aaa401ef0ec6fcd9a5910a824d2f66e4f5e1cd (diff)
parent46ce6aae1e767091d819c53b116d05425ec16226 (diff)
Merge pull request #462 from nextcloud/add-basic-frontend-for-chat
Add basic frontend for chat
-rw-r--r--css/comments.css184
-rw-r--r--js/app.js16
-rw-r--r--js/models/chatmessage.js81
-rw-r--r--js/models/chatmessagecollection.js194
-rw-r--r--js/views/chatview.js268
-rw-r--r--templates/index-public.php4
-rw-r--r--templates/index.php4
7 files changed, 751 insertions, 0 deletions
diff --git a/css/comments.css b/css/comments.css
new file mode 100644
index 000000000..9bdd77423
--- /dev/null
+++ b/css/comments.css
@@ -0,0 +1,184 @@
+/*
+ * Copyright (c) 2016
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+#commentsTabView .emptycontent {
+ margin-top: 0;
+}
+
+#commentsTabView .newCommentForm {
+ position: relative;
+ margin-bottom: 20px;
+}
+
+#commentsTabView .newCommentForm .message {
+ width: calc(100% - 81px); /* 36 (left margin) + 30 (right padding) + 15 (right padding of surrounding box) */
+ margin-left: 36px;
+ padding-right: 30px;
+ display: block;
+}
+
+#commentsTabView .newCommentForm .submit {
+ position: absolute;
+ bottom: 0px;
+ right: 8px;
+ width: 30px;
+ margin: 0;
+ padding: 7px 9px;
+ background-color: transparent;
+ border: none;
+ opacity: .3;
+}
+#commentsTabView .newCommentForm .submit:hover,
+#commentsTabView .newCommentForm .submit:focus {
+ opacity: 1;
+}
+
+#commentsTabView .newCommentForm .submitLoading {
+ background-position: left;
+}
+
+#commentsTabView .newCommentForm .cancel {
+ margin-right: 6px;
+}
+
+#commentsTabView .newCommentForm div.message {
+ resize: none;
+}
+
+#commentsTabView .newCommentForm div.message:empty:before {
+ content: attr(data-placeholder);
+ color: grey;
+}
+
+#commentsTabView .comment {
+ position: relative;
+ margin-bottom: 30px;
+}
+
+#commentsTabView .comment .avatar,
+.atwho-view-ul * .avatar{
+ width: 32px;
+ height: 32px;
+ line-height: 32px;
+}
+
+#commentsTabView .comment .message .avatar,
+.atwho-view-ul * .avatar
+{
+ display: inline-block;
+}
+
+#activityTabView li.comment.collapsed .activitymessage,
+#commentsTabView .comment.collapsed .message {
+ white-space: pre-wrap;
+}
+
+#activityTabView li.comment.collapsed .activitymessage,
+#commentsTabView .comment.collapsed .message {
+ max-height: 70px;
+ overflow: hidden;
+}
+
+#activityTabView li.comment .message-overlay,
+#commentsTabView .comment .message-overlay {
+ display: none;
+}
+
+#activityTabView li.comment.collapsed .message-overlay,
+#commentsTabView .comment.collapsed .message-overlay {
+ display: block;
+ position: absolute;
+ z-index: 2;
+ height: 50px;
+ pointer-events: none;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: -moz-linear-gradient(rgba(255,255,255,0), rgba(255,255,255,1));
+ background: -webkit-linear-gradient(rgba(255,255,255,0), rgba(255,255,255,1));
+ background: -o-linear-gradient(rgba(255,255,255,0), rgba(255,255,255,1));
+ background: -ms-linear-gradient(rgba(255,255,255,0), rgba(255,255,255,1));
+ background: linear-gradient(rgba(255,255,255,0), rgba(255,255,255,1));
+ filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='#00FFFFFF', endColorstr='#FFFFFFFF');
+ background-repeat: no-repeat;
+}
+
+#commentsTabView .authorRow>div {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+#commentsTabView .authorRow>div.hidden {
+ display: none !important;
+}
+
+#commentsTabView .comments li .message .avatar-name-wrapper,
+.atwho-view-ul * .avatar-name-wrapper,
+#commentsTabView .comment .authorRow {
+ position: relative;
+}
+
+.atwho-view-ul * .avatar-name-wrapper {
+ white-space: nowrap;
+}
+
+#commentsTabView .comment .author,
+#commentsTabView .comment .date {
+ opacity: .5;
+}
+#commentsTabView .comment .author {
+ margin-left: 5px;
+}
+#commentsTabView .comment .date {
+ position: absolute;
+ right: 0;
+ top: 5px;
+}
+
+#commentsTabView .comments li .message {
+ padding-left: 40px;
+}
+
+#commentsTabView .comment .action {
+ opacity: 0;
+ vertical-align: middle;
+ display: inline-block;
+}
+
+#commentsTabView .comment:hover .action {
+ opacity: 0.3;
+}
+
+#commentsTabView .comment .action:hover {
+ opacity: 1;
+}
+
+#commentsTabView .comment .action.delete {
+ position: absolute;
+ right: 0;
+}
+
+#commentsTabView .comment.disabled {
+ opacity: 0.3;
+}
+
+#commentsTabView .comment.disabled .action {
+ visibility: hidden;
+}
+
+#commentsTabView .message.error {
+ color: #e9322d;
+ border-color: #e9322d;
+ box-shadow: 0 0 6px #f8b9b7;
+}
+
+.app-files .action-comment {
+ padding: 16px 14px;
+}
diff --git a/js/app.js b/js/app.js
index d01ca287e..c06f74ade 100644
--- a/js/app.js
+++ b/js/app.js
@@ -391,6 +391,9 @@
guestNameModel: this._localStorageModel
});
this._sidebarView.setCallInfoView(callInfoView);
+
+ this._messageCollection.setRoomToken(this.activeRoom.get('token'));
+ this._messageCollection.receiveMessages();
},
setPageTitle: function(title){
if (title) {
@@ -472,6 +475,19 @@
this.disable();
});
+ this._messageCollection = new OCA.SpreedMe.Models.ChatMessageCollection(null, {token: null});
+ this._chatView = new OCA.SpreedMe.Views.ChatView({
+ collection: this._messageCollection,
+ id: 'commentsTabView',
+ className: 'chat tab'
+ });
+
+ this._sidebarView.addTab('chat', { label: t('spreed', 'Chat') }, this._chatView);
+
+ this._messageCollection.listenTo(roomChannel, 'leaveCurrentCall', function() {
+ this.stopReceivingMessages();
+ });
+
$(document).on('click', this.onDocumentClick);
OC.Util.History.addOnPopStateHandler(_.bind(this._onPopState, this));
},
diff --git a/js/models/chatmessage.js b/js/models/chatmessage.js
new file mode 100644
index 000000000..2449be730
--- /dev/null
+++ b/js/models/chatmessage.js
@@ -0,0 +1,81 @@
+/* global Backbone, OC, OCA */
+
+/**
+ *
+ * @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com)
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+(function(OCA, OC, Backbone) {
+ 'use strict';
+
+ OCA.SpreedMe = OCA.SpreedMe || {};
+ OCA.SpreedMe.Models = OCA.SpreedMe.Models || {};
+
+ /**
+ * Model for chat messages.
+ *
+ * ChatMessage can be used as the model of a ChatMessageCollection or as a
+ * standalone model. When used as a standalone model the room token must be
+ * provided in the constructor options (as "token").
+ *
+ * In any case, "create" is the only synchronization method allowed; chat
+ * messages can not be edited nor deleted, and they can not be got
+ * individually either, but as a list through ChatMessageCollection.
+ *
+ * To send a new message create a standalone ChatMessage object and call
+ * "save".
+ */
+ var ChatMessage = Backbone.Model.extend({
+
+ defaults: {
+ actorType: '',
+ actorId: '',
+ actorDisplayName: '',
+ timestamp: 0,
+ message: ''
+ },
+
+ url: function() {
+ if (this.token === undefined) {
+ throw 'Missing parameter token';
+ }
+
+ return OC.linkToOCS('apps/spreed/api/v1/chat', 2) + this.token;
+ },
+
+ initialize: function(options) {
+ // Only needed in standalone mode; when used as the model of a
+ // ChatMessageCollection the synchronization is performed by the
+ // collection instead.
+ this.token = options.token;
+ },
+
+ sync: function(method, model, options) {
+ if (method !== 'create') {
+ throw 'Synchronization method not supported by ChatMessage: ' + method;
+ }
+
+ return Backbone.Model.prototype.sync.call(this, method, model, options);
+ }
+
+ });
+
+ OCA.SpreedMe.Models.ChatMessage = ChatMessage;
+
+})(OCA, OC, Backbone);
diff --git a/js/models/chatmessagecollection.js b/js/models/chatmessagecollection.js
new file mode 100644
index 000000000..9919e7543
--- /dev/null
+++ b/js/models/chatmessagecollection.js
@@ -0,0 +1,194 @@
+/* global Backbone, OC, OCA */
+
+/**
+ *
+ * @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com)
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+(function(OCA, OC, Backbone) {
+ 'use strict';
+
+ OCA.SpreedMe = OCA.SpreedMe || {};
+ OCA.SpreedMe.Models = OCA.SpreedMe.Models || {};
+
+ /**
+ * Collection for chat messages.
+ *
+ * The ChatMessageCollection gives read access to all the chat messages from
+ * a specific chat room. The room token must be provided in the constructor
+ * options (as "token"), either as an actual room token or as null. It is
+ * possible to change the room of a ChatMessageCollection at any time by
+ * calling "setRoomToken". In any case, although null is supported as a
+ * temporal or reset value, note that an actual room token must be set
+ * before synchronizing the collection.
+ *
+ * "read" is the only synchronization method allowed; chat messages can not
+ * be edited nor deleted, and to send a new message a standalone ChatMessage
+ * should be used instead.
+ *
+ * To get the messages from the server "receiveMessages" should be used. It
+ * will enable polling to the server and automatically update the collection
+ * when new messages are received. Once enabled, the polling will go on
+ * indefinitely. Due to this "stopReceivingMessages" must be called once
+ * the ChatMessageCollection is no longer needed.
+ */
+ var ChatMessageCollection = Backbone.Collection.extend({
+
+ model: OCA.SpreedMe.Models.ChatMessage,
+
+ initialize: function(models, options) {
+ if (options.token === undefined) {
+ throw 'Missing parameter token';
+ }
+
+ this._lastFetch = null;
+
+ this.setRoomToken(options.token);
+ },
+
+ parse: function(result) {
+ return result.ocs.data;
+ },
+
+ set: function(models, options) {
+ // The server returns the messages sorted from newest to oldest,
+ // which causes some issues with the default implementation of
+ // collections. If several messages were received at once they would
+ // be added to the collection in that same order, so the newest
+ // message would be the first one added and the oldest message would
+ // be the last one added (and the model id of the newest one would
+ // be lower than the model id of the oldest one). If another group
+ // of messages were received now then the newest message would be
+ // added to the collection after the oldest message from the
+ // previous group. Therefore, the models in the collection would not
+ // follow an absolute order from the newest message to the oldest
+ // one, but a local order for each group of messages fetched.
+ //
+ // Just sorting the collection is not a solution either. Setting
+ // "sort: true" as a fetch option would keep the collection sorted
+ // (although the ids of the models would still have the same problem
+ // described above), but the "add" events would be triggered anyway
+ // in the original order of the messages passed to "set".
+ //
+ // The best solution, besides changing the server to return the
+ // messages sorted from oldest to newest, is to sort the models
+ // passed to "set" from oldest to newest.
+ if (models !== undefined && models !== null && models.ocs !== undefined && models.ocs.data !== undefined) {
+ models.ocs.data = _.sortBy(models.ocs.data, function(model) {
+ return model.timestamp;
+ });
+ }
+
+ return Backbone.Collection.prototype.set.call(this, models, options);
+ },
+
+ /**
+ * Changes the room that this ChatMessageCollection gets its messages
+ * from.
+ *
+ * When a token is set this collection is reset, so the messages from
+ * the previous room are removed.
+ *
+ * If polling was currently being done to the previous room it will be
+ * automatically stopped. Note, however, that "receiveMessages" must be
+ * explicitly called if needed.
+ *
+ * @param string|null token the token of the room.
+ */
+ setRoomToken: function(token) {
+ this.stopReceivingMessages();
+
+ this.token = token;
+
+ this.offset = 0;
+
+ this._waitTimeUntilRetry = 1;
+
+ if (token !== null) {
+ this.url = OC.linkToOCS('apps/spreed/api/v1/chat', 2) + token;
+ } else {
+ this.url = null;
+ }
+
+ this.reset();
+ },
+
+ receiveMessages: function() {
+ this.receiveMessagesAgain = true;
+
+ this._lastFetch = this.fetch({
+ data: {
+ // The notOlderThan parameter could be used to limit the
+ // messages to those shown since the user opened the chat
+ // window. However, it can not be used as a way to keep
+ // track of the last message received. For example, even if
+ // unlikely, if two messages were sent at the same time and
+ // received the same timestamp in two different PHP
+ // processes, it could happen that one of them was committed
+ // to the database and read by another process waiting for
+ // new messages while the second message was not committed
+ // yet and thus not returned. Then, when the reading process
+ // checks the messages again, it would miss the second one
+ // due to its timestamp being the same as the last one it
+ // received.
+ offset: this.offset
+ },
+ success: _.bind(this._successfulFetch, this),
+ error: _.bind(this._failedFetch, this)
+ });
+ },
+
+ stopReceivingMessages: function() {
+ this.receiveMessagesAgain = false;
+
+ if (this._lastFetch !== null) {
+ this._lastFetch.abort();
+ }
+ },
+
+ _successfulFetch: function(collection, response) {
+ this.offset += response.ocs.data.length;
+
+ this._lastFetch = null;
+
+ this._waitTimeUntilRetry = 1;
+
+ if (this.receiveMessagesAgain) {
+ this.receiveMessages();
+ }
+ },
+
+ _failedFetch: function() {
+ this._lastFetch = null;
+
+ if (this.receiveMessagesAgain) {
+ _.delay(_.bind(this.receiveMessages, this), this._waitTimeUntilRetry * 1000);
+
+ // Increase the wait time until retry to at most 64 seconds.
+ if (this._waitTimeUntilRetry < 64) {
+ this._waitTimeUntilRetry *= 2;
+ }
+ }
+ }
+
+ });
+
+ OCA.SpreedMe.Models.ChatMessageCollection = ChatMessageCollection;
+
+})(OCA, OC, Backbone);
diff --git a/js/views/chatview.js b/js/views/chatview.js
new file mode 100644
index 000000000..5e1c9ff4a
--- /dev/null
+++ b/js/views/chatview.js
@@ -0,0 +1,268 @@
+/* global autosize, Backbone, Handlebars, OC, OCA */
+
+/**
+ *
+ * @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com)
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+(function(OCA, OC, Backbone, Handlebars, autosize) {
+ 'use strict';
+
+ OCA.SpreedMe = OCA.SpreedMe || {};
+ OCA.SpreedMe.Views = OCA.SpreedMe.Views || {};
+
+ var TEMPLATE =
+ '<ul class="comments">' +
+ '</ul>' +
+ '<div class="emptycontent"><div class="icon-comment"></div>' +
+ '<p>{{emptyResultLabel}}</p></div>' +
+ '<div class="loading hidden" style="height: 50px"></div>';
+
+ var ADD_COMMENT_TEMPLATE =
+ '<div class="newCommentRow comment" data-id="{{id}}">' +
+ ' <div class="authorRow">' +
+ ' <div class="avatar" data-username="{{actorId}}"></div>' +
+ ' <div class="author">{{actorDisplayName}}</div>' +
+ ' </div>' +
+ ' <form class="newCommentForm">' +
+ ' <div contentEditable="true" class="message" data-placeholder="{{newMessagePlaceholder}}">{{message}}</div>' +
+ ' <input class="submit icon-confirm" type="submit" value="" />' +
+ ' <div class="submitLoading icon-loading-small hidden"></div>'+
+ ' </form>' +
+ '</div>';
+
+ var COMMENT_TEMPLATE =
+ '<li class="comment" data-id="{{id}}">' +
+ ' <div class="authorRow">' +
+ ' <div class="avatar" {{#if actorId}}data-username="{{actorId}}"{{/if}}> </div>' +
+ ' <div class="author">{{actorDisplayName}}</div>' +
+ ' <div class="date has-tooltip live-relative-timestamp" data-timestamp="{{timestamp}}" title="{{altDate}}">{{date}}</div>' +
+ ' </div>' +
+ ' <div class="message">{{{formattedMessage}}}</div>' +
+ '</li>';
+
+ var ChatView = Backbone.View.extend({
+
+ events: {
+ 'submit .newCommentForm': '_onSubmitComment',
+ },
+
+ initialize: function() {
+ this.listenTo(this.collection, 'reset', this.render);
+ this.listenTo(this.collection, 'add', this._onAddModel);
+ },
+
+ template: function(params) {
+ if (!this._template) {
+ this._template = Handlebars.compile(TEMPLATE);
+ }
+ return this._template(params);
+ },
+
+ addCommentTemplate: function(params) {
+ if (!this._addCommentTemplate) {
+ this._addCommentTemplate = Handlebars.compile(A