/* * @copyright Copyright (c) 2018 Julius Härtl * * @author Julius Härtl * * @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 . * */ /* global OC OCA OCP t escapeHTML Handlebars moment */ import CommentCollection from '../legacy/commentcollection'; import CommentModel from '../legacy/commentmodel'; class ActivityController { constructor ($scope, CardService, ActivityService, BoardService) { 'ngInject'; this.cardservice = CardService; this.boardservice = BoardService; this.activityservice = ActivityService; this.$scope = $scope; this.type = ''; this.loading = false; this.status = { commentCreateLoading: false }; this.$scope.newComment = ''; this.$scope.newCommentString = 'New comment…'; this.currentUser = OC.getCurrentUser(); const self = this; this.$scope.$watch(function () { return self.element.id; }, function (params) { if (self.type === 'deck_card') { self.activityservice.loadComments(self.element.id); } if (self.getData(self.element.id).length === 0) { self.loading = true; self.fetchUntilResults(); } self.activityservice.fetchNewerActivities(self.type, self.element.id).then(function () {}); if (self.type === 'deck_card') { self.cardservice.getCurrent().commentsUnread = 0; } }, true); let $target = $('.newCommentForm .message'); this.applyAtWho($target); this.activityservice.subscribe(this.$scope, function() { self.$scope.$apply(); }); if (typeof OCA.Activity.Templates !== 'undefined') { OCA.Activity.Templates.userLocal = Handlebars.template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { var helper; // Compiled handlesbars template // ' {{ name }}'; return " " + container.escapeExpression(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"name","hash":{},"data":data}) : helper))) + ""; },"useData":true}); } else { OCA.Activity.RichObjectStringParser._userLocalTemplate = ' {{ name }}'; } } applyAtWho($target) { const self = this; if (!$target) { return; } $target.atwho({ at: '@', callbacks: { remoteFilter: function(query, callback) { let uids = self.boardservice.getUsers(); uids = uids.filter((x) => x.uid.toLowerCase().includes(query.toLowerCase()) || x.displayname.toLowerCase().includes(query.toLowerCase())); callback(uids); }, highlighter: function (li) { // misuse the highlighter callback to instead of // highlighting loads the avatars. var $li = $(li); $li.find('.avatar').avatar(undefined, 32); return $li; }, sorter: function (q, items) { return items; } }, displayTpl: function (item) { return '
  • ' + '' + '' + '' + '' + escapeHTML(item.displayname) + '' + '
  • '; }, insertTpl: function (item) { return '' + '' + '' + '' + '' + escapeHTML(item.displayname) + '' + ''; }, searchKey: 'displayname' }); $target.on('inserted.atwho', function (je, $el) { $(je.target).find( 'span[data-username="' + $el.find('[data-username]').data('username') + '"]' ).avatar(undefined, 16); }); $target.on('shown.atwho', function (je) { $target.find('.avatar').avatar(undefined, 16); }); } commentBodyToPlain(content) { let $comment = $('
    ').html(content); $comment.find('.avatar-name-wrapper').each(function () { var $this = $(this); var $inserted = $this.parent(); $inserted.html('@' + $this.find('.avatar').data('username')); }); $comment.html(OCP.Comments.richToPlain($comment.html())); $comment.html($comment.html().replace(//gi, '\n')); return $comment.text(); } static _composeHTMLMention(uid, displayName) { var avatar = '' + '' + ''; var isCurrentUser = (uid === OC.getCurrentUser().uid); return '' + '' + '' + avatar + '' + escapeHTML(displayName) + '' + '' + ''; } formatMessage(activity) { let message = activity.message; let mentions = activity.commentModel.get('mentions'); const editMode = false; message = escapeHTML(message).replace(/\n/g, '
    '); for(var i in mentions) { if(!mentions.hasOwnProperty(i)) { return; } var mention = '@' + mentions[i].mentionId; // escape possible regex characters in the name mention = mention.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const displayName = ActivityController._composeHTMLMention(mentions[i].mentionId, mentions[i].mentionDisplayName); // replace every mention either at the start of the input or after a whitespace // followed by a non-word character. message = message.replace(new RegExp('(^|\\s)(' + mention + ')\\b', 'g'), function(match, p1) { // to get number of whitespaces (0 vs 1) right return p1+displayName; } ); } if(editMode !== true) { message = OCP.Comments.plainToRich(message); } return message; } postComment() { const self = this; this.status.commentCreateLoading = true; let content = this.commentBodyToPlain(self.$scope.newComment); if (content.length < 1) { self.status.commentCreateLoading = false; OC.Notification.showTemporary(t('deck', 'Please provide a content for your comment.')); return; } var model = this.activityservice.commentCollection.create({ actorId: OC.getCurrentUser().uid, actorDisplayName: OC.getCurrentUser().displayName, actorType: 'users', verb: 'comment', message: content, creationDateTime: (new Date()).toUTCString() }, { at: 0, // wait for real creation before adding wait: true, success: function() { self.$scope.newComment = ''; self.activityservice.fetchNewerActivities(self.type, self.element.id).then(function () {}); self.status.commentCreateLoading = false; }, error: function() { self.status.commentCreateLoading = false; OC.Notification.showTemporary(t('deck', 'Posting the comment failed.')); } }); } updateComment(item) { item.commentEdit = this.formatMessage(item); let $target = $('.newCommentForm .message'); this.applyAtWho($target); /** Workaround to trigger avatar rendering after the view has been updated */ window.setTimeout(function () { $target.find('.avatar').avatar(undefined, 16); }, 0); } editComment(item) { const self = this; let content = this.commentBodyToPlain(item.commentEdit); if (content.length < 1) { OC.Notification.showTemporary(t('deck', 'Please provide a content for your comment.')); return; } /** We need to save the model and afterwards run a fetch to update the mentions * and call apply to propagate the changes to angular */ item.commentModel.on('sync', function() { item.commentModel.off('sync'); item.commentModel.fetch({ success: function() { self.$scope.$apply(); } }); }); item.commentModel.save({ message: content, }); item.message = content; item.commentEdit = undefined; } deleteComment(item) { item.commentModel.destroy(); item.deleted = true; item.commentModel = undefined; item.message = t('deck', 'The comment has been deleted'); } getData(id) { return this.activityservice.getData(this.type, id); } parseMessage(activity) { let subject = activity.subject_rich[0]; let parameters = activity.subject_rich[1]; if (parameters.after && parameters.after.id && parameters.after.id.startsWith('dt:')) { let dateTime = parameters.after.id.substr(3); parameters.after.name = moment(dateTime).format('L LTS'); } return OCA.Activity.RichObjectStringParser.parseMessage(subject, parameters); } fetchUntilResults () { const self = this; let dataLengthBefore = self.getData(self.element.id).length; let _executeFetch = function() { let promise = self.activityservice.fetchMoreActivities(self.type, self.element.id); promise.then(function (data) { let dataLengthAfter = self.getData(self.element.id).length; if (data !== null && (dataLengthAfter <= dataLengthBefore || dataLengthAfter < self.activityservice.RESULT_PER_PAGE)) { _executeFetch(); } else { self.loading = false; } }, function () { self.loading = false; self.$scope.$apply(); }); }; _executeFetch(); } getComments() { return this.activityservice.comments; } getActivityStream() { let activities = this.activityservice.getData(this.type, this.element.id); return activities; } page() { if (!this.activityservice.since[this.type][this.element.id].finished) { this.loading = true; this.fetchUntilResults(); } else { this.loading = false; } } loadingNewer() { return this.activityservice.runningNewer; } t(text) { return t('deck', text); } } let activityComponent = { templateUrl: OC.linkTo('deck', 'templates/part.card.activity.html'), controller: ActivityController, bindings: { type: '@', element: '=' } }; export default activityComponent;