summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBernhard Posselt <nukeawhale@gmail.com>2013-09-14 02:22:36 +0200
committerBernhard Posselt <nukeawhale@gmail.com>2013-09-14 02:22:44 +0200
commitdf8f6b5fee643c5b2af8e8d33a7865e898518485 (patch)
tree7100d25d814d5f4b4cf502c9e2b9e6dd5f562ab3
parent67d7754c364147c274790a76dab7cbfedd352645 (diff)
implement pull to refresh, fix #44
-rw-r--r--CHANGELOG1
-rw-r--r--appinfo/routes.php6
-rw-r--r--controller/itemcontroller.php29
-rw-r--r--css/items.css21
-rw-r--r--js/app/controllers/itemcontroller.coffee5
-rw-r--r--js/app/directives/pulltorefresh.coffee30
-rw-r--r--js/app/services/businesslayer/itembusinesslayer.coffee8
-rw-r--r--js/app/services/models/itemmodel.coffee14
-rw-r--r--js/app/services/persistence.coffee13
-rw-r--r--js/public/app.js82
-rw-r--r--js/tests/controllers/itemcontrollerSpec.coffee15
-rw-r--r--js/tests/services/businesslayer/itembusinesslayerSpec.coffee16
-rw-r--r--js/tests/services/models/itemmodelSpec.coffee11
-rw-r--r--js/tests/services/persistenceSpec.coffee16
-rw-r--r--templates/main.php1
-rw-r--r--templates/part.items.php2
-rw-r--r--tests/unit/controller/ItemControllerTest.php86
17 files changed, 349 insertions, 7 deletions
diff --git a/CHANGELOG b/CHANGELOG
index c6dbb068b..27bd2e597 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -4,6 +4,7 @@ owncloud-news (1.601)
* Autopurge limit is now added to the number of articles each feed gets when it updates
* Fix CORS headers for OPTIONS request deeper than one level
* Use before and after update cleanup hooks to make sure that read items are not turned unread again after an update. This breaks custom updaters. The updater script has been adjusted accordingly
+* Implement pull to refresh
owncloud-news (1.404)
* Fix bug on postgres databases that would not delete old articles
diff --git a/appinfo/routes.php b/appinfo/routes.php
index de8f5a47a..bc1b7ad7d 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -157,6 +157,12 @@ $this->create('news_items', '/items')->get()->action(
}
);
+$this->create('news_items_new', '/items/new')->get()->action(
+ function($params){
+ App::main('ItemController', 'newItems', $params, new DIContainer());
+ }
+);
+
$this->create('news_items_read', '/items/{itemId}/read')->post()->action(
function($params){
App::main('ItemController', 'read', $params, new DIContainer());
diff --git a/controller/itemcontroller.php b/controller/itemcontroller.php
index a35d27476..a3f632e39 100644
--- a/controller/itemcontroller.php
+++ b/controller/itemcontroller.php
@@ -89,6 +89,35 @@ class ItemController extends Controller {
}
+ /**
+ * @IsAdminExemption
+ * @IsSubAdminExemption
+ * @Ajax
+ */
+ public function newItems() {
+ $userId = $this->api->getUserId();
+ $showAll = $this->api->getUserValue('showAll') === '1';
+
+ $type = (int) $this->params('type');
+ $id = (int) $this->params('id');
+ $lastModified = (int) $this->params('lastModified', 0);
+
+ $params = array();
+
+ try {
+ $params['newestItemId'] = $this->itemBusinessLayer->getNewestItemId($userId);
+ $params['feeds'] = $this->feedBusinessLayer->findAll($userId);
+ $params['starred'] = $this->itemBusinessLayer->starredCount($userId);
+ $params['items'] = $this->itemBusinessLayer->findAllNew($id, $type,
+ $lastModified, $showAll, $userId);
+ // this gets thrown if there are no items
+ // in that case just return an empty array
+ } catch(BusinessLayerException $ex) {}
+
+ return $this->renderJSON($params);
+ }
+
+
private function setStarred($isStarred){
$userId = $this->api->getUserId();
$feedId = (int) $this->params('feedId');
diff --git a/css/items.css b/css/items.css
index 60173ed82..9c5ca9ab2 100644
--- a/css/items.css
+++ b/css/items.css
@@ -30,6 +30,7 @@
background-image: url('%webroot%/core/img/loading.gif');
background-position: center;
background-repeat: no-repeat;
+ background-size: 18px;
display: block;
height: 100%;
}
@@ -45,6 +46,26 @@
}
+.pull-refresh {
+ -webkit-transition: height 0.5s;
+ -moz-transition: height 0.5s;
+ transition: height 0.5s;
+ background-image: url('%webroot%/core/img/loading.gif'), linear-gradient(top, rgb(235,235,235) 0%, rgb(248,248,248) 100%);
+ background-image: url('%webroot%/core/img/loading.gif'), -o-linear-gradient(top, rgb(235,235,235) 0%, rgb(248,248,248) 100%);
+ background-image: url('%webroot%/core/img/loading.gif'), -moz-linear-gradient(top, rgb(235,235,235) 0%, rgb(248,248,248) 100%);
+ background-image: url('%webroot%/core/img/loading.gif'), -webkit-linear-gradient(top, rgb(235,235,235) 0%, rgb(248,248,248) 100%);
+ background-image: url('%webroot%/core/img/loading.gif'), -ms-linear-gradient(top, rgb(235,235,235) 0%, rgb(248,248,248) 100%);
+ background-position: center 45px, center;
+ background-repeat: no-repeat;
+ background-size: 18px, 100%;
+ height: 0;
+ width: 100%;
+}
+
+.refresh {
+ height: 80px;
+}
+
/**
* Rules for a single feed item
*/
diff --git a/js/app/controllers/itemcontroller.coffee b/js/app/controllers/itemcontroller.coffee
index eac508e18..c9a97285f 100644
--- a/js/app/controllers/itemcontroller.coffee
+++ b/js/app/controllers/itemcontroller.coffee
@@ -58,6 +58,11 @@ Language, AutoPageLoading) ->
else
return ''
+ @_$scope.loadNew = =>
+ @_$scope.refresh = true
+ @_itemBusinessLayer.loadNew =>
+ @_$scope.refresh = false
+
@_$scope.$on 'readItem', (scope, data) =>
@_itemBusinessLayer.setRead(data)
diff --git a/js/app/directives/pulltorefresh.coffee b/js/app/directives/pulltorefresh.coffee
new file mode 100644
index 000000000..436280793
--- /dev/null
+++ b/js/app/directives/pulltorefresh.coffee
@@ -0,0 +1,30 @@
+###
+
+ownCloud - News
+
+@author Bernhard Posselt
+@copyright 2012 Bernhard Posselt dev@bernhard-posselt.com
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+License as published by the Free Software Foundation; either
+version 3 of the License, or any later version.
+
+This library 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 library. If not, see <http://www.gnu.org/licenses/>.
+
+###
+
+angular.module('News').directive 'newsPullToRefresh', ->
+ directive =
+ restrict: 'A'
+ link: (scope, elm, attrs) ->
+ scrollTop = 0
+ elm.scroll ->
+ if @scrollTop == 0
+ scope.$apply attrs.newsPullToRefresh \ No newline at end of file
diff --git a/js/app/services/businesslayer/itembusinesslayer.coffee b/js/app/services/businesslayer/itembusinesslayer.coffee
index 0563ff096..1382df190 100644
--- a/js/app/services/businesslayer/itembusinesslayer.coffee
+++ b/js/app/services/businesslayer/itembusinesslayer.coffee
@@ -116,8 +116,12 @@ StarredBusinessLayer, NewestItem) ->
callback()
-
- loadNew: ->
+ loadNew: (onSuccess) ->
+ lastModified = @_itemModel.getLastModified()
+ @_persistence.getNewItems(@_activeFeed.getType(),
+ @_activeFeed.getId(),
+ lastModified,
+ onSuccess)
diff --git a/js/app/services/models/itemmodel.coffee b/js/app/services/models/itemmodel.coffee
index 349147ca5..7da924be3 100644
--- a/js/app/services/models/itemmodel.coffee
+++ b/js/app/services/models/itemmodel.coffee
@@ -21,8 +21,8 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
###
angular.module('News').factory 'ItemModel',
-['_Model', '_MinimumQuery', 'StatusFlag',
-(_Model, _MinimumQuery, StatusFlag) ->
+['_Model', '_MinimumQuery', '_MaximumQuery', 'StatusFlag',
+(_Model, _MinimumQuery, _MaximumQuery, StatusFlag) ->
class ItemModel extends _Model
@@ -109,5 +109,15 @@ angular.module('News').factory 'ItemModel',
return 0
+ getLastModified: ->
+ query = new _MaximumQuery('lastModified')
+ lastModified = @get(query)
+
+ if angular.isDefined(lastModified)
+ return lastModified.lastModified
+ else
+ return 0
+
+
return new ItemModel()
] \ No newline at end of file
diff --git a/js/app/services/persistence.coffee b/js/app/services/persistence.coffee
index 559601f69..d51c7acb1 100644
--- a/js/app/services/persistence.coffee
+++ b/js/app/services/persistence.coffee
@@ -86,6 +86,19 @@ $rootScope, $q) ->
@_request.get 'news_items', params
+ getNewItems: (type, id, lastModified, onSuccess) ->
+ onSuccess or= ->
+ params =
+ data:
+ type: type
+ id: id
+ lastModified: lastModified
+ onSuccess: onSuccess
+ onFailure: onSuccess
+
+ @_request.get 'news_items_new', params
+
+
starItem: (feedId, guidHash) ->
###
Stars an item
diff --git a/js/public/app.js b/js/public/app.js
index cd822b6a4..ab237c645 100644
--- a/js/public/app.js
+++ b/js/public/app.js
@@ -452,6 +452,48 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
(function() {
+ angular.module('News').directive('newsPullToRefresh', function() {
+ var directive;
+ return directive = {
+ restrict: 'A',
+ link: function(scope, elm, attrs) {
+ var scrollTop;
+ scrollTop = 0;
+ return elm.scroll(function() {
+ if (this.scrollTop === 0) {
+ return scope.$apply(attrs.newsPullToRefresh);
+ }
+ });
+ }
+ };
+ });
+
+}).call(this);
+
+// Generated by CoffeeScript 1.6.3
+/*
+
+ownCloud - News
+
+@author Bernhard Posselt
+@copyright 2012 Bernhard Posselt dev@bernhard-posselt.com
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+License as published by the Free Software Foundation; either
+version 3 of the License, or any later version.
+
+This library 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 library. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+
+(function() {
angular.module('News').directive('undoNotification', [
'$rootScope', '$timeout', 'Config', function($rootScope, $timeout, Config) {
return function(scope, elm, attr) {
@@ -735,6 +777,12 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
return '';
}
};
+ this._$scope.loadNew = function() {
+ _this._$scope.refresh = true;
+ return _this._itemBusinessLayer.loadNew(function() {
+ return _this._$scope.refresh = false;
+ });
+ };
this._$scope.$on('readItem', function(scope, data) {
return _this._itemBusinessLayer.setRead(data);
});
@@ -1525,7 +1573,11 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
}
};
- ItemBusinessLayer.prototype.loadNew = function() {};
+ ItemBusinessLayer.prototype.loadNew = function(onSuccess) {
+ var lastModified;
+ lastModified = this._itemModel.getLastModified();
+ return this._persistence.getNewItems(this._activeFeed.getType(), this._activeFeed.getId(), lastModified, onSuccess);
+ };
return ItemBusinessLayer;
@@ -2212,7 +2264,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
angular.module('News').factory('ItemModel', [
- '_Model', '_MinimumQuery', 'StatusFlag', function(_Model, _MinimumQuery, StatusFlag) {
+ '_Model', '_MinimumQuery', '_MaximumQuery', 'StatusFlag', function(_Model, _MinimumQuery, _MaximumQuery, StatusFlag) {
var ItemModel;
ItemModel = (function(_super) {
__extends(ItemModel, _super);
@@ -2309,6 +2361,17 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
}
};
+ ItemModel.prototype.getLastModified = function() {
+ var lastModified, query;
+ query = new _MaximumQuery('lastModified');
+ lastModified = this.get(query);
+ if (angular.isDefined(lastModified)) {
+ return lastModified.lastModified;
+ } else {
+ return 0;
+ }
+ };
+
return ItemModel;
})(_Model);
@@ -2574,6 +2637,21 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
return this._request.get('news_items', params);
};
+ Persistence.prototype.getNewItems = function(type, id, lastModified, onSuccess) {
+ var params;
+ onSuccess || (onSuccess = function() {});
+ params = {
+ data: {
+ type: type,
+ id: id,
+ lastModified: lastModified
+ },
+ onSuccess: onSuccess,
+ onFailure: onSuccess
+ };
+ return this._request.get('news_items_new', params);
+ };
+
Persistence.prototype.starItem = function(feedId, guidHash) {
/*
Stars an item
diff --git a/js/tests/controllers/itemcontrollerSpec.coffee b/js/tests/controllers/itemcontrollerSpec.coffee
index b06e16a9c..27fc82f71 100644
--- a/js/tests/controllers/itemcontrollerSpec.coffee
+++ b/js/tests/controllers/itemcontrollerSpec.coffee
@@ -169,3 +169,18 @@ describe 'ItemController', ->
expect(@persistence.getItems.callCount).toBe(2)
+
+ it 'should set refresh to true when pull to refresh is activated', =>
+ @ItemBusinessLayer.loadNew = ->
+
+ @scope.loadNew()
+ expect(@scope.refresh).toBe(true)
+
+
+ it 'should set refresh to false after load next was successful', =>
+ @ItemBusinessLayer.loadNew = jasmine.createSpy('loadNew')
+ @ItemBusinessLayer.loadNew.andCallFake (callback) ->
+ callback()
+
+ @scope.loadNew()
+ expect(@scope.refresh).toBe(false) \ No newline at end of file
diff --git a/js/tests/services/businesslayer/itembusinesslayerSpec.coffee b/js/tests/services/businesslayer/itembusinesslayerSpec.coffee
index e14ba54b3..75804323d 100644
--- a/js/tests/services/businesslayer/itembusinesslayerSpec.coffee
+++ b/js/tests/services/businesslayer/itembusinesslayerSpec.coffee
@@ -248,3 +248,19 @@ describe 'ItemBusinessLayer', ->
expect(@persistence.getItems).toHaveBeenCalledWith(
@FeedType.Feed, 3, 1, jasmine.any(Function))
+
+
+ it 'should load the next items', =>
+ @NewestItem.handle(13)
+ @persistence.getNewItems = jasmine.createSpy('loadnew')
+ callback = ->
+
+ @ItemModel.add({id: 2, guidHash: 'abc', feedId: 2, lastModified: 2})
+ @ItemModel.add({id: 3, guidHash: 'abcd', feedId: 2, lastModified: 4})
+ @ItemModel.add({id: 1, guidHash: 'abce', feedId: 2, lastModified: 3})
+ @ItemModel.add({id: 6, guidHash: 'abcf', feedId: 2, lastModified: 1})
+
+ @ItemBusinessLayer.loadNew(callback)
+
+ expect(@persistence.getNewItems).toHaveBeenCalledWith(
+ @FeedType.Feed, 3, 4, callback)
diff --git a/js/tests/services/models/itemmodelSpec.coffee b/js/tests/services/models/itemmodelSpec.coffee
index 4decc7918..d2fed6630 100644
--- a/js/tests/services/models/itemmodelSpec.coffee
+++ b/js/tests/services/models/itemmodelSpec.coffee
@@ -110,4 +110,13 @@ describe 'ItemModel', ->
@ItemModel.add({id: 1, guidHash: 'abce', feedId: 2, status: 16})
@ItemModel.add({id: 6, guidHash: 'abcf', feedId: 2, status: 16})
- expect(@ItemModel.getLowestId()).toBe(1) \ No newline at end of file
+ expect(@ItemModel.getLowestId()).toBe(1)
+
+
+ it 'should return the highest lastModified', =>
+ @ItemModel.add({id: 2, guidHash: 'abc', feedId: 2, lastModified: 3})
+ @ItemModel.add({id: 3, guidHash: 'abcd', feedId: 2, lastModified: 13})
+ @ItemModel.add({id: 1, guidHash: 'abce', feedId: 2, lastModified: 15})
+ @ItemModel.add({id: 6, guidHash: 'abcf', feedId: 2, lastModified: 11})
+
+ expect(@ItemModel.getLastModified()).toBe(15) \ No newline at end of file
diff --git a/js/tests/services/persistenceSpec.coffee b/js/tests/services/persistenceSpec.coffee
index e9f12f669..38e02705a 100644
--- a/js/tests/services/persistenceSpec.coffee
+++ b/js/tests/services/persistenceSpec.coffee
@@ -74,6 +74,22 @@ describe 'Persistence', ->
expect(@req.get).toHaveBeenCalledWith('news_items', expected)
+ it 'should send a load new items request', =>
+ success = ->
+ params =
+ data:
+ type: 2
+ id: 5
+ lastModified: 3
+ onSuccess: success
+ onFailure: success
+
+ @Persistence.getNewItems(params.data.type, params.data.id,
+ params.data.lastModified, success)
+
+ expect(@req.get).toHaveBeenCalledWith('news_items_new', params)
+
+
it 'send a correct star item request', =>
params =
routeParams:
diff --git a/templates/main.php b/templates/main.php
index e0722a91d..80cf5eb86 100644
--- a/templates/main.php
+++ b/templates/main.php
@@ -61,6 +61,7 @@ if($version[0] > 5 || ($version[0] >= 5 && $version[1] >= 80)) {
ng-show="initialized && !feedBusinessLayer.noFeeds()"
news-item-scroll="true"
item-shortcuts
+ news-pull-to-refresh="loadNew()"
tabindex="-1">
<?php print_unescaped($this->inc("part.items")); ?>
</div>
diff --git a/templates/part.items.php b/templates/part.items.php
index 9a067cf6e..a0c0c3cc4 100644
--- a/templates/part.items.php
+++ b/templates/part.items.php
@@ -1,3 +1,5 @@
+<div class="pull-refresh" ng-class="{refresh: refresh}"></div>
+
<ul>
<li class="feed_item"
ng-repeat="item in itemBusinessLayer.getAll() | orderBy:['-id'] "
diff --git a/tests/unit/controller/ItemControllerTest.php b/tests/unit/controller/ItemControllerTest.php
index 5a1104137..9d104b8f0 100644
--- a/tests/unit/controller/ItemControllerTest.php
+++ b/tests/unit/controller/ItemControllerTest.php
@@ -84,11 +84,16 @@ class ItemControllerTest extends ControllerTestUtility {
$this->assertAnnotations($this->controller, $methodName, $annotations);
}
+
public function testItemsAnnotations(){
$this->assertItemControllerAnnotations('items');
}
+ public function testNewItemsAnnotations(){
+ $this->assertItemControllerAnnotations('newItems');
+ }
+
public function testStarAnnotations(){
$this->assertItemControllerAnnotations('star');
}
@@ -446,5 +451,86 @@ class ItemControllerTest extends ControllerTestUtility {
}
+ public function testNewItems(){
+ $feeds = array(new Feed());
+ $result = array(
+ 'items' => array(new Item()),
+ 'feeds' => $feeds,
+ 'newestItemId' => $this->newestItemId,
+ 'starred' => 3111
+ );
+ $post = array(
+ 'lastModified' => 3,
+ 'type' => FeedType::FEED,
+ 'id' => 2
+ );
+ $this->controller = $this->getPostController($post);
+
+ $this->api->expects($this->once())
+ ->method('getUserValue')
+ ->with($this->equalTo('showAll'))
+ ->will($this->returnValue('1'));
+ $this->api->expects($this->once())
+ ->method('getUserId')
+ ->will($this->returnValue($this->user));
+
+ $this->feedBusinessLayer->expects($this->once())
+ ->method('findAll')
+ ->with($this->equalTo($this->user))
+ ->will($this->returnValue($feeds));
+
+ $this->itemBusinessLayer->expects($this->once())
+ ->method('getNewestItemId')
+ ->with($this->equalTo($this->user))
+ ->will($this->returnValue($this->newestItemId));
+
+ $this->itemBusinessLayer->expects($this->once())
+ ->method('starredCount')
+ ->with($this->equalTo($this->user))
+ ->will($this->returnValue(3111));
+
+ $this->itemBusinessLayer->expects($this->once())
+ ->method('findAllNew')
+ ->with(
+ $this->equalTo($post['id']),
+ $this->equalTo($post['type']),
+ $this->equalTo($post['lastModified']),
+ $this->equalTo(true),
+ $this->equalTo($this->user))
+ ->will($this->returnValue($result['items']));
+
+ $response = $this->controller->newItems();
+ $this->assertEquals($result, $response->getParams());
+ $this->assertTrue($response instanceof JSONResponse);
+ }
+
+
+ public function testGetNewItemsNoNewestItemsId(){
+ $result = array();
+ $post = array(
+ 'lastModified' => 3,
+ 'type' => FeedType::FEED,
+ 'id' => 2
+ );
+ $this->controller = $this->getPostController($post);
+
+ $this->api->expects($this->once())
+ ->method('getUserValue')
+ ->with($this->equalTo('showAll'))
+ ->will($this->returnValue('1'));
+ $this->api->expects($this->once())
+ ->method('getUserId')
+ ->will($this->returnValue($this->user));
+
+ $this->itemBusinessLayer->expects($this->once())
+ ->method('getNewestItemId')
+ ->with($this->equalTo($this->user))
+ ->will($this->throwException(new BusinessLayerException('')));
+
+ $response = $this->controller->newItems();
+ $this->assertEquals($result, $response->getParams());
+ $this->assertTrue($response instanceof JSONResponse);
+ }
+
} \ No newline at end of file