Merge branch 'master' into bgfetch

* master: (38 commits)
  New Play Store image assets.
  Fix layout bug on small screens.
  Revert "Updating stories which give integrity errors."
  Fix laggy updates for saved stories unsave ops.
  Ability to unsave stories.
  Make height of saved stories row match other folders.
  Make rows reliably hilight when tapped. (#282)
  Allowing replacement of story dates on integrityerror updated stories.
  Swapping errors.
  Updating stories which give integrity errors.
  Fixing margin on subscribe/follow taskbar button.
  Upgrading Android Studio.
  Fixing #261. Decoding html entities in story titles (both in the share dialog and in the story detail view).
  Flattening gradients on feed and story selection. Adding colored background back in to selected river stories.
  Fixing modal resizing. Thanks to @tomtaylor for finding the broken dialog.
  Remove unused ABS progress indicator support for reading view.
  Implement #426 - preference to show/hide public comments
  Pressed state for overlay_done button.
  Pressed states for all overlay buttons.
  Fit overlays on 360dp screens. Move progress indicator to overlay.  Add progress indicator for text mode.
  ...
This commit is contained in:
Samuel Clay 2013-11-21 18:38:11 -08:00
commit c167ccb46c
91 changed files with 1450 additions and 254 deletions

View file

@ -187,6 +187,7 @@ class UserAgentBanMiddleware:
if 'profile' in request.path: return
if 'haproxy' in request.path: return
if getattr(settings, 'TEST_DEBUG'): return
if any(ua in user_agent for ua in BANNED_USER_AGENTS):
data = {

View file

@ -17,7 +17,7 @@
"pk": 1,
"model": "reader.usersubscriptionfolders",
"fields": {
"folders": "[1, {\"Tech\": [4, 5, {\"Deep Tech\": [6, 7]}]}, 2, 3, 8, 9, {\"Blogs\": [8, 9]}]",
"folders": "[{\"Tech\": [1, 4, 5, {\"Deep Tech\": [6, 7]}]}, 2, 3, 8, 9, {\"Blogs\": [8, 9]}, 1]",
"user": 1
}
},

View file

@ -917,7 +917,25 @@ class UserSubscriptionFolders(models.Model):
user_sub_folders = add_object_to_folder(obj, parent_folder, user_sub_folders)
self.folders = json.encode(user_sub_folders)
self.save()
def arranged_folders(self):
user_sub_folders = json.decode(self.folders)
def _arrange_folder(folder):
folder_feeds = []
folder_folders = []
for item in folder:
if isinstance(item, int):
folder_feeds.append(item)
elif isinstance(item, dict):
for f_k, f_v in item.items():
arranged_folder = _arrange_folder(f_v)
folder_folders.append({f_k: arranged_folder})
arranged_folder = folder_feeds + folder_folders
return arranged_folder
return _arrange_folder(user_sub_folders)
def delete_feed(self, feed_id, in_folder, commit_delete=True):
def _find_feed_in_folders(old_folders, folder_name='', multiples_found=False, deleted=False):
new_folders = []
@ -944,7 +962,7 @@ class UserSubscriptionFolders(models.Model):
return new_folders, multiples_found, deleted
user_sub_folders = json.decode(self.folders)
user_sub_folders = self.arranged_folders()
user_sub_folders, multiples_found, deleted = _find_feed_in_folders(user_sub_folders)
self.folders = json.encode(user_sub_folders)
self.save()

View file

@ -6,7 +6,9 @@ from django.conf import settings
from mongoengine.connection import connect, disconnect
class ReaderTest(TestCase):
fixtures = ['subscriptions.json', 'stories.json', '../../rss_feeds/fixtures/gawker1.json']
fixtures = ['../../rss_feeds/fixtures/rss_feeds.json',
'subscriptions.json', 'stories.json',
'../../rss_feeds/fixtures/gawker1.json']
def setUp(self):
@ -23,16 +25,16 @@ class ReaderTest(TestCase):
response = self.client.get(reverse('load-feeds'))
content = json.decode(response.content)
self.assertEquals(len(content['feeds']), 1)
self.assertEquals(len(content['feeds']), 10)
self.assertEquals(content['feeds']['1']['feed_title'], 'Gawker')
self.assertEquals(content['folders'], [1, {'Tech': [4, 5, {'Deep Tech': [6, 7]}]}, 2, 3, 8, 9, {'Blogs': [8, 9]}])
self.assertEquals(content['folders'], [{'Tech': [1, 4, 5, {'Deep Tech': [6, 7]}]}, 2, 3, 8, 9, {'Blogs': [8, 9]}, 1])
def test_delete_feed(self):
self.client.login(username='conesus', password='test')
response = self.client.get(reverse('load-feeds'))
feeds = json.decode(response.content)
self.assertEquals(feeds['folders'], [1, {'Tech': [4, 5, {'Deep Tech': [6, 7]}]}, 2, 3, 8, 9, {'Blogs': [8, 9]}])
self.assertEquals(feeds['folders'], [{'Tech': [1, 4, 5, {'Deep Tech': [6, 7]}]}, 2, 3, 8, 9, {'Blogs': [8, 9]}, 1])
# Delete feed
response = self.client.post(reverse('delete-feed'), {'feed_id': 1, 'in_folder': ''})
@ -41,7 +43,7 @@ class ReaderTest(TestCase):
response = self.client.get(reverse('load-feeds'))
feeds = json.decode(response.content)
self.assertEquals(feeds['folders'], [{'Tech': [4, 5, {'Deep Tech': [6, 7]}]}, 2, 3, 8, 9, {'Blogs': [8, 9]}])
self.assertEquals(feeds['folders'], [2, 3, 8, 9, {'Tech': [1, 4, 5, {'Deep Tech': [6, 7]}]}, {'Blogs': [8, 9]}])
# Delete feed
response = self.client.post(reverse('delete-feed'), {'feed_id': 9, 'in_folder': 'Blogs'})
@ -50,7 +52,7 @@ class ReaderTest(TestCase):
response = self.client.get(reverse('load-feeds'))
feeds = json.decode(response.content)
self.assertEquals(feeds['folders'], [{'Tech': [4, 5, {'Deep Tech': [6, 7]}]}, 2, 3, 8, 9, {'Blogs': [8]}])
self.assertEquals(feeds['folders'], [2, 3, 8, 9, {'Tech': [1, 4, 5, {'Deep Tech': [6, 7]}]}, {'Blogs': [8]}])
# Delete feed
response = self.client.post(reverse('delete-feed'), {'feed_id': 5, 'in_folder': 'Tech'})
@ -59,7 +61,7 @@ class ReaderTest(TestCase):
response = self.client.get(reverse('load-feeds'))
feeds = json.decode(response.content)
self.assertEquals(feeds['folders'], [{'Tech': [4, {'Deep Tech': [6, 7]}]}, 2, 3, 8, 9, {'Blogs': [8]}])
self.assertEquals(feeds['folders'], [2, 3, 8, 9, {'Tech': [1, 4, {'Deep Tech': [6, 7]}]}, {'Blogs': [8]}])
# Delete feed
response = self.client.post(reverse('delete-feed'), {'feed_id': 4, 'in_folder': 'Tech'})
@ -68,7 +70,7 @@ class ReaderTest(TestCase):
response = self.client.get(reverse('load-feeds'))
feeds = json.decode(response.content)
self.assertEquals(feeds['folders'], [{'Tech': [{'Deep Tech': [6, 7]}]}, 2, 3, 8, 9, {'Blogs': [8]}])
self.assertEquals(feeds['folders'], [2, 3, 8, 9, {'Tech': [1, {'Deep Tech': [6, 7]}]}, {'Blogs': [8]}])
# Delete feed
response = self.client.post(reverse('delete-feed'), {'feed_id': 8, 'in_folder': ''})
@ -77,7 +79,23 @@ class ReaderTest(TestCase):
response = self.client.get(reverse('load-feeds'))
feeds = json.decode(response.content)
self.assertEquals(feeds['folders'], [{'Tech': [{'Deep Tech': [6, 7]}]}, 2, 3, 9, {'Blogs': [8]}])
self.assertEquals(feeds['folders'], [2, 3, 9, {'Tech': [1, {'Deep Tech': [6, 7]}]}, {'Blogs': [8]}])
def test_delete_feed__multiple_folders(self):
self.client.login(username='conesus', password='test')
response = self.client.get(reverse('load-feeds'))
feeds = json.decode(response.content)
self.assertEquals(feeds['folders'], [{'Tech': [1, 4, 5, {'Deep Tech': [6, 7]}]}, 2, 3, 8, 9, {'Blogs': [8, 9]}, 1])
# Delete feed
response = self.client.post(reverse('delete-feed'), {'feed_id': 1})
response = json.decode(response.content)
self.assertEquals(response['code'], 1)
response = self.client.get(reverse('load-feeds'))
feeds = json.decode(response.content)
self.assertEquals(feeds['folders'], [2, 3, 8, 9, {'Tech': [1, 4, 5, {'Deep Tech': [6, 7]}]}, {'Blogs': [8, 9]}])
def test_load_single_feed(self):
# from django.conf import settings

View file

@ -307,7 +307,7 @@ def load_feeds_flat(request):
feeds = {}
flat_folders = {" ": []}
iphone_version = "3.0"
iphone_version = "2.1"
if include_favicons == 'false': include_favicons = False
if update_counts == 'false': update_counts = False
@ -607,8 +607,9 @@ def load_single_feed(request, feed_id):
if not include_story_content:
del story['story_content']
story_date = localtime_for_timezone(story['story_date'], user.profile.timezone)
story['short_parsed_date'] = format_story_link_date__short(story_date)
story['long_parsed_date'] = format_story_link_date__long(story_date, now)
nowtz = localtime_for_timezone(now, user.profile.timezone)
story['short_parsed_date'] = format_story_link_date__short(story_date, nowtz)
story['long_parsed_date'] = format_story_link_date__long(story_date, nowtz)
if usersub:
story['read_status'] = 1
if (read_filter == 'all' or query) and usersub:
@ -782,12 +783,13 @@ def load_starred_stories(request):
comments=story.comments))
for story in shared_stories])
nowtz = localtime_for_timezone(now, user.profile.timezone)
for story in stories:
story_date = localtime_for_timezone(story['story_date'], user.profile.timezone)
story['short_parsed_date'] = format_story_link_date__short(story_date)
story['long_parsed_date'] = format_story_link_date__long(story_date, now)
story['short_parsed_date'] = format_story_link_date__short(story_date, nowtz)
story['long_parsed_date'] = format_story_link_date__long(story_date, nowtz)
starred_date = localtime_for_timezone(story['starred_date'], user.profile.timezone)
story['starred_date'] = format_story_link_date__long(starred_date, now)
story['starred_date'] = format_story_link_date__long(starred_date, nowtz)
story['read_status'] = 1
story['starred'] = True
story['intelligence'] = {
@ -939,6 +941,7 @@ def load_river_stories__redis(request):
# Just need to format stories
nowtz = localtime_for_timezone(now, user.profile.timezone)
for story in stories:
story['read_status'] = 0
if read_filter == 'all':
@ -946,8 +949,8 @@ def load_river_stories__redis(request):
story['story_hash'] not in unread_feed_story_hashes):
story['read_status'] = 1
story_date = localtime_for_timezone(story['story_date'], user.profile.timezone)
story['short_parsed_date'] = format_story_link_date__short(story_date)
story['long_parsed_date'] = format_story_link_date__long(story_date, now)
story['short_parsed_date'] = format_story_link_date__short(story_date, nowtz)
story['long_parsed_date'] = format_story_link_date__long(story_date, nowtz)
if story['story_hash'] in starred_stories:
story['starred'] = True
starred_date = localtime_for_timezone(starred_stories[story['story_hash']],
@ -1399,7 +1402,7 @@ def add_folder(request):
def delete_feed(request):
feed_id = int(request.POST['feed_id'])
in_folder = request.POST.get('in_folder', None)
if in_folder == ' ':
if not in_folder or in_folder == ' ':
in_folder = ""
user_sub_folders = get_object_or_404(UserSubscriptionFolders, user=request.user)

View file

@ -136,5 +136,302 @@
"email": "samuel@newsblur.com",
"date_joined": "2009-01-04 17:32:58"
}
},
{
"pk": 2,
"model": "rss_feeds.feed",
"fields": {
"premium_subscribers": -1,
"creation": "2011-08-27",
"exception_code": 0,
"last_load_time": 0,
"active_subscribers": 1,
"feed_address": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker2.xml",
"feed_link": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker2.html",
"hash_address_and_link": "2",
"feed_link_locked": true,
"last_update": "2011-08-27 02:45:21",
"etag": null,
"average_stories_per_month": 0,
"feed_title": "Gawker",
"last_modified": null,
"next_scheduled_update": "2011-08-28 14:33:50",
"favicon_color": null,
"stories_last_month": 0,
"active": true,
"favicon_not_found": false,
"has_page_exception": false,
"fetched_once": false,
"days_to_trim": 90,
"num_subscribers": 1,
"last_story_date": "2011-08-28 00:03:50",
"min_to_decay": 720,
"has_feed_exception": false
}
},
{
"pk": 3,
"model": "rss_feeds.feed",
"fields": {
"premium_subscribers": -1,
"creation": "2011-08-27",
"exception_code": 0,
"last_load_time": 0,
"active_subscribers": 1,
"feed_address": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker3.xml",
"feed_link": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker3.html",
"hash_address_and_link": "3",
"feed_link_locked": true,
"last_update": "2011-08-27 02:45:21",
"etag": null,
"average_stories_per_month": 0,
"feed_title": "Gawker",
"last_modified": null,
"next_scheduled_update": "2011-08-28 14:33:50",
"favicon_color": null,
"stories_last_month": 0,
"active": true,
"favicon_not_found": false,
"has_page_exception": false,
"fetched_once": false,
"days_to_trim": 90,
"num_subscribers": 1,
"last_story_date": "2011-08-28 00:03:50",
"min_to_decay": 720,
"has_feed_exception": false
}
},
{
"pk": 4,
"model": "rss_feeds.feed",
"fields": {
"premium_subscribers": -1,
"creation": "2011-08-27",
"exception_code": 0,
"last_load_time": 0,
"active_subscribers": 1,
"feed_address": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker4.xml",
"feed_link": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker4.html",
"hash_address_and_link": "4",
"feed_link_locked": true,
"last_update": "2011-08-27 02:45:21",
"etag": null,
"average_stories_per_month": 0,
"feed_title": "Gawker",
"last_modified": null,
"next_scheduled_update": "2011-08-28 14:33:50",
"favicon_color": null,
"stories_last_month": 0,
"active": true,
"favicon_not_found": false,
"has_page_exception": false,
"fetched_once": false,
"days_to_trim": 90,
"num_subscribers": 1,
"last_story_date": "2011-08-28 00:03:50",
"min_to_decay": 720,
"has_feed_exception": false
}
},
{
"pk": 5,
"model": "rss_feeds.feed",
"fields": {
"premium_subscribers": -1,
"creation": "2011-08-27",
"exception_code": 0,
"last_load_time": 0,
"active_subscribers": 1,
"feed_address": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker5.xml",
"feed_link": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker5.html",
"hash_address_and_link": "5",
"feed_link_locked": true,
"last_update": "2011-08-27 02:45:21",
"etag": null,
"average_stories_per_month": 0,
"feed_title": "Gawker",
"last_modified": null,
"next_scheduled_update": "2011-08-28 14:33:50",
"favicon_color": null,
"stories_last_month": 0,
"active": true,
"favicon_not_found": false,
"has_page_exception": false,
"fetched_once": false,
"days_to_trim": 90,
"num_subscribers": 1,
"last_story_date": "2011-08-28 00:03:50",
"min_to_decay": 720,
"has_feed_exception": false
}
},
{
"pk": 6,
"model": "rss_feeds.feed",
"fields": {
"premium_subscribers": -1,
"creation": "2011-08-27",
"exception_code": 0,
"last_load_time": 0,
"active_subscribers": 1,
"feed_address": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker6.xml",
"feed_link": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker6.html",
"hash_address_and_link": "6",
"feed_link_locked": true,
"last_update": "2011-08-27 02:45:21",
"etag": null,
"average_stories_per_month": 0,
"feed_title": "Gawker",
"last_modified": null,
"next_scheduled_update": "2011-08-28 14:33:50",
"favicon_color": null,
"stories_last_month": 0,
"active": true,
"favicon_not_found": false,
"has_page_exception": false,
"fetched_once": false,
"days_to_trim": 90,
"num_subscribers": 1,
"last_story_date": "2011-08-28 00:03:50",
"min_to_decay": 720,
"has_feed_exception": false
}
},
{
"pk": 7,
"model": "rss_feeds.feed",
"fields": {
"premium_subscribers": -1,
"creation": "2011-08-27",
"exception_code": 0,
"last_load_time": 0,
"active_subscribers": 1,
"feed_address": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker7.xml",
"feed_link": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker7.html",
"hash_address_and_link": "7",
"feed_link_locked": true,
"last_update": "2011-08-27 02:45:21",
"etag": null,
"average_stories_per_month": 0,
"feed_title": "Gawker",
"last_modified": null,
"next_scheduled_update": "2011-08-28 14:33:50",
"favicon_color": null,
"stories_last_month": 0,
"active": true,
"favicon_not_found": false,
"has_page_exception": false,
"fetched_once": false,
"days_to_trim": 90,
"num_subscribers": 1,
"last_story_date": "2011-08-28 00:03:50",
"min_to_decay": 720,
"has_feed_exception": false
}
},
{
"pk": 8,
"model": "rss_feeds.feed",
"fields": {
"premium_subscribers": -1,
"creation": "2011-08-27",
"exception_code": 0,
"last_load_time": 0,
"active_subscribers": 1,
"feed_address": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker8.xml",
"feed_link": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker8.html",
"hash_address_and_link": "8",
"feed_link_locked": true,
"last_update": "2011-08-27 02:45:21",
"etag": null,
"average_stories_per_month": 0,
"feed_title": "Gawker",
"last_modified": null,
"next_scheduled_update": "2011-08-28 14:33:50",
"favicon_color": null,
"stories_last_month": 0,
"active": true,
"favicon_not_found": false,
"has_page_exception": false,
"fetched_once": false,
"days_to_trim": 90,
"num_subscribers": 1,
"last_story_date": "2011-08-28 00:03:50",
"min_to_decay": 720,
"has_feed_exception": false
}
},
{
"pk": 9,
"model": "rss_feeds.feed",
"fields": {
"premium_subscribers": -1,
"creation": "2011-08-27",
"exception_code": 0,
"last_load_time": 0,
"active_subscribers": 1,
"feed_address": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker9.xml",
"feed_link": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker9.html",
"hash_address_and_link": "9",
"feed_link_locked": true,
"last_update": "2011-08-27 02:45:21",
"etag": null,
"average_stories_per_month": 0,
"feed_title": "Gawker",
"last_modified": null,
"next_scheduled_update": "2011-08-28 14:33:50",
"favicon_color": null,
"stories_last_month": 0,
"active": true,
"favicon_not_found": false,
"has_page_exception": false,
"fetched_once": false,
"days_to_trim": 90,
"num_subscribers": 1,
"last_story_date": "2011-08-28 00:03:50",
"min_to_decay": 720,
"has_feed_exception": false
}
},
{
"pk": 56,
"model": "rss_feeds.feed",
"fields": {
"premium_subscribers": -1,
"creation": "2011-08-27",
"exception_code": 0,
"last_load_time": 0,
"active_subscribers": 1,
"feed_address": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker56.xml",
"feed_link": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/gawker56.html",
"hash_address_and_link": "56",
"feed_link_locked": true,
"last_update": "2011-08-27 02:45:21",
"etag": null,
"average_stories_per_month": 0,
"feed_title": "Gawker",
"last_modified": null,
"next_scheduled_update": "2011-08-28 14:33:50",
"favicon_color": null,
"stories_last_month": 0,
"active": true,
"favicon_not_found": false,
"has_page_exception": false,
"fetched_once": false,
"days_to_trim": 90,
"num_subscribers": 1,
"last_story_date": "2011-08-28 00:03:50",
"min_to_decay": 720,
"has_feed_exception": false
}
}
]

View file

@ -912,6 +912,7 @@ class Feed(models.Model):
story_content = strip_comments(story_content)
story_tags = self.get_tags(story)
story_link = self.get_permalink(story)
replace_story_date = False
try:
existing_story, story_has_changed = _1(story, story_content, existing_stories)
@ -993,7 +994,8 @@ class Feed(models.Model):
existing_story.story_tags = story_tags
# Do not allow publishers to change the story date once a story is published.
# Leads to incorrect unread story counts.
# existing_story.story_date = story.get('published') # No, don't
if replace_story_date:
existing_story.story_date = story.get('published') # Really shouldn't do this.
existing_story.extract_image_urls()
try:

View file

@ -3001,14 +3001,17 @@ class MActivity(mongo.Document):
@classmethod
def remove_shared_story(cls, user_id, story_feed_id, story_id):
params = dict(user_id=user_id,
category='sharedstory',
feed_id="social:%s" % user_id,
story_feed_id=story_feed_id,
content_id=story_id)
try:
a = cls.objects.get(user_id=user_id,
category='sharedstory',
feed_id="social:%s" % user_id,
story_feed_id=story_feed_id,
content_id=story_id)
a = cls.objects.get(**params)
except cls.DoesNotExist:
return
except cls.MultipleObjectsReturned:
a = cls.objects.filter(**params)
a.delete()

View file

@ -119,12 +119,13 @@ def load_social_stories(request, user_id, username=None):
comments=story.comments))
for story in shared_stories])
nowtz = localtime_for_timezone(now, user.profile.timezone)
for story in stories:
story['social_user_id'] = social_user_id
# story_date = localtime_for_timezone(story['story_date'], user.profile.timezone)
shared_date = localtime_for_timezone(story['shared_date'], user.profile.timezone)
story['short_parsed_date'] = format_story_link_date__short(shared_date)
story['long_parsed_date'] = format_story_link_date__long(shared_date)
story['short_parsed_date'] = format_story_link_date__short(shared_date, nowtz)
story['long_parsed_date'] = format_story_link_date__long(shared_date, nowtz)
story['read_status'] = 1
if (read_filter == 'all' or query) and socialsub:
@ -270,13 +271,14 @@ def load_river_blurblog(request):
classifier_tags = []
# Just need to format stories
nowtz = localtime_for_timezone(now, user.profile.timezone)
for story in stories:
story['read_status'] = 0
if story['story_hash'] not in unread_feed_story_hashes:
story['read_status'] = 1
story_date = localtime_for_timezone(story['story_date'], user.profile.timezone)
story['short_parsed_date'] = format_story_link_date__short(story_date)
story['long_parsed_date'] = format_story_link_date__long(story_date, now)
story['short_parsed_date'] = format_story_link_date__short(story_date, nowtz)
story['long_parsed_date'] = format_story_link_date__long(story_date, nowtz)
if story['story_hash'] in starred_stories:
story['starred'] = True
starred_date = localtime_for_timezone(starred_stories[story['story_hash']], user.profile.timezone)
@ -521,7 +523,9 @@ def mark_story_as_shared(request):
source_user_id = request.POST.get('source_user_id')
relative_user_id = request.POST.get('relative_user_id') or request.user.pk
post_to_services = request.POST.getlist('post_to_services')
format = request.REQUEST.get('format', 'json')
format = request.REQUEST.get('format', 'json')
now = datetime.datetime.now()
nowtz = localtime_for_timezone(now, request.user.profile.timezone)
MSocialProfile.get_user(request.user.pk)
@ -576,8 +580,8 @@ def mark_story_as_shared(request):
story['shared_by_user'] = True
story['shared'] = True
shared_date = localtime_for_timezone(shared_story['shared_date'], request.user.profile.timezone)
story['short_parsed_date'] = format_story_link_date__short(shared_date)
story['long_parsed_date'] = format_story_link_date__long(shared_date)
story['short_parsed_date'] = format_story_link_date__short(shared_date, nowtz)
story['long_parsed_date'] = format_story_link_date__long(shared_date, nowtz)
if post_to_services:
for service in post_to_services:

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.newsblur"
android:versionCode="43"
android:versionName="2.5.0" >
android:versionCode="50"
android:versionName="3.0.0" >
<uses-sdk
android:minSdkVersion="8"

View file

@ -10,16 +10,23 @@
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Android 4.2.2" jdkType="Android SDK" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="android-support-v4" level="project" />
<orderEntry type="library" name="actionbarsherlock" level="project" />
<orderEntry type="library" name="gson-2.2.3" level="project" />
<orderEntry type="library" name="classes" level="project" />
<orderEntry type="module" module-name="ActionBarSherlock" />
<orderEntry type="library" name="android-support-v4" level="project" />
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/libs/ActionBarSherlock/bin/actionbarsherlock.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
</component>
</module>

View file

@ -11,12 +11,11 @@
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Android 4.2.2" jdkType="Android SDK" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="android-support-v4" level="project" />
</component>
</module>

View file

@ -9,4 +9,4 @@
android.library=true
# Project target.
target=android-17
target=android-14

View file

@ -11,5 +11,5 @@
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-17
target=android-14
android.library.reference.1=libs/ActionBarSherlock

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 605 B

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:shape="rectangle">
<solid android:color="@color/feed_background_selected_start"/>
</shape>
</item>
<item
android:top="0.5dp"
android:bottom="0.5dp">
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/feed_background_selected_end"/>
</shape>
</item>
</layer-list>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="90"
android:type="linear"
android:startColor="@color/folder_background_start"
android:endColor="@color/folder_background_end"/>
</shape>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="90"
android:type="linear"
android:startColor="@color/folder_background_selected_start"
android:endColor="@color/folder_background_selected_end"/>
</shape>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_pressed="true" android:drawable="@drawable/gradient_activation_highlight" />
<item android:state_pressed="true" android:drawable="@drawable/feed_background_highlight" />
<item android:drawable="@drawable/feed_background_default" />
</selector>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_pressed="true" android:drawable="@drawable/gradient_activation_highlight" />
<item android:drawable="@drawable/gradient_background_default" />
<item android:state_pressed="true" android:drawable="@drawable/folder_background_highlight" />
<item android:drawable="@drawable/folder_background_default" />
</selector>

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_pressed="true" android:drawable="@drawable/overlay_right_pressed" />
<item android:state_enabled="false" android:drawable="@drawable/overlay_right_disabled" />
<item android:state_enabled="true" android:drawable="@drawable/overlay_right_enabled" />
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_pressed="true" android:drawable="@drawable/overlay_right_done_pressed" />
<item android:state_enabled="true" android:drawable="@drawable/overlay_right_done_enabled" />
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_pressed="true" android:drawable="@drawable/overlay_send_pressed" />
<item android:state_enabled="true" android:drawable="@drawable/overlay_send_enabled" />
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_pressed="true" android:drawable="@drawable/overlay_story_pressed" />
<item android:state_enabled="true" android:drawable="@drawable/overlay_story_enabled" />
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_pressed="true" android:drawable="@drawable/overlay_text_pressed" />
<item android:state_enabled="true" android:drawable="@drawable/overlay_text_enabled" />
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/story_background_highlight"/>
<item android:drawable="@drawable/story_background_default"/>
</selector>

View file

Before

Width:  |  Height:  |  Size: 517 B

After

Width:  |  Height:  |  Size: 517 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/story_background"/>
</shape>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:shape="rectangle">
<solid android:color="@color/story_background_start"/>
</shape>
</item>
<item
android:top="0.5dp"
android:bottom="0.5dp">
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/story_background_end"/>
</shape>
</item>
</layer-list>

View file

@ -14,6 +14,42 @@
android:layout_height="1dip"
android:layout_alignParentTop="true" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp"
android:layout_marginLeft="8dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true" >
<Button
android:id="@+id/reading_overlay_text"
android:layout_width="110.6dp"
android:layout_height="40dp"
android:background="@drawable/selector_overlay_bg_text"
android:text="@string/overlay_text"
android:textColor="@color/half_darkgray"
android:textSize="14sp"
android:gravity="left|center_vertical"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:paddingLeft="40dp"
android:paddingRight="6dp"
android:onClick="overlayText" />
<Button
android:id="@+id/reading_overlay_send"
android:layout_width="51.8dp"
android:layout_height="40dp"
android:background="@drawable/selector_overlay_bg_send"
android:textSize="14sp"
android:padding="6dp"
android:layout_marginLeft="1dp"
android:onClick="overlaySend" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -25,8 +61,8 @@
<Button
android:id="@+id/reading_overlay_left"
android:layout_width="52dp"
android:layout_height="41dp"
android:layout_width="50.6dp"
android:layout_height="40dp"
android:background="@drawable/selector_overlay_bg_left"
android:textSize="14sp"
android:padding="6dp"
@ -35,8 +71,8 @@
<Button
android:id="@+id/reading_overlay_right"
android:layout_width="125dp"
android:layout_height="41dp"
android:layout_width="122.4dp"
android:layout_height="40dp"
android:background="@drawable/selector_overlay_bg_right"
android:text="@string/overlay_next"
android:textColor="@color/half_darkgray"
@ -46,22 +82,35 @@
</LinearLayout>
<TextView
android:id="@+id/reading_overlay_count"
android:text=""
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="100dp"
android:layout_marginBottom="18dp"
android:background="@drawable/neutral_count_rect"
android:paddingLeft="3dp"
android:paddingRight="3dp"
android:shadowDy="1"
android:shadowRadius="1"
android:textColor="@color/white"
android:textSize="14sp"
android:textStyle="bold"
<com.newsblur.view.ProgressCircle
android:id="@+id/reading_overlay_progress"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginRight="98dp"
android:layout_marginBottom="16dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true" />
android:layout_alignParentRight="true"
android:onClick="overlayCount" />
<ProgressBar
android:id="@+id/reading_overlay_progress_right"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginRight="98dp"
android:layout_marginBottom="16dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:indeterminate="true" />
<ProgressBar
android:id="@+id/reading_overlay_progress_left"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginLeft="90dp"
android:layout_marginBottom="16dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:indeterminate="true" />
</RelativeLayout>

View file

@ -19,4 +19,13 @@
android:defaultValue="@string/default_read_filter_value" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/settings_social">
<CheckBoxPreference
android:defaultValue="true"
android:key="show_public_comments"
android:title="@string/settings_show_public_comments" >
</CheckBoxPreference>
</PreferenceCategory>
</PreferenceScreen>

View file

@ -91,7 +91,7 @@
android:layout_toRightOf="@id/comment_user_image"
android:textColor="@color/newsblur_blue"
android:textSize="14sp" />
<TextView
android:id="@+id/comment_text"
android:layout_width="match_parent"
@ -105,6 +105,18 @@
android:textColor="@color/darkgray"
android:textSize="14dp" />
<TextView
android:id="@+id/comment_location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/comment_text"
android:layout_marginRight="10dp"
android:layout_marginBottom="4dp"
android:layout_toRightOf="@id/comment_user_image"
android:textColor="@color/lightgray"
android:textSize="12dp"
/>
<com.newsblur.view.FlowLayout
xmlns:newsblur="http://schemas.android.com/apk/res/com.newsblur"
android:id="@+id/comment_favourite_avatars"
@ -121,6 +133,7 @@
<LinearLayout
android:id="@+id/comment_replies_container"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
@ -129,6 +142,6 @@
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="5dp"
android:background="#A6A6A6" />
android:background="#F0F0F0" />
</LinearLayout>

View file

@ -5,7 +5,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="15dp" >
android:layout_marginTop="15dp"
android:layout_marginBottom="15dp"
android:baselineAligned="false" >
<Button
android:id="@+id/share_story_button"
@ -20,7 +22,7 @@
android:paddingBottom="6dp"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:drawableLeft="@drawable/share_half"
android:drawableLeft="@drawable/share_icon"
android:text="@string/share_this" />
<Button
@ -36,7 +38,7 @@
android:paddingBottom="6dp"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:drawableLeft="@drawable/clock_half"
android:drawableLeft="@drawable/clock"
android:text="@string/save_this" />
</LinearLayout>
@ -45,51 +47,103 @@
android:id="@+id/reading_shared_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginBottom="50dp" >
<LinearLayout
android:id="@+id/reading_friend_comment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:id="@+id/reading_friend_comment_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/darkgray"
android:id="@+id/reading_friend_comment_total"
android:paddingTop="3dp"
android:textStyle="bold"
android:paddingBottom="3dp" />
</LinearLayout>
android:visibility="gone"
android:orientation="vertical">
<View
android:id="@+id/comment_divider"
android:layout_width="match_parent"
android:layout_height="3dp"
android:layout_below="@id/reading_friend_comment_container"
android:background="@drawable/divider_light" />
<View
android:id="@+id/reading_friend_header_top_border"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="12dp"
android:background="@color/lightgray"/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:textColor="@color/darkgray"
android:id="@+id/reading_friend_comment_total"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:textStyle="bold"
android:textSize="10sp"
android:background="@drawable/gradient_background_default"
/>
<View
android:id="@+id/reading_friend_header_bottom_border"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@id/reading_friend_comment_total"
android:background="@color/lightgray"/>
</LinearLayout>
<LinearLayout
android:id="@+id/reading_public_comment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/comment_divider"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:id="@+id/reading_friend_comment_container"
android:layout_below="@id/reading_friend_comment_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/darkgray"
android:paddingTop="3dp"
android:textStyle="bold"
android:id="@+id/reading_public_comment_total"
android:paddingBottom="3dp" />
</LinearLayout>
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:orientation="vertical">
</LinearLayout>
<LinearLayout
android:id="@+id/reading_public_comment_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/comment_divider"
android:visibility="gone"
android:orientation="vertical">
<View
android:id="@+id/reading_public_header_top_border"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="12dp"
android:background="@color/lightgray"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/darkgray"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:textStyle="bold"
android:textSize="10sp"
android:background="@drawable/gradient_background_default"
android:id="@+id/reading_public_comment_total" />
<View
android:id="@+id/reading_public_header_bottom_border"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@id/reading_public_comment_total"
android:background="@color/lightgray"/>
</LinearLayout>
<LinearLayout
android:id="@+id/reading_public_comment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_below="@id/reading_public_comment_header"
android:orientation="vertical">
</LinearLayout>
</RelativeLayout>
</merge>

View file

@ -2,7 +2,8 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@drawable/selector_folder_background">
android:background="@drawable/selector_folder_background"
android:addStatesFromChildren="true" >
<ImageView
android:id="@+id/row_folder_icon"
@ -86,7 +87,7 @@
android:shadowDy="1"
android:shadowRadius="1"
android:maxLines="1"
android:ellipsize="end"/>
android:ellipsize="end" />
<View
android:layout_height="1dp"

View file

@ -2,7 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/item_background"
android:background="@drawable/selector_story_background"
android:orientation="horizontal" >
<RelativeLayout

View file

@ -2,7 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/selector_feed_background"
android:background="@drawable/selector_story_background"
android:orientation="horizontal" >
<RelativeLayout
@ -20,7 +20,6 @@
android:layout_height="match_parent"
android:layout_toRightOf="@id/row_item_favicon_borderbar_1" />
<RelativeLayout
android:id="@+id/row_item_title_container"
android:layout_width="match_parent"

View file

@ -21,11 +21,11 @@
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_toRightOf="@id/row_saved_icon"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:paddingBottom="9dp"
android:paddingTop="9dp"
android:text="@string/saved_stories_row_title"
android:textColor="@color/folder_text"
android:textSize="14dp"
android:textSize="13dp"
android:textStyle="bold" />
<TextView
@ -37,7 +37,10 @@
android:layout_marginRight="12dp"
android:background="@drawable/saved_count_rect"
android:gravity="center"
android:padding="3dp"
android:paddingLeft="3dp"
android:paddingRight="3dp"
android:paddingTop="1dp"
android:paddingBottom="2dp"
android:shadowColor="@color/saved_drop_shadow"
android:shadowDy="1"
android:shadowRadius="1"

View file

@ -2,7 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/selector_feed_background"
android:background="@drawable/selector_story_background"
android:orientation="horizontal" >
<RelativeLayout

View file

@ -6,16 +6,21 @@
<color name="midgray">#898989</color>
<color name="lightgray">#ccc</color>
<color name="folder_background_end">#E9EBE4</color>
<color name="folder_background_start">#DDE0D7</color>
<color name="folder_background_selected_end">#fdfcca</color>
<color name="folder_background_selected_start">#fbec8c</color>
<color name="folder_background_end">#E9EBE4</color>
<color name="folder_background_start">#DDE0D7</color>
<color name="folder_background_selected_end">#D9DBD4</color>
<color name="folder_background_selected_start">#CDD0C7</color>
<color name="folder_text">#4C4C4C</color>
<color name="folder_border_top">#FDFDFD</color>
<color name="folder_border_bottom">#B7BBAA</color>
<color name="feed_background">#F7F8F5</color>
<color name="feed_background_end">#303030</color>
<color name="feed_background_start">#505050</color>
<color name="feed_background_selected_end">#FFFFD2</color>
<color name="feed_background_selected_start">#E3D0AE</color>
<color name="story_background">#F7F8F5</color>
<color name="story_background_end">#FFFDEF</color>
<color name="story_background_start">#DFDDCF</color>
<color name="story_title_unread">#333333</color>
<color name="story_title_read">#808080</color>
@ -70,6 +75,9 @@
<color name="newsblur_blue">#0b445a</color>
<color name="progress_circle_complete">#9a9c96</color>
<color name="progress_circle_remaining">#dbe4e1</color>
<!-- To use a selector on a bg color, the options must be drawables, not colors. -->
<drawable name="toggle_bg_selected">#fdfdfd</drawable>
<drawable name="toggle_bg_normal">#dfe1dd</drawable>

View file

@ -56,10 +56,15 @@
<string name="share_newsblur">Share \"%s\" to your Blurblog?</string>
<string name="save_this">SAVE THIS STORY</string>
<string name="unsave_this">UNSAVE THIS STORY</string>
<string name="share_this">SHARE THIS STORY</string>
<string name="overlay_next">NEXT</string>
<string name="overlay_done">DONE</string>
<string name="overlay_count_toast_N">%d unread stories</string>
<string name="overlay_count_toast_1">1 unread story</string>
<string name="overlay_text">TEXT</string>
<string name="overlay_story">STORY</string>
<string name="reply_to">Reply to \"%s\"</string>
@ -94,6 +99,7 @@
<string name="menu_sharenewsblur">Share this story</string>
<string name="menu_textsize">Adjust text size</string>
<string name="menu_save_story">Save this story</string>
<string name="menu_unsave_story">Unsave this story</string>
<string name="menu_mark_previous_stories_as_read">Mark previous as read</string>
<string name="menu_mark_story_as_read">Mark as read</string>
<string name="menu_mark_unread">Mark as unread</string>
@ -106,10 +112,14 @@
<string name="toast_story_saved">Story saved</string>
<string name="toast_story_save_error">Error marking story as saved.</string>
<string name="toast_story_unsaved">Story unsaved</string>
<string name="toast_story_unsave_error">Error marking story as unsaved.</string>
<string name="toast_story_unread">Story marked as unread</string>
<string name="toast_story_unread_error">Error marking story as unread</string>
<string name="toast_unread_search_error">Could not load next unread story</string>
<string name="toast_feed_deleted">Feed deleted</string>
<string name="toast_feed_delete_error">There was an error deleting the feed.</string>
@ -184,4 +194,6 @@
<item>Mark entire folder read</item>
<item>Cancel</item>
</string-array>
<string name="settings_social">Social</string>
<string name="settings_show_public_comments">Show Public Comments</string>
</resources>

View file

@ -1,6 +1,7 @@
package com.newsblur.activity;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import com.newsblur.R;
@ -8,6 +9,7 @@ import com.newsblur.database.DatabaseConstants;
import com.newsblur.database.FeedProvider;
import com.newsblur.database.MixedFeedsReadingAdapter;
import com.newsblur.service.SyncService;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefConstants;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.StoryOrder;
@ -21,6 +23,13 @@ public class AllSharedStoriesReading extends Reading {
StoryOrder storyOrder = PrefsUtils.getStoryOrderForFolder(this, PrefConstants.ALL_SHARED_STORIES_FOLDER_NAME);
stories = contentResolver.query(FeedProvider.ALL_SHARED_STORIES_URI, null, DatabaseConstants.getStorySelectionFromState(currentState), null, DatabaseConstants.getStorySortOrder(storyOrder));
setTitle(getResources().getString(R.string.all_shared_stories));
Cursor folderCursor = contentResolver.query(FeedProvider.SOCIALCOUNT_URI, null, DatabaseConstants.getBlogSelectionFromState(currentState), null, null);
int unreadCount = FeedUtils.getCursorUnreadCount(folderCursor, currentState);
folderCursor.close();
this.startingUnreadCount = unreadCount;
this.currentUnreadCount = unreadCount;
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver(), stories);
setupPager();
@ -30,7 +39,7 @@ public class AllSharedStoriesReading extends Reading {
@Override
public void triggerRefresh(int page) {
setSupportProgressBarIndeterminateVisibility(true);
updateSyncStatus(true);
final Intent intent = new Intent(Intent.ACTION_SYNC, null, this, SyncService.class);
intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, syncFragment.receiver);
intent.putExtra(SyncService.EXTRA_TASK_TYPE, SyncService.TaskType.MULTISOCIALFEED_UPDATE);

View file

@ -9,6 +9,7 @@ import com.newsblur.database.DatabaseConstants;
import com.newsblur.database.FeedProvider;
import com.newsblur.database.MixedFeedsReadingAdapter;
import com.newsblur.service.SyncService;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefConstants;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.StoryOrder;
@ -22,6 +23,13 @@ public class AllStoriesReading extends Reading {
StoryOrder storyOrder = PrefsUtils.getStoryOrderForFolder(this, PrefConstants.ALL_STORIES_FOLDER_NAME);
stories = contentResolver.query(FeedProvider.ALL_STORIES_URI, null, DatabaseConstants.getStorySelectionFromState(currentState), null, DatabaseConstants.getStorySortOrder(storyOrder));
setTitle(getResources().getString(R.string.all_stories_row_title));
Cursor folderCursor = contentResolver.query(FeedProvider.FEED_COUNT_URI, null, DatabaseConstants.getBlogSelectionFromState(currentState), null, null);
int unreadCount = FeedUtils.getCursorUnreadCount(folderCursor, currentState);
folderCursor.close();
this.startingUnreadCount = unreadCount;
this.currentUnreadCount = unreadCount;
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver(), stories);
setupPager();
@ -31,7 +39,7 @@ public class AllStoriesReading extends Reading {
@Override
public void triggerRefresh(int page) {
setSupportProgressBarIndeterminateVisibility(true);
updateSyncStatus(true);
final Intent intent = new Intent(Intent.ACTION_SYNC, null, this, SyncService.class);
intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, syncFragment.receiver);
intent.putExtra(SyncService.EXTRA_TASK_TYPE, SyncService.TaskType.MULTIFEED_UPDATE);

View file

@ -42,7 +42,9 @@ public class FeedReading extends Reading {
feed = Feed.fromCursor(feedCursor);
setTitle(feed.title);
this.unreadCount = FeedUtils.getFeedUnreadCount(this.feed, this.currentState);
int unreadCount = FeedUtils.getFeedUnreadCount(this.feed, this.currentState);
this.startingUnreadCount = unreadCount;
this.currentUnreadCount = unreadCount;
readingAdapter = new FeedReadingAdapter(getSupportFragmentManager(), feed, stories, classifier);
@ -53,7 +55,7 @@ public class FeedReading extends Reading {
@Override
public void triggerRefresh(int page) {
setSupportProgressBarIndeterminateVisibility(true);
updateSyncStatus(true);
final Intent intent = new Intent(Intent.ACTION_SYNC, null, this, SyncService.class);
intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, syncFragment.receiver);
intent.putExtra(SyncService.EXTRA_TASK_TYPE, SyncService.TaskType.FEED_UPDATE);

View file

@ -1,6 +1,7 @@
package com.newsblur.activity;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
@ -8,6 +9,7 @@ import com.newsblur.database.DatabaseConstants;
import com.newsblur.database.FeedProvider;
import com.newsblur.database.MixedFeedsReadingAdapter;
import com.newsblur.service.SyncService;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils;
public class FolderReading extends Reading {
@ -26,6 +28,12 @@ public class FolderReading extends Reading {
Uri storiesURI = FeedProvider.MULTIFEED_STORIES_URI;
stories = contentResolver.query(storiesURI, null, DatabaseConstants.getStorySelectionFromState(currentState), feedIds, null);
Cursor folderCursor = contentResolver.query(FeedProvider.FOLDERS_URI.buildUpon().appendPath(folderName).build(), null, null, new String[] { DatabaseConstants.getFolderSelectionFromState(currentState) }, null);
int unreadCount = FeedUtils.getCursorUnreadCount(folderCursor, currentState);
folderCursor.close();
this.startingUnreadCount = unreadCount;
this.currentUnreadCount = unreadCount;
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver(), stories);
setupPager();
@ -35,7 +43,7 @@ public class FolderReading extends Reading {
@Override
public void triggerRefresh(int page) {
setSupportProgressBarIndeterminateVisibility(true);
updateSyncStatus(true);
final Intent intent = new Intent(Intent.ACTION_SYNC, null, this, SyncService.class);
intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, syncFragment.receiver);
intent.putExtra(SyncService.EXTRA_TASK_TYPE, SyncService.TaskType.MULTIFEED_UPDATE);

View file

@ -154,7 +154,9 @@ public class Main extends NbFragmentActivity implements StateChangedListener, Sy
public void updateAfterSync() {
folderFeedList.hasUpdated();
setSupportProgressBarIndeterminateVisibility(false);
menu.findItem(R.id.menu_refresh).setEnabled(true);
MenuItem refreshItem = menu.findItem(R.id.menu_refresh);
if (refreshItem != null) refreshItem.setEnabled(true);
}
/**

View file

@ -1,28 +1,36 @@
package com.newsblur.activity;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import android.content.ContentResolver;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.OnPageChangeListener;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
import android.widget.Toast;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
import com.actionbarsherlock.view.Window;
import com.newsblur.R;
import com.newsblur.activity.Main;
import com.newsblur.domain.Story;
import com.newsblur.domain.UserDetails;
import com.newsblur.fragment.ReadingItemFragment;
@ -30,6 +38,7 @@ import com.newsblur.fragment.ShareDialogFragment;
import com.newsblur.fragment.SyncUpdateFragment;
import com.newsblur.fragment.TextSizeDialogFragment;
import com.newsblur.network.APIManager;
import com.newsblur.network.domain.StoryTextResponse;
import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefConstants;
@ -38,7 +47,7 @@ import com.newsblur.util.UIUtils;
import com.newsblur.util.ViewUtils;
import com.newsblur.view.NonfocusScrollview.ScrollChangeListener;
public abstract class Reading extends NbFragmentActivity implements OnPageChangeListener, SyncUpdateFragment.SyncUpdateFragmentInterface, OnSeekBarChangeListener, ScrollChangeListener {
public abstract class Reading extends NbFragmentActivity implements OnPageChangeListener, SyncUpdateFragment.SyncUpdateFragmentInterface, OnSeekBarChangeListener, ScrollChangeListener, FeedUtils.ActionCompletionListener {
public static final String EXTRA_FEED = "feed_selected";
public static final String EXTRA_POSITION = "feed_position";
@ -48,15 +57,26 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
public static final String EXTRA_FEED_IDS = "feed_ids";
private static final String TEXT_SIZE = "textsize";
private static final int OVERLAY_RANGE_TOP_DP = 50;
private static final int OVERLAY_RANGE_TOP_DP = 45;
private static final int OVERLAY_RANGE_BOT_DP = 60;
/** The minimum screen width (in DP) needed to show all the overlay controls. */
private static final int OVERLAY_MIN_WIDTH_DP = 355;
/** The longest time (in seconds) the UI will wait for API pages to load while
searching for the next unread story. */
private static final long UNREAD_SEARCH_LOAD_WAIT_SECONDS = 30;
private final Object UNREAD_SEARCH_MUTEX = new Object();
private CountDownLatch unreadSearchLatch;
protected int passedPosition;
protected int currentState;
protected ViewPager pager;
protected Button overlayLeft, overlayRight;
protected TextView overlayCount;
protected ProgressBar overlayProgress, overlayProgressRight, overlayProgressLeft;
protected Button overlayText, overlaySend;
protected FragmentManager fragmentManager;
protected ReadingAdapter readingAdapter;
protected ContentResolver contentResolver;
@ -68,28 +88,33 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
private int currentApiPage = 0;
private Set<Story> storiesToMarkAsRead;
// subclasses may set this to a nonzero value to enable the unread count overlay
protected int unreadCount = 0;
// unread counts for the circular progress overlay. set to nonzero to activate the progress indicator overlay
protected int startingUnreadCount = 0;
protected int currentUnreadCount = 0;
// keep a local cache of stories we have viewed within this activity cycle. We need
// this to track unread counts since it would be too costly to query and update the DB
// on every page change.
// A list of stories we have marked as read during this reading session. Needed to help keep track of unread
// counts since it would be too costly to query and update the DB on every page change.
private Set<Story> storiesAlreadySeen;
private float overlayRangeTopPx;
private float overlayRangeBotPx;
private List<Story> pageHistory;
private Boolean textMode = false;
@Override
protected void onCreate(Bundle savedInstanceBundle) {
requestWindowFeature(Window.FEATURE_PROGRESS);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
super.onCreate(savedInstanceBundle);
setContentView(R.layout.activity_reading);
this.overlayLeft = (Button) findViewById(R.id.reading_overlay_left);
this.overlayRight = (Button) findViewById(R.id.reading_overlay_right);
this.overlayCount = (TextView) findViewById(R.id.reading_overlay_count);
this.overlayProgress = (ProgressBar) findViewById(R.id.reading_overlay_progress);
this.overlayProgressRight = (ProgressBar) findViewById(R.id.reading_overlay_progress_right);
this.overlayProgressLeft = (ProgressBar) findViewById(R.id.reading_overlay_progress_left);
this.overlayText = (Button) findViewById(R.id.reading_overlay_text);
this.overlaySend = (Button) findViewById(R.id.reading_overlay_send);
fragmentManager = getSupportFragmentManager();
@ -107,11 +132,9 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
this.overlayRangeTopPx = (float) UIUtils.convertDPsToPixels(this, OVERLAY_RANGE_TOP_DP);
this.overlayRangeBotPx = (float) UIUtils.convertDPsToPixels(this, OVERLAY_RANGE_BOT_DP);
// the unread count overlay defaults to neutral colour. set it to positive if we are in focus mode
if (this.currentState == AppConstants.STATE_BEST) {
ViewUtils.setViewBackground(this.overlayCount, R.drawable.positive_count_rect);
}
this.pageHistory = new ArrayList<Story>();
enableProgressCircle(overlayProgressLeft, false);
}
/**
@ -129,9 +152,13 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
pager.setPageMargin(UIUtils.convertDPsToPixels(getApplicationContext(), 1));
pager.setPageMarginDrawable(R.drawable.divider_light);
pager.setOnPageChangeListener(this);
pager.setAdapter(readingAdapter);
pager.setCurrentItem(passedPosition);
// setCurrentItem sometimes fails to pass the first page to the callback, so call it manually
// for the first one.
this.onPageSelected(passedPosition);
this.enableOverlays();
}
@ -143,6 +170,14 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
Story story = readingAdapter.getStory(pager.getCurrentItem());
menu.findItem(R.id.menu_reading_save).setTitle(story.starred ? R.string.menu_unsave_story : R.string.menu_save_story);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int currentItem = pager.getCurrentItem();
@ -161,8 +196,7 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
return true;
} else if (item.getItemId() == R.id.menu_reading_sharenewsblur) {
if (story != null) {
ReadingItemFragment currentFragment = (ReadingItemFragment) readingAdapter.instantiateItem(pager, currentItem);
DialogFragment newFragment = ShareDialogFragment.newInstance(currentFragment, story, currentFragment.previouslySavedShareText);
DialogFragment newFragment = ShareDialogFragment.newInstance(getReadingFragment(), story, getReadingFragment().previouslySavedShareText);
newFragment.show(getSupportFragmentManager(), "dialog");
}
return true;
@ -175,7 +209,11 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
textSize.show(getSupportFragmentManager(), TEXT_SIZE);
return true;
} else if (item.getItemId() == R.id.menu_reading_save) {
FeedUtils.saveStory(story, Reading.this, apiManager);
if (story.starred) {
FeedUtils.unsaveStory(story, Reading.this, apiManager, this);
} else {
FeedUtils.saveStory(story, Reading.this, apiManager, this);
}
return true;
} else if (item.getItemId() == R.id.menu_reading_markunread) {
this.markStoryUnread(story);
@ -185,6 +223,14 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
}
}
@Override
public void actionCompleteCallback() {
stories.requery();
ReadingItemFragment fragment = getReadingFragment();
fragment.updateStory(readingAdapter.getStory(pager.getCurrentItem()));
fragment.updateSaveButton();
}
// interface OnPageChangeListener
@Override
@ -197,12 +243,18 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
@Override
public void onPageSelected(int position) {
this.enableOverlays();
if (readingAdapter.getStory(position) != null) {
addStoryToMarkAsRead(readingAdapter.getStory(position));
Story story = readingAdapter.getStory(position);
if (story != null) {
synchronized (this.pageHistory) {
// if the history is just starting out or the last entry in it isn't this page, add this page
if ((this.pageHistory.size() < 1) || (!story.equals(this.pageHistory.get(this.pageHistory.size()-1)))) {
this.pageHistory.add(story);
}
}
addStoryToMarkAsRead(story);
checkStoryCount(position);
}
this.enableOverlays();
}
// interface ScrollChangeListener
@ -231,36 +283,69 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
private void setOverlayAlpha(float a) {
UIUtils.setViewAlpha(this.overlayLeft, a);
UIUtils.setViewAlpha(this.overlayRight, a);
if (this.unreadCount > 0) {
UIUtils.setViewAlpha(this.overlayCount, a);
} else {
UIUtils.setViewAlpha(this.overlayCount, 0.0f);
}
UIUtils.setViewAlpha(this.overlayProgress, a);
UIUtils.setViewAlpha(this.overlayProgressLeft, a);
UIUtils.setViewAlpha(this.overlayProgressRight, a);
UIUtils.setViewAlpha(this.overlayText, a);
UIUtils.setViewAlpha(this.overlaySend, a);
}
/**
* Check and correct the display status of the overlays. Call this any time
* an event happens that might change out list position.
* an event happens that might change our list position.
*/
private void enableOverlays() {
int page = this.pager.getCurrentItem();
this.overlayLeft.setEnabled(page > 0);
this.overlayRight.setEnabled(page < (this.readingAdapter.getCount()-1));
this.overlayRight.setText((page < (this.readingAdapter.getCount()-1)) ? R.string.overlay_next : R.string.overlay_done);
// check to see if the device even has room for all the overlays, moving some to overflow if not
int widthPX = findViewById(android.R.id.content).getMeasuredWidth();
if (widthPX != 0) {
float widthDP = UIUtils.px2dp(this, widthPX);
if ( widthDP < OVERLAY_MIN_WIDTH_DP ){
this.overlaySend.setVisibility(View.GONE);
} else {
this.overlaySend.setVisibility(View.VISIBLE);
}
}
this.overlayLeft.setEnabled(this.getLastReadPosition(false) != -1);
this.overlayRight.setText((this.currentUnreadCount > 0) ? R.string.overlay_next : R.string.overlay_done);
this.overlayRight.setBackgroundResource((this.currentUnreadCount > 0) ? R.drawable.selector_overlay_bg_right : R.drawable.selector_overlay_bg_right_done);
if (this.startingUnreadCount == 0 ) {
// sessions with no unreads just show a full progress bar
this.overlayProgress.setMax(1);
this.overlayProgress.setProgress(1);
} else {
int unreadProgress = this.startingUnreadCount - this.currentUnreadCount;
this.overlayProgress.setMax(this.startingUnreadCount);
this.overlayProgress.setProgress(unreadProgress);
}
this.overlayProgress.invalidate();
// make sure we start in story mode and the ui reflects it
synchronized (textMode) {
enableStoryMode();
}
this.overlayCount.setText(Integer.toString(this.unreadCount));
this.setOverlayAlpha(1.0f);
}
public void onWindowFocusChanged(boolean hasFocus) {
// this callback is a good API-level-independent way to tell when the root view size/layout changes
super.onWindowFocusChanged(hasFocus);
enableOverlays();
}
@Override
public void updateAfterSync() {
this.requestedPage = false;
setSupportProgressBarIndeterminateVisibility(false);
updateSyncStatus(false);
stories.requery();
readingAdapter.notifyDataSetChanged();
this.enableOverlays();
checkStoryCount(pager.getCurrentItem());
if (this.unreadSearchLatch != null) {
this.unreadSearchLatch.countDown();
}
}
@Override
@ -269,6 +354,9 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
readingAdapter.notifyDataSetChanged();
this.enableOverlays();
checkStoryCount(pager.getCurrentItem());
if (this.unreadSearchLatch != null) {
this.unreadSearchLatch.countDown();
}
}
/**
@ -278,11 +366,20 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
@Override
public void setNothingMoreToUpdate() {
this.noMoreApiPages = true;
if (this.unreadSearchLatch !=null) {
this.unreadSearchLatch.countDown();
}
}
private void checkStoryCount(int position) {
/**
* While navigating the story list and at the specified position, see if it is possible
* and desirable to start loading more stories in the background. Note that if a load
* is triggered, this method will be called again by the callback to ensure another
* load is not needed and all latches are tripped.
*/
private void checkStoryCount(int position) {
// if the pager is at or near the number of stories loaded, check for more unless we know we are at the end of the list
if (((position + 1) >= stories.getCount()) && !noMoreApiPages && !requestedPage) {
if (((position + 2) >= stories.getCount()) && !noMoreApiPages && !requestedPage) {
currentApiPage += 1;
requestedPage = true;
triggerRefresh(currentApiPage);
@ -290,10 +387,24 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
}
@Override
public void updateSyncStatus(boolean syncRunning) {
setSupportProgressBarIndeterminateVisibility(syncRunning);
public void updateSyncStatus(final boolean syncRunning) {
enableProgressCircle(overlayProgressRight, syncRunning);
}
private void enableProgressCircle(final ProgressBar view, final boolean enabled) {
runOnUiThread(new Runnable() {
public void run() {
if (enabled) {
view.setProgress(0);
view.setVisibility(View.VISIBLE);
} else {
view.setProgress(100);
view.setVisibility(View.GONE);
}
}
});
}
public abstract void triggerRefresh(int page);
@Override
@ -316,9 +427,10 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
if (this.storiesToMarkAsRead.size() >= AppConstants.MAX_MARK_READ_BATCH) {
flushStoriesMarkedRead();
}
if (this.storiesAlreadySeen.add(story)) {
if (!this.storiesAlreadySeen.contains(story)) {
// only decrement the cached story count if the story wasn't already read
this.unreadCount--;
this.storiesAlreadySeen.add(story);
this.currentUnreadCount--;
}
this.enableOverlays();
}
@ -341,12 +453,13 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
// operation, or it was read long before now.
FeedUtils.markStoryUnread(story, Reading.this, this.apiManager);
this.unreadCount++;
this.currentUnreadCount++;
this.storiesAlreadySeen.remove(story);
this.enableOverlays();
}
// NB: this callback is for the text size slider
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
getSharedPreferences(PrefConstants.PREFERENCES, 0).edit().putFloat(PrefConstants.PREFERENCE_TEXT_SIZE, (float) progress / AppConstants.FONT_SIZE_INCREMENT_FACTOR).commit();
@ -363,12 +476,193 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
public void onStopTrackingTouch(SeekBar seekBar) {
}
/**
* Click handler for the righthand overlay nav button.
*/
public void overlayRight(View v) {
pager.setCurrentItem(pager.getCurrentItem()+1, true);
if (this.currentUnreadCount == 0) {
// if there are no unread stories, go back to the feed list
Intent i = new Intent(this, Main.class);
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(i);
} else {
// if there are unreads, go to the next one
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
nextUnread();
return null;
}
}.execute();
//}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
/**
* Search our set of stories for the next unread one. This requires some heavy
* cooperation with the way stories are automatically loaded in the background
* as we walk through the list.
*/
private void nextUnread() {
synchronized (UNREAD_SEARCH_MUTEX) {
int candidate = 0;
boolean unreadFound = false;
boolean error = false;
unreadSearch:while (!unreadFound) {
Story story = readingAdapter.getStory(candidate);
if (story == null) {
if (this.noMoreApiPages) {
// this is odd. if there were no unreads, how was the button even enabled?
Log.e(this.getClass().getName(), "Ran out of stories while looking for unreads.");
break unreadSearch;
}
} else {
if ((candidate == pager.getCurrentItem()) || (story.read) || (this.storiesAlreadySeen.contains(story))) {
candidate++;
continue unreadSearch;
} else {
unreadFound = true;
break unreadSearch;
}
}
// if we didn't find a story trigger a check to see if there are any more to search before proceeding
this.unreadSearchLatch = new CountDownLatch(1);
this.checkStoryCount(candidate+1);
try {
boolean unlatched = this.unreadSearchLatch.await(UNREAD_SEARCH_LOAD_WAIT_SECONDS, TimeUnit.SECONDS);
if (unlatched) {
continue unreadSearch;
} else {
Log.e(this.getClass().getName(), "Timed out waiting for next API page while looking for unreads.");
break unreadSearch;
}
} catch (InterruptedException ie) {
Log.e(this.getClass().getName(), "Interrupted waiting for next API page while looking for unreads.");
break unreadSearch;
}
}
if (error) {
runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(Reading.this, R.string.toast_unread_search_error, Toast.LENGTH_LONG).show();
}
});
}
if (unreadFound) {
final int page = candidate;
runOnUiThread(new Runnable() {
public void run() {
pager.setCurrentItem(page, true);
}
});
}
}
}
/**
* Click handler for the lefthand overlay nav button.
*/
public void overlayLeft(View v) {
pager.setCurrentItem(pager.getCurrentItem()-1, true);
int targetPosition = this.getLastReadPosition(true);
if (targetPosition != -1) {
pager.setCurrentItem(targetPosition, true);
} else {
Log.e(this.getClass().getName(), "reading history contained item not found in cursor.");
}
}
/**
* Get the pager position of the last story read during this activity or -1 if there is nothing
* in the history.
*
* @param trimHistory optionally trim the history of the currently displayed page iff the
* back button has been pressed.
*/
private int getLastReadPosition(boolean trimHistory) {
synchronized (this.pageHistory) {
// the last item is always the currently shown page, do not count it
if (this.pageHistory.size() < 2) {
return -1;
}
Story targetStory = this.pageHistory.get(this.pageHistory.size()-2);
int targetPosition = this.readingAdapter.getPosition(targetStory);
if (trimHistory && (targetPosition != -1)) {
this.pageHistory.remove(this.pageHistory.size()-1);
}
return targetPosition;
}
}
/**
* Click handler for the progress indicator on the righthand overlay nav button.
*/
public void overlayCount(View v) {
String unreadText = getString((this.currentUnreadCount == 1) ? R.string.overlay_count_toast_1 : R.string.overlay_count_toast_N);
Toast.makeText(this, String.format(unreadText, this.currentUnreadCount), Toast.LENGTH_SHORT).show();
}
public void overlaySend(View v) {
Story story = readingAdapter.getStory(pager.getCurrentItem());
FeedUtils.shareStory(story, this);
}
public void overlayText(View v) {
synchronized (textMode) {
// if we were already in text mode, switch back to story mode
if (textMode) {
enableStoryMode();
} else {
enableTextMode();
}
}
}
private void enableTextMode() {
final Story story = readingAdapter.getStory(pager.getCurrentItem());
if (story != null) {
new AsyncTask<Void, Void, StoryTextResponse>() {
@Override
protected void onPreExecute() {
enableProgressCircle(overlayProgressLeft, true);
}
@Override
protected StoryTextResponse doInBackground(Void... arg) {
return apiManager.getStoryText(story.feedId, story.id);
}
@Override
protected void onPostExecute(StoryTextResponse result) {
ReadingItemFragment item = getReadingFragment();
if (item != null) item.setCustomWebview(result.originalText);
enableProgressCircle(overlayProgressLeft, false);
}
}.execute();
}
this.overlayText.setBackgroundResource(R.drawable.selector_overlay_bg_story);
this.overlayText.setText(R.string.overlay_story);
this.textMode = true;
}
private void enableStoryMode() {
ReadingItemFragment item = getReadingFragment();
if (item != null) item.setDefaultWebview();
this.overlayText.setBackgroundResource(R.drawable.selector_overlay_bg_text);
this.overlayText.setText(R.string.overlay_text);
this.textMode = false;
}
private ReadingItemFragment getReadingFragment() {
Object o = readingAdapter.instantiateItem(pager, pager.getCurrentItem());
if (o instanceof ReadingItemFragment) {
return (ReadingItemFragment) o;
} else {
return null;
}
}
}

View file

@ -9,7 +9,6 @@ import android.view.ViewGroup;
import com.newsblur.domain.Story;
import com.newsblur.fragment.LoadingFragment;
import com.newsblur.fragment.ReadingItemFragment;
public abstract class ReadingAdapter extends FragmentStatePagerAdapter {
@ -41,6 +40,18 @@ public abstract class ReadingAdapter extends FragmentStatePagerAdapter {
return Story.fromCursor(stories);
}
}
public int getPosition(Story story) {
int pos = 0;
while (pos < stories.getCount()) {
stories.moveToPosition(pos);
if (Story.fromCursor(stories).equals(story)) {
return pos;
}
pos++;
}
return -1;
}
@Override
public int getItemPosition(Object object) {

View file

@ -34,7 +34,9 @@ public class SocialFeedReading extends Reading {
stories = contentResolver.query(storiesURI, null, DatabaseConstants.getStorySelectionFromState(currentState), null, null);
setTitle(getIntent().getStringExtra(EXTRA_USERNAME));
this.unreadCount = FeedUtils.getFeedUnreadCount(this.socialFeed, this.currentState);
int unreadCount = FeedUtils.getFeedUnreadCount(this.socialFeed, this.currentState);
this.startingUnreadCount = unreadCount;
this.currentUnreadCount = unreadCount;
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver(), stories);
@ -45,7 +47,7 @@ public class SocialFeedReading extends Reading {
@Override
public void triggerRefresh(int page) {
setSupportProgressBarIndeterminateVisibility(true);
updateSyncStatus(true);
final Intent intent = new Intent(Intent.ACTION_SYNC, null, this, SyncService.class);
intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, syncFragment.receiver);
intent.putExtra(SyncService.EXTRA_TASK_TYPE, SyncService.TaskType.SOCIALFEED_UPDATE);

View file

@ -11,7 +11,7 @@ public class BlurDatabase extends SQLiteOpenHelper {
private final String TEXT = " text";
private final String INTEGER = " integer";
public final static String DB_NAME = "blur.db";
private final static int VERSION = 1;
private final static int VERSION = 2;
public BlurDatabase(Context context) {
super(context, DB_NAME, null, VERSION);
@ -44,7 +44,8 @@ public class BlurDatabase extends SQLiteOpenHelper {
private final String USER_SQL = "CREATE TABLE " + DatabaseConstants.USER_TABLE + " (" +
DatabaseConstants.USER_PHOTO_URL + TEXT + ", " +
DatabaseConstants.USER_USERID + INTEGER + " PRIMARY KEY, " +
DatabaseConstants.USER_USERNAME + TEXT + ")";
DatabaseConstants.USER_USERNAME + TEXT + ", " +
DatabaseConstants.USER_LOCATION + TEXT + ")";
private final String SOCIAL_FEED_SQL = "CREATE TABLE " + DatabaseConstants.SOCIALFEED_TABLE + " (" +
DatabaseConstants.SOCIAL_FEED_ID + INTEGER + " PRIMARY KEY, " +
@ -107,6 +108,7 @@ public class BlurDatabase extends SQLiteOpenHelper {
DatabaseConstants.STORY_TAGS + TEXT + ", " +
DatabaseConstants.STORY_PERMALINK + TEXT + ", " +
DatabaseConstants.STORY_READ + INTEGER + ", " +
DatabaseConstants.STORY_STARRED + INTEGER + ", " +
DatabaseConstants.STORY_TITLE + TEXT;
private final String STORY_SQL = "CREATE TABLE " + DatabaseConstants.STORY_TABLE + " (" + STORY_TABLES_COLS + ")";

View file

@ -66,7 +66,8 @@ public class DatabaseConstants {
public static final String USER_TABLE = "user_table";
public static final String USER_USERID = BaseColumns._ID;
public static final String USER_USERNAME = "username";
public static final String USER_USERNAME = "username";
public static final String USER_LOCATION = "location";
public static final String USER_PHOTO_URL = "photo_url";
public static final String STORY_TABLE = "stories";
@ -84,6 +85,7 @@ public class DatabaseConstants {
public static final String STORY_INTELLIGENCE_TITLE = "intelligence_title";
public static final String STORY_PERMALINK = "permalink";
public static final String STORY_READ = "read";
public static final String STORY_STARRED = "starred";
public static final String STORY_SHARE_COUNT = "share_count";
public static final String STORY_SHARED_USER_IDS = "shared_user_ids";
public static final String STORY_FRIEND_USER_IDS = "comment_user_ids";
@ -169,11 +171,11 @@ public class DatabaseConstants {
public static final String[] STORY_COLUMNS = {
STORY_AUTHORS, STORY_COMMENT_COUNT, STORY_CONTENT, STORY_DATE, STORY_SHARED_DATE, STORY_SHORTDATE, STORY_LONGDATE, STORY_TABLE + "." + STORY_FEED_ID, STORY_TABLE + "." + STORY_ID, STORY_INTELLIGENCE_AUTHORS, STORY_INTELLIGENCE_FEED, STORY_INTELLIGENCE_TAGS, STORY_INTELLIGENCE_TITLE,
STORY_PERMALINK, STORY_READ, STORY_SHARE_COUNT, STORY_TAGS, STORY_TITLE, STORY_SOCIAL_USER_ID, STORY_SOURCE_USER_ID, STORY_SHARED_USER_IDS, STORY_FRIEND_USER_IDS, STORY_PUBLIC_USER_IDS, STORY_SUM_TOTAL, STORY_HASH
STORY_PERMALINK, STORY_READ, STORY_STARRED, STORY_SHARE_COUNT, STORY_TAGS, STORY_TITLE, STORY_SOCIAL_USER_ID, STORY_SOURCE_USER_ID, STORY_SHARED_USER_IDS, STORY_FRIEND_USER_IDS, STORY_PUBLIC_USER_IDS, STORY_SUM_TOTAL, STORY_HASH
};
public static final String[] STARRED_STORY_COLUMNS = {
STORY_AUTHORS, STORY_COMMENT_COUNT, STORY_CONTENT, STORY_DATE, STORY_SHARED_DATE, STORY_SHORTDATE, STORY_LONGDATE, STARRED_STORIES_TABLE + "." + STORY_FEED_ID, STARRED_STORIES_TABLE + "." + STORY_ID, STORY_INTELLIGENCE_AUTHORS, STORY_INTELLIGENCE_FEED, STORY_INTELLIGENCE_TAGS, STORY_INTELLIGENCE_TITLE,
STORY_PERMALINK, STORY_READ, STORY_SHARE_COUNT, STORY_TAGS, STORY_TITLE, STORY_SOCIAL_USER_ID, STORY_SOURCE_USER_ID, STORY_SHARED_USER_IDS, STORY_FRIEND_USER_IDS, STORY_PUBLIC_USER_IDS, STORY_SUM_TOTAL, STORY_HASH
STORY_PERMALINK, STORY_READ, STORY_STARRED, STORY_SHARE_COUNT, STORY_TAGS, STORY_TITLE, STORY_SOCIAL_USER_ID, STORY_SOURCE_USER_ID, STORY_SHARED_USER_IDS, STORY_FRIEND_USER_IDS, STORY_PUBLIC_USER_IDS, STORY_SUM_TOTAL, STORY_HASH
};
/**

View file

@ -336,7 +336,11 @@ public class FeedProvider extends ContentProvider {
Log.d(LoggingDatabase.class.getName(), "rawQuery: " + sql);
Log.d(LoggingDatabase.class.getName(), "selArgs : " + Arrays.toString(selectionArgs));
}
return mdb.rawQuery(sql, selectionArgs);
Cursor cursor = mdb.rawQuery(sql, selectionArgs);
if (AppConstants.VERBOSE_LOG) {
Log.d(LoggingDatabase.class.getName(), "result rows: " + cursor.getCount());
}
return cursor;
}
public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) {
return mdb.query(table, columns, selection, selectionArgs, groupBy, having, orderBy);
@ -599,8 +603,11 @@ public class FeedProvider extends ContentProvider {
case SOCIALFEED_STORIES:
return db.update(DatabaseConstants.SOCIALFEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[] { uri.getLastPathSegment() });
case INDIVIDUAL_STORY:
return db.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_ID + " = ?", new String[] { uri.getLastPathSegment() });
// In order to run a raw SQL query whereby we make decrement the column we need to a dynamic reference - something the usual content provider can't easily handle. Hence this circuitous hack.
int count = 0;
count += db.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_ID + " = ?", new String[] { uri.getLastPathSegment() });
count += db.update(DatabaseConstants.STARRED_STORIES_TABLE, values, DatabaseConstants.STORY_ID + " = ?", new String[] { uri.getLastPathSegment() });
return count;
// In order to run a raw SQL query whereby we make decrement the column we need to a dynamic reference - something the usual content provider can't easily handle. Hence this circuitous hack.
case FEED_COUNT:
db.execSQL("UPDATE " + DatabaseConstants.FEED_TABLE + " SET " + selectionArgs[0] + " = " + selectionArgs[0] + " - 1 WHERE " + DatabaseConstants.FEED_ID + " = " + selectionArgs[1]);
return 0;

View file

@ -14,7 +14,6 @@ import com.newsblur.fragment.ReadingItemFragment;
public class MixedFeedsReadingAdapter extends ReadingAdapter {
private String TAG = "FeedReadingAdapter";
private LoadingFragment loadingFragment;
private final ContentResolver resolver;

View file

@ -41,6 +41,9 @@ public class Story implements Serializable {
@SerializedName("read_status")
public boolean read;
@SerializedName("starred")
public boolean starred;
@SerializedName("story_tags")
public String[] tags;
@ -110,6 +113,7 @@ public class Story implements Serializable {
values.put(DatabaseConstants.STORY_INTELLIGENCE_TITLE, intelligence.intelligenceTitle);
values.put(DatabaseConstants.STORY_TAGS, TextUtils.join(",", tags));
values.put(DatabaseConstants.STORY_READ, read);
values.put(DatabaseConstants.STORY_STARRED, starred);
values.put(DatabaseConstants.STORY_FEED_ID, feedId);
values.put(DatabaseConstants.STORY_HASH, storyHash);
return values;
@ -137,6 +141,7 @@ public class Story implements Serializable {
story.intelligence.intelligenceTags = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_INTELLIGENCE_TAGS));
story.intelligence.intelligenceTitle = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_INTELLIGENCE_TITLE));
story.read = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_READ)) > 0;
story.starred = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_STARRED)) > 0;
story.tags = TextUtils.split(cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_TAGS)), ",");
story.feedId = cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_FEED_ID));
story.id = cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_ID));

View file

@ -15,8 +15,9 @@ public class UserProfile {
@SerializedName("user_id")
public String userId;
public String username;
public String username;
public String location;
public static UserProfile fromCursor(final Cursor c) {
if (c.isBeforeFirst()) {
@ -26,7 +27,8 @@ public class UserProfile {
UserProfile profile = new UserProfile();
profile.userId = c.getString(c.getColumnIndex(DatabaseConstants.USER_USERID));
profile.photoUrl = c.getString(c.getColumnIndex(DatabaseConstants.USER_PHOTO_URL));
profile.username = c.getString(c.getColumnIndex(DatabaseConstants.USER_USERNAME));
profile.username = c.getString(c.getColumnIndex(DatabaseConstants.USER_USERNAME));
profile.location = c.getString(c.getColumnIndex(DatabaseConstants.USER_LOCATION));
return profile;
}
@ -35,7 +37,8 @@ public class UserProfile {
final ContentValues values = new ContentValues();
values.put(DatabaseConstants.USER_PHOTO_URL, photoUrl);
values.put(DatabaseConstants.USER_USERID, userId);
values.put(DatabaseConstants.USER_USERNAME, username);
values.put(DatabaseConstants.USER_USERNAME, username);
values.put(DatabaseConstants.USER_LOCATION, location);
return values;
}

View file

@ -11,12 +11,15 @@ import android.graphics.Color;
import android.graphics.drawable.TransitionDrawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ScaleDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.text.Html;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
@ -67,6 +70,7 @@ public class ReadingItemFragment extends Fragment implements ClassifierDialogFra
public String previouslySavedShareText;
private ImageView feedIcon;
private Reading activity;
private Boolean customContent = false;
public static ReadingItemFragment newInstance(Story story, String feedTitle, String feedFaviconColor, String feedFaviconFade, String feedFaviconBorder, String faviconText, String faviconUrl, Classifier classifier, boolean displayFeedDetails) {
ReadingItemFragment readingFragment = new ReadingItemFragment();
@ -120,6 +124,10 @@ public class ReadingItemFragment extends Fragment implements ClassifierDialogFra
receiver = new TextSizeReceiver();
getActivity().registerReceiver(receiver, new IntentFilter(TEXT_SIZE_CHANGED));
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putSerializable("story", story);
}
@Override
public void onDestroy() {
@ -146,7 +154,12 @@ public class ReadingItemFragment extends Fragment implements ClassifierDialogFra
view = inflater.inflate(R.layout.fragment_readingitem, null);
web = (NewsblurWebview) view.findViewById(R.id.reading_webview);
setupWebview(web);
synchronized (customContent) {
setupWebview(story.content);
customContent = false;
}
setupItemMetadata();
setupShareButton();
setupSaveButton();
@ -164,19 +177,31 @@ public class ReadingItemFragment extends Fragment implements ClassifierDialogFra
}
private void setupSaveButton() {
final Button saveButton = (Button) view.findViewById(R.id.save_story_button);
saveButton.setText(story.starred ? R.string.unsave_this : R.string.save_this);
Button saveButton = (Button) view.findViewById(R.id.save_story_button);
saveButton.setOnClickListener(new OnClickListener() {
saveButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
FeedUtils.saveStory(story, getActivity(), apiManager);
if (story.starred) {
FeedUtils.unsaveStory(story, activity, apiManager, activity);
} else {
FeedUtils.saveStory(story, activity, apiManager, activity);
}
}
});
}
private void setupShareButton() {
public void updateSaveButton() {
Button saveButton = (Button) view.findViewById(R.id.save_story_button);
saveButton.setText(story.starred ? R.string.unsave_this : R.string.save_this);
}
public void updateStory(Story story) {
this.story = story;
}
private void setupShareButton() {
Button shareButton = (Button) view.findViewById(R.id.share_story_button);
for (String userId : story.sharedUserIds) {
@ -195,7 +220,6 @@ public class ReadingItemFragment extends Fragment implements ClassifierDialogFra
});
}
public void changeTextSize(float newTextSize) {
if (web != null) {
web.setTextSize(newTextSize);
@ -247,7 +271,7 @@ public class ReadingItemFragment extends Fragment implements ClassifierDialogFra
itemFeed.setText(feedTitle);
}
itemTitle.setText(story.title);
itemTitle.setText(Html.fromHtml(story.title));
itemDate.setText(story.longDate);
if (!TextUtils.isEmpty(story.authors)) {
@ -300,7 +324,34 @@ public class ReadingItemFragment extends Fragment implements ClassifierDialogFra
}
private void setupWebview(NewsblurWebview web) {
/**
* Set the webview to show the default story content.
*/
public void setDefaultWebview() {
// if the default content hasn't been changed, don't reset it
synchronized (customContent) {
if (!customContent) return;
setupWebview(story.content);
customContent = false;
}
}
/**
* Set the webview to show non-default content, tracking the change.
*/
public void setCustomWebview(String content) {
synchronized (customContent) {
setupWebview(content);
customContent = true;
}
}
private void setupWebview(String storyText) {
if (getActivity() == null) {
// this method gets called by async UI bits that might hold stale fragment references with no assigned
// activity. If this happens, just abort the call.
return;
}
final SharedPreferences preferences = getActivity().getSharedPreferences(PrefConstants.PREFERENCES, 0);
float currentSize = preferences.getFloat(PrefConstants.PREFERENCE_TEXT_SIZE, 0.5f);
@ -310,10 +361,9 @@ public class ReadingItemFragment extends Fragment implements ClassifierDialogFra
builder.append(String.format("body { font-size: %s em; } ", Float.toString(currentSize + AppConstants.FONT_SIZE_LOWER_BOUND)));
builder.append("</style>");
builder.append("<link rel=\"stylesheet\" type=\"text/css\" href=\"reading.css\" /></head><body><div class=\"NB-story\">");
builder.append(story.content);
builder.append(storyText);
builder.append("</div></body></html>");
web.loadDataWithBaseURL("file:///android_asset/", builder.toString(), "text/html", "UTF-8", null);
}
private class TextSizeReceiver extends BroadcastReceiver {
@ -377,11 +427,18 @@ public class ReadingItemFragment extends Fragment implements ClassifierDialogFra
View commentView = inflater.inflate(R.layout.include_comment, null);
commentView.setTag(SetupCommentSectionTask.COMMENT_VIEW_BY + user.id);
TextView commentText = (TextView) commentView.findViewById(R.id.comment_text);
commentText.setTag("commentBy" + user.id);
commentText.setText(sharedText);
TextView commentText = (TextView) commentView.findViewById(R.id.comment_text);
commentText.setTag("commentBy" + user.id);
commentText.setText(sharedText);
if (PrefsUtils.getUserImage(getActivity()) != null) {
TextView commentLocation = (TextView) commentView.findViewById(R.id.comment_location);
if (!TextUtils.isEmpty(user.location)) {
commentLocation.setText(user.location.toUpperCase());
} else {
commentLocation.setVisibility(View.GONE);
}
if (PrefsUtils.getUserImage(getActivity()) != null) {
ImageView commentImage = (ImageView) commentView.findViewById(R.id.comment_user_image);
commentImage.setImageBitmap(UIUtils.roundCorners(PrefsUtils.getUserImage(getActivity()), 10f));
}

View file

@ -8,6 +8,7 @@ import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.text.Editable;
import android.text.Html;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater;
@ -108,7 +109,7 @@ public class ShareDialogFragment extends DialogFragment {
public void onTextChanged(CharSequence s, int start, int before, int count) { }
});
message.setText(String.format(shareString, story.title));
message.setText(String.format(shareString, Html.fromHtml(story.title)));
if (hasBeenShared) {
shareButton.setText(R.string.edit);

View file

@ -32,6 +32,7 @@ public class APIConstants {
public static final String URL_MARK_STORIES_READ = NEWSBLUR_URL + "/reader/mark_story_hashes_as_read/";
public static final String URL_SHARE_STORY = NEWSBLUR_URL + "/social/share_story";
public static final String URL_MARK_STORY_AS_STARRED = NEWSBLUR_URL + "/reader/mark_story_as_starred/";
public static final String URL_MARK_STORY_AS_UNSTARRED = NEWSBLUR_URL + "/reader/mark_story_as_unstarred/";
public static final String URL_MARK_STORY_AS_UNREAD = NEWSBLUR_URL + "/reader/mark_story_as_unread/";
public static final String URL_STARRED_STORIES = NEWSBLUR_URL + "/reader/starred_stories";
public static final String URL_FEED_AUTOCOMPLETE = NEWSBLUR_URL + "/rss_feeds/feed_autocomplete";
@ -41,6 +42,7 @@ public class APIConstants {
public static final String URL_ADD_FEED = NEWSBLUR_URL + "/reader/add_url";
public static final String URL_DELETE_FEED = NEWSBLUR_URL + "/reader/delete_feed";
public static final String URL_CLASSIFIER_SAVE = NEWSBLUR_URL + "/classifier/save";
public static final String URL_STORY_TEXT = NEWSBLUR_URL + "/rss_feeds/original_text";
public static final String PARAMETER_FEEDS = "f";
public static final String PARAMETER_PASSWORD = "password";

View file

@ -42,6 +42,7 @@ import com.newsblur.network.domain.ProfileResponse;
import com.newsblur.network.domain.RegisterResponse;
import com.newsblur.network.domain.SocialFeedResponse;
import com.newsblur.network.domain.StoriesResponse;
import com.newsblur.network.domain.StoryTextResponse;
import com.newsblur.serialization.BooleanTypeAdapter;
import com.newsblur.serialization.DateStringTypeAdapter;
import com.newsblur.util.AppConstants;
@ -135,6 +136,14 @@ public class APIManager {
final APIResponse response = post(APIConstants.URL_MARK_STORY_AS_STARRED, values, false);
return response.getResponse(gson, NewsBlurResponse.class);
}
public NewsBlurResponse markStoryAsUnstarred(final String feedId, final String storyId) {
final ValueMultimap values = new ValueMultimap();
values.put(APIConstants.PARAMETER_FEEDID, feedId);
values.put(APIConstants.PARAMETER_STORYID, storyId);
final APIResponse response = post(APIConstants.URL_MARK_STORY_AS_UNSTARRED, values, false);
return response.getResponse(gson, NewsBlurResponse.class);
}
public NewsBlurResponse markStoryAsUnread( String feedId, String storyId ) {
final ValueMultimap values = new ValueMultimap();
@ -602,6 +611,19 @@ public class APIManager {
}
}
public StoryTextResponse getStoryText(String feedId, String storyId) {
final ContentValues values = new ContentValues();
values.put(APIConstants.PARAMETER_FEEDID, feedId);
values.put(APIConstants.PARAMETER_STORYID, storyId);
final APIResponse response = get(APIConstants.URL_STORY_TEXT, values);
if (!response.isError()) {
StoryTextResponse storyTextResponse = (StoryTextResponse) response.getResponse(gson, StoryTextResponse.class);
return storyTextResponse;
} else {
return null;
}
}
public void refreshFeedCounts() {
final APIResponse response = get(APIConstants.URL_FEED_COUNTS);
if (!response.isError()) {

View file

@ -76,6 +76,12 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
while (commentCursor.moveToNext()) {
final Comment comment = Comment.fromCursor(commentCursor);
// skip public comments if they are disabled
if (!comment.byFriend && !PrefsUtils.showPublicComments(context)) {
continue;
}
View commentView = inflater.inflate(R.layout.include_comment, null);
commentView.setTag(COMMENT_VIEW_BY + comment.userId);
@ -87,7 +93,7 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
ImageView commentImage = (ImageView) commentView.findViewById(R.id.comment_user_image);
TextView commentSharedDate = (TextView) commentView.findViewById(R.id.comment_shareddate);
commentSharedDate.setText(comment.sharedDate.toUpperCase() + " AGO");
commentSharedDate.setText(comment.sharedDate + " ago");
commentSharedDate.setTag(COMMENT_DATE_BY + comment.userId);
final FlowLayout favouriteContainer = (FlowLayout) commentView.findViewById(R.id.comment_favourite_avatars);
@ -167,7 +173,7 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
}
TextView replySharedDate = (TextView) replyView.findViewById(R.id.reply_shareddate);
replySharedDate.setText(reply.shortDate.toUpperCase() + " AGO");
replySharedDate.setText(reply.shortDate + " ago");
((LinearLayout) commentView.findViewById(R.id.comment_replies_container)).addView(replyView);
}
@ -179,7 +185,14 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
commentUsername.setText(commentUser.username);
String userPhoto = commentUser.photoUrl;
if (!TextUtils.isEmpty(comment.sourceUserId)) {
TextView commentLocation = (TextView) commentView.findViewById(R.id.comment_location);
if (!TextUtils.isEmpty(commentUser.location)) {
commentLocation.setText(commentUser.location.toUpperCase());
} else {
commentLocation.setVisibility(View.GONE);
}
if (!TextUtils.isEmpty(comment.sourceUserId)) {
commentImage.setVisibility(View.INVISIBLE);
ImageView usershareImage = (ImageView) commentView.findViewById(R.id.comment_user_reshare_image);
ImageView sourceUserImage = (ImageView) commentView.findViewById(R.id.comment_sharesource_image);
@ -270,7 +283,8 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
commentCount = commentCount.substring(0, commentCount.length() - 1);
}
publicCommentTotal.setText(String.format(commentCount, publicCommentViews.size()));
}
viewHolder.get().findViewById(R.id.reading_public_comment_header).setVisibility(View.VISIBLE);
}
if (friendCommentViews.size() > 0) {
String commentCount = context.getString(R.string.friends_comments_count);
@ -278,7 +292,8 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
commentCount = commentCount.substring(0, commentCount.length() - 1);
}
friendCommentTotal.setText(String.format(commentCount, friendCommentViews.size()));
}
viewHolder.get().findViewById(R.id.reading_friend_comment_header).setVisibility(View.VISIBLE);
}
for (int i = 0; i < publicCommentViews.size(); i++) {
if (i == publicCommentViews.size() - 1) {

View file

@ -0,0 +1,10 @@
package com.newsblur.network.domain;
import com.google.gson.annotations.SerializedName;
public class StoryTextResponse extends NewsBlurResponse {
@SerializedName("original_text")
public String originalText;
}

View file

@ -12,8 +12,10 @@ import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.text.Html;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
@ -36,27 +38,44 @@ public class FeedUtils {
private static Gson gson = new Gson();
public static void saveStory(final Story story, final Context context, final APIManager apiManager) {
if (story != null) {
new AsyncTask<Void, Void, NewsBlurResponse>() {
@Override
protected NewsBlurResponse doInBackground(Void... arg) {
private static void setStorySaved(final Story story, final boolean saved, final Context context, final APIManager apiManager, final ActionCompletionListener receiver) {
new AsyncTask<Void, Void, NewsBlurResponse>() {
@Override
protected NewsBlurResponse doInBackground(Void... arg) {
if (saved) {
return apiManager.markStoryAsStarred(story.feedId, story.storyHash);
} else {
return apiManager.markStoryAsUnstarred(story.feedId, story.storyHash);
}
@Override
protected void onPostExecute(NewsBlurResponse result) {
if (!result.isError()) {
Toast.makeText(context, R.string.toast_story_saved, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, result.getErrorMessage(context.getString(R.string.toast_story_save_error)), Toast.LENGTH_LONG).show();
}
}
@Override
protected void onPostExecute(NewsBlurResponse result) {
if (!result.isError()) {
Toast.makeText(context, (saved ? R.string.toast_story_saved : R.string.toast_story_unsaved), Toast.LENGTH_SHORT).show();
story.starred = saved;
Uri storyUri = FeedProvider.STORY_URI.buildUpon().appendPath(story.id).build();
ContentValues values = new ContentValues();
values.put(DatabaseConstants.STORY_STARRED, saved);
context.getContentResolver().update(storyUri, values, null, null);
} else {
Toast.makeText(context, result.getErrorMessage(context.getString(saved ? R.string.toast_story_save_error : R.string.toast_story_unsave_error)), Toast.LENGTH_LONG).show();
}
}.execute();
} else {
Log.w(FeedUtils.class.getName(), "Couldn't save story, no selection found.");
}
if (receiver != null) {
receiver.actionCompleteCallback();
}
}
}.execute();
}
public static void saveStory(final Story story, final Context context, final APIManager apiManager, ActionCompletionListener receiver) {
setStorySaved(story, true, context, apiManager, receiver);
}
public static void unsaveStory(final Story story, final Context context, final APIManager apiManager, ActionCompletionListener receiver) {
setStorySaved(story, false, context, apiManager, receiver);
}
public static void deleteFeed( final long feedId, final String folderName, final Context context, final APIManager apiManager) {
new AsyncTask<Void, Void, NewsBlurResponse>() {
@ -138,6 +157,11 @@ public class FeedUtils {
}.execute();
}
// update the local object to show as read even before requeried
for (Story story : stories) {
story.read = true;
}
}
private static void appendStoryReadOperations(Story story, List<ContentProviderOperation> operations) {
@ -234,14 +258,39 @@ public class FeedUtils {
}
return count;
}
public static int getCursorUnreadCount(Cursor cursor, int currentState) {
int count = 0;
for (int i = 0; i < cursor.getCount(); i++) {
cursor.moveToPosition(i);
count += cursor.getInt(cursor.getColumnIndexOrThrow(DatabaseConstants.SUM_POS));
if ((currentState == AppConstants.STATE_ALL) || (currentState == AppConstants.STATE_SOME)) {
count += cursor.getInt(cursor.getColumnIndexOrThrow(DatabaseConstants.SUM_NEUT));
}
if (currentState == AppConstants.STATE_ALL ) {
count += cursor.getInt(cursor.getColumnIndexOrThrow(DatabaseConstants.SUM_NEG));
}
}
return count;
}
public static void shareStory(Story story, Context context) {
Intent intent = new Intent(android.content.Intent.ACTION_SEND);
intent.setType("text/plain");
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
intent.putExtra(Intent.EXTRA_SUBJECT, story.title);
intent.putExtra(Intent.EXTRA_SUBJECT, Html.fromHtml(story.title));
final String shareString = context.getResources().getString(R.string.share);
intent.putExtra(Intent.EXTRA_TEXT, String.format(shareString, new Object[] { story.title, story.permalink }));
intent.putExtra(Intent.EXTRA_TEXT, String.format(shareString, new Object[] { Html.fromHtml(story.title),
story.permalink }));
context.startActivity(Intent.createChooser(intent, "Share using"));
}
/**
* An interface usable by callers of this utility class that allows them to receive
* notification that the async methods here have finihed and may have updated the DB
* as a result.
*/
public interface ActionCompletionListener {
public abstract void actionCompleteCallback();
}
}

View file

@ -36,4 +36,6 @@ public class PrefConstants {
public static final String DEFAULT_STORY_ORDER = "default_story_order";
public static final String DEFAULT_READ_FILTER = "default_read_filter";
public static final String SHOW_PUBLIC_COMMENTS = "show_public_comments";
}

View file

@ -234,4 +234,9 @@ public class PrefsUtils {
private static ReadFilter getDefaultReadFilter(SharedPreferences prefs) {
return ReadFilter.valueOf(prefs.getString(PrefConstants.DEFAULT_READ_FILTER, ReadFilter.ALL.toString()));
}
public static boolean showPublicComments(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
return prefs.getBoolean(PrefConstants.SHOW_PUBLIC_COMMENTS, true);
}
}

View file

@ -78,13 +78,16 @@ public class UIUtils {
return (int) (dps * scale + 0.5f);
}
public static float px2dp(Context context, int px) {
return ((float) px) / context.getResources().getDisplayMetrics().density;
}
/**
* Sets the alpha of a view in a manner that is safe to use before API version 11.
* If alpha isn't supported, just make the view invisible if the alpha is so low
* that it may as well be.
*/
public static void setViewAlpha(View v, float alpha) {
v.setVisibility((alpha > 0.001) ? View.VISIBLE : View.INVISIBLE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
v.setAlpha(alpha);
}

View file

@ -0,0 +1,81 @@
package com.newsblur.view;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.ProgressBar;
import com.newsblur.R;
/**
* A determinate, circular progress indicator.
*
* NB: You *must* set the style attribute of the indicator to "@android:style/Widget.ProgressBar.Horizontal",
* even though this is circular. The parent class disables determinate behaviour otherwise.
*/
public class ProgressCircle extends ProgressBar {
/** The thickness of the circular ring, in DP. */
public static final int STROKE_THICKNESS = 5;
public ProgressCircle(Context context) {
super(context);
}
public ProgressCircle(Context context, AttributeSet attrs) {
super(context, attrs);
this.setIndeterminate(false);
}
protected void onDraw(Canvas canvas) {
// the outline of the view w.r.t the screen
Rect r = new Rect();
this.getDrawingRect(r);
// a bitmap on which we will render so that clearing can be done
Bitmap bm = Bitmap.createBitmap(r.width(), r.height(), Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bm);
// the outline of the view w.r.t the bitmap
Rect cr = new Rect();
cr.top = 0;
cr.left = 0;
cr.bottom = r.width();
cr.right = r.height();
float angle = (360f * this.getProgress()) / this.getMax();
Paint p = new Paint();
p.setStyle( Paint.Style.FILL );
p.setAntiAlias(true);
// draw the "remaining" part of the arc as a background
p.setColor( getResources().getColor(R.color.progress_circle_remaining) );
c.drawArc(new RectF(cr), -90f, 360f, true, p);
// draw the "completed" part of the arc over that
p.setColor( getResources().getColor(R.color.progress_circle_complete) );
c.drawArc(new RectF(cr), -90f, angle, true, p);
// clear the centre to form a ring
p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
p.setAlpha(0xFF);
RectF innerR = new RectF(cr);
innerR.top += STROKE_THICKNESS;
innerR.left += STROKE_THICKNESS;
innerR.bottom -= STROKE_THICKNESS;
innerR.right -= STROKE_THICKNESS;
c.drawArc(innerR, -90f, 360f, true, p);
// apply the bitmap onto this view
canvas.drawBitmap(bm, r.left, r.top, null);
}
}

View file

@ -19,7 +19,7 @@ gunicorn==0.17.2
httplib2==0.8
iconv==1.0
kombu==2.5.7
lxml==3.1.0
# lxml==3.1.0
mongoengine==0.8.2
nltk==2.0.4
oauth2==1.5.211

View file

@ -3809,7 +3809,7 @@ background: transparent;
#story_taskbar .NB-tryfeed-add,
#story_taskbar .NB-tryfeed-follow,
#story_taskbar .NB-tryout-signup {
margin: 6px auto 0px;
margin: 2px auto 0px;
width: 80px;
height: 14px;
text-align: center;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 301 KiB

View file

@ -35,6 +35,7 @@ NEWSBLUR.Modal.prototype = {
});
setTimeout(function() {
// $(window).resize();
self.resize();
self.flags.modal_loaded = true;
});
});
@ -60,6 +61,7 @@ NEWSBLUR.Modal.prototype = {
resize: function() {
// $(window).trigger('resize.simplemodal');
$.modal.resize();
},
close: function(callback) {

View file

@ -20,6 +20,9 @@ NEWSBLUR.ReaderAccount.prototype.constructor = NEWSBLUR.ReaderAccount;
_.extend(NEWSBLUR.ReaderAccount.prototype, {
runner: function() {
this.options.onOpen = _.bind(function() {
// $(window).resize();
}, this);
this.make_modal();
this.open_modal();

View file

@ -82,6 +82,9 @@
return $.modal.impl.init(data, options);
};
$.modal.resize = function (callback) {
$.modal.impl.resize(callback);
};
/*
* Close the modal dialog.
*/
@ -391,22 +394,24 @@
});
// update window size
$(window).bind('resize.simplemodal', function () {
// redetermine the window width/height
w = s.getDimensions();
$(window).bind('resize.simplemodal', _.bind(this.resize_modal, this));
},
resize_modal: function () {
// redetermine the window width/height
w = this.getDimensions();
// reposition the dialog
s.o.autoResize ? s.setContainerDimensions() : s.o.autoPosition && s.setPosition();
// reposition the dialog
this.o.autoResize ? this.setContainerDimensions() : this.o.autoPosition && this.setPosition();
if (ie6 || ieQuirks) {
s.fixIE();
}
else if (s.o.modal) {
// update the iframe & overlay
s.d.iframe && s.d.iframe.css({height: w[0], width: w[1]});
s.d.overlay.css({height: w[0], width: w[1]});
}
});
if (ie6 || ieQuirks) {
this.fixIE();
}
else if (this.o.modal) {
// update the iframe & overlay
this.d.iframe && this.d.iframe.css({height: w[0], width: w[1]});
this.d.overlay.css({height: w[0], width: w[1]});
}
},
/*
* Unbind events

View file

@ -17,8 +17,10 @@ from vendor import reseekfile
# COMMENTS_RE = re.compile('\<![ \r\n\t]*(--([^\-]|[\r\n]|-[^\-])*--[ \r\n\t]*)\>')
COMMENTS_RE = re.compile('\<!--.*?--\>')
def midnight_today():
return datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
def midnight_today(now=None):
if not now:
now = datetime.datetime.now()
return now.replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None)
def midnight_yesterday(midnight=None):
if not midnight:
@ -28,9 +30,11 @@ def midnight_yesterday(midnight=None):
def beginning_of_this_month():
return datetime.datetime.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0)
def format_story_link_date__short(date):
def format_story_link_date__short(date, now=None):
if not now:
now = datetime.datetime.now()
date = date.replace(tzinfo=None)
midnight = midnight_today()
midnight = midnight_today(now)
if date > midnight:
return date.strftime('%I:%M%p').lstrip('0').lower()
elif date > midnight_yesterday(midnight):