diff --git a/css/activity.css b/css/activity.css index e69de29bb..3cfcbd3b9 100644 --- a/css/activity.css +++ b/css/activity.css @@ -0,0 +1,9 @@ +.activitymessage .visualdiff ins { + background-color: rgba(70, 186, 97, 0.2); + text-decoration: none; +} + +.activitymessage .visualdiff del { + background-color: rgba(233, 50, 45, 0.2); + text-decoration: none; +} diff --git a/js/app/App.js b/js/app/App.js index 1a77c69cc..f47a34fc3 100644 --- a/js/app/App.js +++ b/js/app/App.js @@ -4,20 +4,20 @@ * @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 angular */ @@ -48,13 +48,15 @@ import ngsortable from 'ng-sortable'; import md from 'angular-markdown-it'; import nganimate from 'angular-animate'; import 'angular-file-upload'; +import ngInfiniteScroll from 'ng-infinite-scroll'; var app = angular.module('Deck', [ ngsanitize, uirouter, angularuiselect, ngsortable, md, nganimate, - 'angularFileUpload' + 'angularFileUpload', + ngInfiniteScroll ]); export default app; diff --git a/js/controller/ActivityController.js b/js/controller/ActivityController.js new file mode 100644 index 000000000..964eb952a --- /dev/null +++ b/js/controller/ActivityController.js @@ -0,0 +1,65 @@ +/* + * @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 */ + +class ActivityController { + constructor ($scope, CardService, ActivityService) { + 'ngInject'; + this.cardservice = CardService; + this.activityservice = ActivityService; + this.$scope = $scope; + this.type = ''; + var self = this; + this.$scope.$watch(function() { + return self.element.id; + }, function (params) { + self.activityservice.fetchMoreActivities(self.type, self.element.id); + self.activityservice.fetchNewerActivities(self.type, self.element.id); + }, true); + console.log('constructor'); + } + + getData(id) { + return this.activityservice.getData(this.type, id); + } + + parseMessage(subject, parameters) { + OCA.Activity.RichObjectStringParser._userLocalTemplate = ''; + return OCA.Activity.RichObjectStringParser.parseMessage(subject, parameters); + } + + page() { + this.activityservice.fetchMoreActivities(this.type, this.element.id); + } + +} + +let activityComponent = { + templateUrl: OC.linkTo('deck', 'templates/part.card.activity.html'), + controller: ActivityController, + bindings: { + type: '@', + element: '=' + } +}; +export default activityComponent; diff --git a/js/init.js b/js/init.js index 3cbcbedb7..0609fcc04 100644 --- a/js/init.js +++ b/js/init.js @@ -14,9 +14,11 @@ import './app/Run.js'; import ListController from 'controller/ListController.js'; import attachmentListComponent from './controller/AttachmentController.js'; +import activityComponent from './controller/ActivityController.js'; app.controller('ListController', ListController); app.component('attachmentListComponent', attachmentListComponent); +app.component('activityComponent', activityComponent); // require all the js files from subdirectories diff --git a/js/package-lock.json b/js/package-lock.json index e3009410d..42e9a5b62 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -2970,8 +2970,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -2992,14 +2991,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3014,20 +3011,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -3144,8 +3138,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -3157,7 +3150,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3172,7 +3164,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3180,14 +3171,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -3206,7 +3195,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -3287,8 +3275,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -3300,7 +3287,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -3386,8 +3372,7 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -3423,7 +3408,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3443,7 +3427,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3487,14 +3470,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -4917,6 +4898,11 @@ "integrity": "sha512-vdqTKI9GBIYcAEbFAcpKPErKINfPF5zIuz3/niBfq8WUZjpT2tytLlFVrBgWdOtqI4uaA/Rb6No0hux39XXDuw==", "dev": true }, + "ng-infinite-scroll": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ng-infinite-scroll/-/ng-infinite-scroll-1.3.0.tgz", + "integrity": "sha1-wumNj9E0sFJaTSz1jJXZtYN1URI=" + }, "ng-sortable": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ng-sortable/-/ng-sortable-1.3.8.tgz", diff --git a/js/package.json b/js/package.json index 5a20b4b4d..0b11f9b98 100644 --- a/js/package.json +++ b/js/package.json @@ -17,6 +17,7 @@ "babel-polyfill": "^6.26.0", "markdown-it": "^8.4.2", "markdown-it-link-target": "^1.0.2", + "ng-infinite-scroll": "^1.3.0", "ng-sortable": "^1.3.8", "ui-select": "^0.19.8" }, diff --git a/js/service/ActivityService.js b/js/service/ActivityService.js new file mode 100644 index 000000000..cc951fb2a --- /dev/null +++ b/js/service/ActivityService.js @@ -0,0 +1,168 @@ +/* + * @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 . + * + */ + +import app from '../app/App.js'; + +const DECK_ACTIVITY_TYPE_BOARD = 'deck_board'; +const DECK_ACTIVITY_TYPE_CARD = 'deck_card'; + +/* global OC oc_requesttoken */ +class ActivityService { + + constructor ($rootScope, $filter, $http, $q) { + this.running = false; + this.$filter = $filter; + this.$http = $http; + this.$q = $q; + this.data = {}; + this.data[DECK_ACTIVITY_TYPE_BOARD] = {}; + this.data[DECK_ACTIVITY_TYPE_CARD] = {}; + this.since = { + deck_card: { + + }, + deck_board: { + + }, + } + } + + static getUrl(type, id, since) { + if (type === DECK_ACTIVITY_TYPE_CARD) + return OC.linkToOCS('apps/activity/api/v2/activity', 2) + 'filter?format=json&object_type=deck_card&object_id=' + id + '&limit=5&since=' + since; + if (type === DECK_ACTIVITY_TYPE_BOARD) + return OC.linkToOCS('apps/activity/api/v2/activity', 2) + 'filter?format=json&object_type=deck_board&object_id=' + id + '&limit=5&since=' + since; + } + + fetchCardActivities(type, id, since) { + var deferred = this.$q.defer(); + this.running = true; + + this.checkData(type, id); + var self = this; + this.$http.get(ActivityService.getUrl(type, id, since)).then(function (response) { + var objects = response.data.ocs.data; + + for (let index in objects) { + let item = objects[index]; + self.addItem(type, id, item); + if (item.activity_id > self.since[type][id].latest) { + self.since[type][id].latest = item.activity_id; + } + } + self.data[type][id].sort(function(a, b) { + return b.activity_id - a.activity_id; + }); + self.since[type][id].oldest = response.headers('X-Activity-Last-Given'); + self.running = false; + deferred.resolve(objects); + }, function (error) { + if (error.status === 304) { + self.since[type][id].finished = true; + } + self.running = false; + }); + return deferred.promise; + } + fetchMoreActivities(type, id) { + this.checkData(type, id); + if (this.running === true) { + return; + } + if (!this.since[type][id].finished) { + return this.fetchCardActivities(type, id, this.since[type][id].oldest); + } + } + + checkData(type, id) { + if (!Array.isArray(this.data[type][id])) { + this.data[type][id] = []; + } + if (typeof this.since[type][id] === 'undefined') { + this.since[type][id] = { + latest: 0, + oldestCatchedUp: false, + oldest: '0', + finished: false, + }; + } + } + + addItem(type, id, item) { + console.log(id); + if (this.data[type][id].findIndex((entry) => { return entry.activity_id === item.activity_id; }) === -1) { + item.timestamp = new Date(item.datetime).getTime(); + this.data[type][id].push(item); + } + } + + /** + * Fetch newer activities starting from the latest ones that are in cache + * + * @param type + * @param id + */ + fetchNewerActivities(type, id) { + if (this.since[type][id].latest === 0) { + return; + } + let self = this; + this.fetchNewer(type, id).then(function() { + self.fetchNewerActivities(type, id); + }); + } + + fetchNewer(type, id) { + var deferred = this.$q.defer(); + this.running = true; + var self = this; + this.$http.get(ActivityService.getUrl(type, id, this.since[type][id].latest) + '&sort=asc').then(function (response) { + var objects = response.data.ocs.data; + + let data = []; + for (let index in objects) { + let item = objects[index]; + self.addItem(type, id, item); + } + self.data[type][id].sort(function(a, b) { + return b.activity_id - a.activity_id; + }); + self.since[type][id].latest = response.headers('X-Activity-Last-Given'); + self.data[type][id] = data.concat(self.data[type][id]); + self.running = false; + deferred.resolve(objects); + }, function (error) { + self.running = false; + }); + return deferred.promise; + } + + getData(type, id) { + return this.data[type][id]; + } + +}; + +app.service('ActivityService', ActivityService); + +export default ActivityService; +export {DECK_ACTIVITY_TYPE_BOARD, DECK_ACTIVITY_TYPE_CARD}; diff --git a/templates/main.php b/templates/main.php index 636c1107b..7221882e9 100644 --- a/templates/main.php +++ b/templates/main.php @@ -23,6 +23,9 @@ use OCP\Util; +Util::addScript('activity', 'richObjectStringParser'); +Util::addStyle('activity', 'style'); + Util::addStyle('deck', '../js/build/vendor'); Util::addScript('deck', 'build/vendor'); diff --git a/templates/part.board.sidebarView.php b/templates/part.board.sidebarView.php index 6fa652929..891e1c6be 100644 --- a/templates/part.board.sidebarView.php +++ b/templates/part.board.sidebarView.php @@ -16,6 +16,8 @@
  • t('Tags')); ?>
  • t('Deleted Stacks')); ?>
  • t('Deleted Cards')); ?>
  • +
  • t('Activity')); ?>
  • +
    @@ -147,4 +149,10 @@
    + +
    + +
    + +
    diff --git a/templates/part.card.activity.html b/templates/part.card.activity.html new file mode 100644 index 000000000..e17726a65 --- /dev/null +++ b/templates/part.card.activity.html @@ -0,0 +1,12 @@ +
      +
    • +
      + +
      +
      + {{ activity.timestamp/1000 | relativeDateFilter }} +
      {{ activity.activity_id }}
      +
    • +
    diff --git a/templates/part.card.php b/templates/part.card.php index a21f4735b..7f1104470 100644 --- a/templates/part.card.php +++ b/templates/part.card.php @@ -92,6 +92,8 @@
    t('Saved')); ?> @@ -128,4 +130,10 @@ ng-if="!description()">t('Add a card description…')); ?>
    + +
    + +
    + +