summaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorThomas Müller <thomas.mueller@tmit.eu>2016-02-03 20:53:40 +0100
committerThomas Müller <thomas.mueller@tmit.eu>2016-02-03 20:53:40 +0100
commitda0462015507053c4424f62c32a4a182301fae22 (patch)
tree8ea008a05a11719ab042eedd72c9dacec997eef2 /apps
parentdd2f62e3e479a9e9aa8a624d8961d8645c8c1aad (diff)
parent85bec3ffcb0e2c948c57cee2817307af2826d847 (diff)
downloadnextcloud-server-da0462015507053c4424f62c32a4a182301fae22.tar.gz
nextcloud-server-da0462015507053c4424f62c32a4a182301fae22.zip
Merge pull request #22101 from owncloud/comments-filerow
Add file row indicator for unread comments
Diffstat (limited to 'apps')
-rw-r--r--apps/comments/appinfo/app.php1
-rw-r--r--apps/comments/css/comments.css4
-rw-r--r--apps/comments/js/commentcollection.js61
-rw-r--r--apps/comments/js/commentmodel.js5
-rw-r--r--apps/comments/js/commentstabview.js27
-rw-r--r--apps/comments/js/commentsummarymodel.js65
-rw-r--r--apps/comments/js/filesplugin.js84
-rw-r--r--apps/comments/tests/js/commentscollectionSpec.js44
-rw-r--r--apps/comments/tests/js/commentstabviewSpec.js42
-rw-r--r--apps/comments/tests/js/filespluginSpec.js102
10 files changed, 424 insertions, 11 deletions
diff --git a/apps/comments/appinfo/app.php b/apps/comments/appinfo/app.php
index c6f36567c51..a1eb4f6899d 100644
--- a/apps/comments/appinfo/app.php
+++ b/apps/comments/appinfo/app.php
@@ -27,6 +27,7 @@ $eventDispatcher->addListener(
\OCP\Util::addScript('comments', 'app');
\OCP\Util::addScript('comments', 'commentmodel');
\OCP\Util::addScript('comments', 'commentcollection');
+ \OCP\Util::addScript('comments', 'commentsummarymodel');
\OCP\Util::addScript('comments', 'commentstabview');
\OCP\Util::addScript('comments', 'filesplugin');
\OCP\Util::addStyle('comments', 'comments');
diff --git a/apps/comments/css/comments.css b/apps/comments/css/comments.css
index c1624dcc57b..5e247aaeb71 100644
--- a/apps/comments/css/comments.css
+++ b/apps/comments/css/comments.css
@@ -49,3 +49,7 @@
position: absolute;
right: 0;
}
+
+.app-files .action-comment>img {
+ margin-right: 5px;
+}
diff --git a/apps/comments/js/commentcollection.js b/apps/comments/js/commentcollection.js
index d10e5e00865..a15039cf484 100644
--- a/apps/comments/js/commentcollection.js
+++ b/apps/comments/js/commentcollection.js
@@ -10,8 +10,6 @@
(function(OC, OCA) {
- var NS_OWNCLOUD = 'http://owncloud.org/ns';
-
/**
* @class OCA.Comments.CommentCollection
* @classdesc
@@ -26,12 +24,40 @@
model: OCA.Comments.CommentModel,
+ /**
+ * Object type
+ *
+ * @type string
+ */
_objectType: 'files',
+
+ /**
+ * Object id
+ *
+ * @type string
+ */
_objectId: null,
+ /**
+ * True if there are no more page results left to fetch
+ *
+ * @type bool
+ */
_endReached: false,
+
+ /**
+ * Number of comments to fetch per page
+ *
+ * @type int
+ */
_limit : 20,
+ /**
+ * Initializes the collection
+ *
+ * @param {string} [options.objectType] object type
+ * @param {string} [options.objectId] object id
+ */
initialize: function(models, options) {
options = options || {};
if (options.objectType) {
@@ -58,6 +84,7 @@
reset: function() {
this._endReached = false;
+ this._summaryModel = null;
return OC.Backbone.Collection.prototype.reset.apply(this, arguments);
},
@@ -81,6 +108,7 @@
var success = options.success;
options = _.extend({
remove: false,
+ parse: true,
data: body,
davProperties: CommentCollection.prototype.model.prototype.davProperties,
success: function(resp) {
@@ -102,6 +130,35 @@
}, options);
return this.sync('REPORT', this, options);
+ },
+
+ /**
+ * Returns the matching summary model
+ *
+ * @return {OCA.Comments.CommentSummaryModel} summary model
+ */
+ getSummaryModel: function() {
+ if (!this._summaryModel) {
+ this._summaryModel = new OCA.Comments.CommentSummaryModel({
+ id: this._objectId,
+ objectType: this._objectType
+ });
+ }
+ return this._summaryModel;
+ },
+
+ /**
+ * Updates the read marker for this comment thread
+ *
+ * @param {Date} [date] optional date, defaults to now
+ * @param {Object} [options] backbone options
+ */
+ updateReadMarker: function(date, options) {
+ options = options || {};
+
+ return this.getSummaryModel().save({
+ readMarker: (date || new Date()).toUTCString()
+ }, options);
}
});
diff --git a/apps/comments/js/commentmodel.js b/apps/comments/js/commentmodel.js
index b945f71fdd2..ba04fd61de3 100644
--- a/apps/comments/js/commentmodel.js
+++ b/apps/comments/js/commentmodel.js
@@ -34,11 +34,12 @@
'actorDisplayName': '{' + NS_OWNCLOUD + '}actorDisplayName',
'creationDateTime': '{' + NS_OWNCLOUD + '}creationDateTime',
'objectType': '{' + NS_OWNCLOUD + '}objectType',
- 'objectId': '{' + NS_OWNCLOUD + '}objectId'
+ 'objectId': '{' + NS_OWNCLOUD + '}objectId',
+ 'isUnread': '{' + NS_OWNCLOUD + '}isUnread'
},
parse: function(data) {
- // TODO: parse non-string values
+ data.isUnread = (data.isUnread === 'true');
return data;
}
});
diff --git a/apps/comments/js/commentstabview.js b/apps/comments/js/commentstabview.js
index 463ac2d76ef..188d8c5943c 100644
--- a/apps/comments/js/commentstabview.js
+++ b/apps/comments/js/commentstabview.js
@@ -31,7 +31,7 @@
'<div class="loading hidden" style="height: 50px"></div>';
var COMMENT_TEMPLATE =
- '<li class="comment">' +
+ '<li class="comment{{#if isUnread}} unread{{/if}}" data-id="{{id}}">' +
' <div class="authorRow">' +
' {{#if avatarEnabled}}' +
' <div class="avatar" data-username="{{actorId}}"> </div>' +
@@ -97,12 +97,14 @@
setFileInfo: function(fileInfo) {
if (fileInfo) {
+ this.model = fileInfo;
this.render();
this.collection.setObjectId(fileInfo.id);
// reset to first page
this.collection.reset([], {silent: true});
this.nextPage();
} else {
+ this.model = null;
this.render();
this.collection.reset();
}
@@ -139,10 +141,29 @@
this.$el.find('.showMore').addClass('hidden');
},
- _onEndRequest: function() {
+ _onEndRequest: function(type) {
+ var fileInfoModel = this.model;
this._toggleLoading(false);
this.$el.find('.empty').toggleClass('hidden', !!this.collection.length);
this.$el.find('.showMore').toggleClass('hidden', !this.collection.hasMoreResults());
+
+ if (type !== 'REPORT') {
+ return;
+ }
+
+ // find first unread comment
+ var firstUnreadComment = this.collection.findWhere({isUnread: true});
+ if (firstUnreadComment) {
+ // update read marker
+ this.collection.updateReadMarker(
+ null,
+ {
+ success: function() {
+ fileInfoModel.set('commentsUnread', 0);
+ }
+ }
+ );
+ }
},
_onAddModel: function(model, collection, options) {
@@ -210,7 +231,7 @@
actorType: 'users',
verb: 'comment',
message: $textArea.val(),
- creationDateTime: (new Date()).getTime()
+ creationDateTime: (new Date()).toUTCString()
}, {
at: 0,
success: function() {
diff --git a/apps/comments/js/commentsummarymodel.js b/apps/comments/js/commentsummarymodel.js
new file mode 100644
index 00000000000..d405315ca1f
--- /dev/null
+++ b/apps/comments/js/commentsummarymodel.js
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2016
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function(OC, OCA) {
+ var NS_OWNCLOUD = 'http://owncloud.org/ns';
+ /**
+ * @class OCA.Comments.CommentSummaryModel
+ * @classdesc
+ *
+ * Model containing summary information related to comments
+ * like the read marker.
+ *
+ */
+ var CommentSummaryModel = OC.Backbone.Model.extend(
+ /** @lends OCA.Comments.CommentSummaryModel.prototype */ {
+ sync: OC.Backbone.davSync,
+
+ /**
+ * Object type
+ *
+ * @type string
+ */
+ _objectType: 'files',
+
+ /**
+ * Object id
+ *
+ * @type string
+ */
+ _objectId: null,
+
+ davProperties: {
+ 'readMarker': '{' + NS_OWNCLOUD + '}readMarker'
+ },
+
+ /**
+ * Initializes the summary model
+ *
+ * @param {string} [options.objectType] object type
+ * @param {string} [options.objectId] object id
+ */
+ initialize: function(attrs, options) {
+ options = options || {};
+ if (options.objectType) {
+ this._objectType = options.objectType;
+ }
+ },
+
+ url: function() {
+ return OC.linkToRemote('dav') + '/comments/' +
+ encodeURIComponent(this._objectType) + '/' +
+ encodeURIComponent(this.id) + '/';
+ }
+ });
+
+ OCA.Comments.CommentSummaryModel = CommentSummaryModel;
+})(OC, OCA);
+
diff --git a/apps/comments/js/filesplugin.js b/apps/comments/js/filesplugin.js
index c8d91e0ede3..bf6bb05146b 100644
--- a/apps/comments/js/filesplugin.js
+++ b/apps/comments/js/filesplugin.js
@@ -8,7 +8,15 @@
*
*/
+/* global Handlebars */
+
(function() {
+ var TEMPLATE_COMMENTS_UNREAD =
+ '<a class="action action-comment permanent" title="{{countMessage}}" href="#">' +
+ '<img class="svg" src="{{iconUrl}}"/>' +
+ '{{count}}' +
+ '</a>';
+
OCA.Comments = _.extend({}, OCA.Comments);
if (!OCA.Comments) {
/**
@@ -26,12 +34,88 @@
'favorites'
],
+ _formatCommentCount: function(count) {
+ if (!this._commentsUnreadTemplate) {
+ this._commentsUnreadTemplate = Handlebars.compile(TEMPLATE_COMMENTS_UNREAD);
+ }
+ return this._commentsUnreadTemplate({
+ count: count,
+ countMessage: t('comments', '{count} unread comments', {count: count}),
+ iconUrl: OC.imagePath('core', 'actions/comment')
+ });
+ },
+
attach: function(fileList) {
+ var self = this;
if (this.allowedLists.indexOf(fileList.id) < 0) {
return;
}
fileList.registerTabView(new OCA.Comments.CommentsTabView('commentsTabView'));
+
+ var NS_OC = 'http://owncloud.org/ns';
+
+ var oldGetWebdavProperties = fileList._getWebdavProperties;
+ fileList._getWebdavProperties = function() {
+ var props = oldGetWebdavProperties.apply(this, arguments);
+ props.push('{' + NS_OC + '}comments-unread');
+ return props;
+ };
+
+ fileList.filesClient.addFileInfoParser(function(response) {
+ var data = {};
+ var props = response.propStat[0].properties;
+ var commentsUnread = props['{' + NS_OC + '}comments-unread'];
+ if (!_.isUndefined(commentsUnread) && commentsUnread !== '') {
+ data.commentsUnread = parseInt(commentsUnread, 10);
+ }
+ return data;
+ });
+
+ fileList.$el.addClass('has-comments');
+ var oldCreateRow = fileList._createRow;
+ fileList._createRow = function(fileData) {
+ var $tr = oldCreateRow.apply(this, arguments);
+ if (fileData.commentsUnread) {
+ $tr.attr('data-comments-unread', fileData.commentsUnread);
+ }
+ return $tr;
+ };
+
+ // register "comment" action for reading comments
+ fileList.fileActions.registerAction({
+ name: 'Comment',
+ displayName: t('comments', 'Comment'),
+ mime: 'all',
+ permissions: OC.PERMISSION_READ,
+ type: OCA.Files.FileActions.TYPE_INLINE,
+ render: function(actionSpec, isDefault, context) {
+ var $file = context.$file;
+ var unreadComments = $file.data('comments-unread');
+ if (unreadComments) {
+ var $actionLink = $(self._formatCommentCount(unreadComments));
+ context.$file.find('a.name>span.fileactions').append($actionLink);
+ return $actionLink;
+ }
+ return '';
+ },
+ actionHandler: function(fileName, context) {
+ context.$file.find('.action-comment').tooltip('hide');
+ // open sidebar in comments section
+ context.fileList.showDetailsView(fileName, 'commentsTabView');
+ }
+ });
+
+ // add attribute to "elementToFile"
+ var oldElementToFile = fileList.elementToFile;
+ fileList.elementToFile = function($el) {
+ var fileInfo = oldElementToFile.apply(this, arguments);
+ var commentsUnread = $el.data('comments-unread');
+ if (commentsUnread) {
+ fileInfo.commentsUnread = commentsUnread;
+ }
+ return fileInfo;
+ };
}
};
diff --git a/apps/comments/tests/js/commentscollectionSpec.js b/apps/comments/tests/js/commentscollectionSpec.js
index 0dc68cc167c..2f41a272f67 100644
--- a/apps/comments/tests/js/commentscollectionSpec.js
+++ b/apps/comments/tests/js/commentscollectionSpec.js
@@ -100,5 +100,49 @@ describe('OCA.Comments.CommentCollection', function() {
expect(collection.hasMoreResults()).toEqual(true);
});
+ describe('resetting read marker', function() {
+ var updateStub;
+ var clock;
+
+ beforeEach(function() {
+ updateStub = sinon.stub(OCA.Comments.CommentSummaryModel.prototype, 'save');
+ clock = sinon.useFakeTimers(Date.UTC(2016, 1, 3, 10, 5, 9));
+ });
+ afterEach(function() {
+ updateStub.restore();
+ clock.restore();
+ });
+
+ it('resets read marker to the default date', function() {
+ var successStub = sinon.stub();
+ collection.updateReadMarker(null, {
+ success: successStub
+ });
+
+ expect(updateStub.calledOnce).toEqual(true);
+ expect(updateStub.lastCall.args[0]).toEqual({
+ readMarker: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString()
+ });
+
+ updateStub.yieldTo('success');
+
+ expect(successStub.calledOnce).toEqual(true);
+ });
+ it('resets read marker to the given date', function() {
+ var successStub = sinon.stub();
+ collection.updateReadMarker(new Date(Date.UTC(2016, 1, 2, 3, 4, 5)), {
+ success: successStub
+ });
+
+ expect(updateStub.calledOnce).toEqual(true);
+ expect(updateStub.lastCall.args[0]).toEqual({
+ readMarker: new Date(Date.UTC(2016, 1, 2, 3, 4, 5)).toUTCString()
+ });
+
+ updateStub.yieldTo('success');
+
+ expect(successStub.calledOnce).toEqual(true);
+ });
+ });
});
diff --git a/apps/comments/tests/js/commentstabviewSpec.js b/apps/comments/tests/js/commentstabviewSpec.js
index 0fb5eec0653..432fa5ddc4c 100644
--- a/apps/comments/tests/js/commentstabviewSpec.js
+++ b/apps/comments/tests/js/commentstabviewSpec.js
@@ -48,7 +48,7 @@ describe('OCA.Comments.CommentsTabView tests', function() {
objectType: 'files',
objectId: 5,
message: 'First',
- creationDateTime: Date.UTC(2016, 1, 3, 10, 5, 0)
+ creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 0)).toUTCString()
});
var comment2 = new OCA.Comments.CommentModel({
id: 2,
@@ -58,7 +58,7 @@ describe('OCA.Comments.CommentsTabView tests', function() {
objectType: 'files',
objectId: 5,
message: 'Second\nNewline',
- creationDateTime: Date.UTC(2016, 1, 3, 10, 0, 0)
+ creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 0, 0)).toUTCString()
});
testComments = [comment1, comment2];
@@ -142,7 +142,7 @@ describe('OCA.Comments.CommentsTabView tests', function() {
objectType: 'files',
objectId: 5,
message: 'Third',
- creationDateTime: Date.UTC(2016, 1, 3, 5, 0, 0)
+ creationDateTime: new Date(Date.UTC(2016, 1, 3, 5, 0, 0)).toUTCString()
});
view.collection.add(comment3);
@@ -184,7 +184,7 @@ describe('OCA.Comments.CommentsTabView tests', function() {
actorType: 'users',
verb: 'comment',
message: 'New message',
- creationDateTime: Date.UTC(2016, 1, 3, 10, 5, 9)
+ creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString()
});
});
it('does not create a comment if the field is empty', function() {
@@ -195,4 +195,38 @@ describe('OCA.Comments.CommentsTabView tests', function() {
});
});
+ describe('read marker', function() {
+ var updateMarkerStub;
+
+ beforeEach(function() {
+ updateMarkerStub = sinon.stub(OCA.Comments.CommentCollection.prototype, 'updateReadMarker');
+ });
+ afterEach(function() {
+ updateMarkerStub.restore();
+ });
+
+ it('resets the read marker after REPORT', function() {
+ testComments[0].set('isUnread', true, {silent: true});
+ testComments[1].set('isUnread', true, {silent: true});
+ view.collection.set(testComments);
+ view.collection.trigger('sync', 'REPORT');
+
+ expect(updateMarkerStub.calledOnce).toEqual(true);
+ expect(updateMarkerStub.lastCall.args[0]).toBeFalsy();
+ });
+ it('does not reset the read marker if there was no unread comments', function() {
+ view.collection.set(testComments);
+ view.collection.trigger('sync', 'REPORT');
+
+ expect(updateMarkerStub.notCalled).toEqual(true);
+ });
+ it('does not reset the read marker when posting comments', function() {
+ testComments[0].set('isUnread', true, {silent: true});
+ testComments[1].set('isUnread', true, {silent: true});
+ view.collection.set(testComments);
+ view.collection.trigger('sync', 'POST');
+
+ expect(updateMarkerStub.notCalled).toEqual(true);
+ });
+ });
});
diff --git a/apps/comments/tests/js/filespluginSpec.js b/apps/comments/tests/js/filespluginSpec.js
new file mode 100644
index 00000000000..78becc5af09
--- /dev/null
+++ b/apps/comments/tests/js/filespluginSpec.js
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2016 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+describe('OCA.Comments.FilesPlugin tests', function() {
+ var fileList;
+ var testFiles;
+
+ beforeEach(function() {
+ var $content = $('<div id="content"></div>');
+ $('#testArea').append($content);
+ // dummy file list
+ var $div = $(
+ '<div>' +
+ '<table id="filestable">' +
+ '<thead></thead>' +
+ '<tbody id="fileList"></tbody>' +
+ '</table>' +
+ '</div>');
+ $('#content').append($div);
+
+ fileList = new OCA.Files.FileList($div);
+ OCA.Comments.FilesPlugin.attach(fileList);
+
+ testFiles = [{
+ id: 1,
+ type: 'file',
+ name: 'One.txt',
+ path: '/subdir',
+ mimetype: 'text/plain',
+ size: 12,
+ permissions: OC.PERMISSION_ALL,
+ etag: 'abc',
+ shareOwner: 'User One',
+ isShareMountPoint: false,
+ commentsUnread: 3
+ }];
+ });
+ afterEach(function() {
+ fileList.destroy();
+ fileList = null;
+ });
+
+ describe('Comment icon', function() {
+ it('does not render icon when no unread comments available', function() {
+ testFiles[0].commentsUnread = 0;
+ fileList.setFiles(testFiles);
+ var $tr = fileList.findFileEl('One.txt');
+ expect($tr.find('.action-comment').length).toEqual(0);
+ });
+ it('renders comment icon and extra data', function() {
+ var $action, $tr;
+ fileList.setFiles(testFiles);
+ $tr = fileList.findFileEl('One.txt');
+ $action = $tr.find('.action-comment');
+ expect($action.length).toEqual(1);
+ expect($action.hasClass('permanent')).toEqual(true);
+
+ expect($tr.attr('data-comments-unread')).toEqual('3');
+ });
+ it('clicking icon opens sidebar', function() {
+ var sidebarStub = sinon.stub(fileList, 'showDetailsView');
+ var $action, $tr;
+ fileList.setFiles(testFiles);
+ $tr = fileList.findFileEl('One.txt');
+ $action = $tr.find('.action-comment');
+ $action.click();
+
+ expect(sidebarStub.calledOnce).toEqual(true);
+ expect(sidebarStub.lastCall.args[0]).toEqual('One.txt');
+ expect(sidebarStub.lastCall.args[1]).toEqual('commentsTabView');
+ });
+ });
+ describe('elementToFile', function() {
+ it('returns comment count', function() {
+ fileList.setFiles(testFiles);
+ var $tr = fileList.findFileEl('One.txt');
+ var data = fileList.elementToFile($tr);
+ expect(data.commentsUnread).toEqual(3);
+ });
+ it('does not set comment count when not set', function() {
+ delete testFiles[0].commentsUnread;
+ fileList.setFiles(testFiles);
+ var $tr = fileList.findFileEl('One.txt');
+ var data = fileList.elementToFile($tr);
+ expect(data.commentsUnread).not.toBeDefined();
+ });
+ it('does not set comment count when zero', function() {
+ testFiles[0].commentsUnread = 0;
+ fileList.setFiles(testFiles);
+ var $tr = fileList.findFileEl('One.txt');
+ var data = fileList.elementToFile($tr);
+ expect(data.commentsUnread).not.toBeDefined();
+ });
+ });
+});