Merge branch 'master' into shareext
* master: (247 commits) Handling issue when story has no original content. Switching to Mercury text parser, which is an upgraded Readability. Using old readability as backup. Baseline autolayout changes for story page control for iPhone X, but a lot needs to happen to get sizing correct for story traversal buttons and story web views. Fixing feed detail view for iPhone X. Updating twitter avatar photo on homepage. Removing links to Readability. Fixes #1045. Removing breaking text importer UTF-8 encoding. Wrong margins on iPhone X. Attempting to fix notifier on iPhone X. Fixing font size selector styling. More UI in background thread warnings. Fixing long press menu location on feed titles. Show all story tags. Fixing warnings. Adding feed list and story list font size segmented controls to feed and story menus. Also changing feed and story title list to be variable height depending on font size. Fixing badge unread counts for users without any notifications. Fixing layout of unread counts on dashboard top. remove hardcoded background resource IDs in favour of using theme engine migrate some hardcoded theme logic to use theme engine fix broken colors when training and un-training story metadata ...
|
@ -188,12 +188,17 @@ class MUserFeedNotification(mongo.Document):
|
|||
feed_title = usersub.user_title or usersub.feed.feed_title
|
||||
# title = "%s: %s" % (feed_title, story['story_title'])
|
||||
title = feed_title
|
||||
subtitle = story['story_title']
|
||||
subtitle = HTMLParser().unescape(story['story_title'])
|
||||
# body = HTMLParser().unescape(strip_tags(story['story_content']))
|
||||
soup = BeautifulSoup(story['story_content'].strip())
|
||||
# print story['story_content']
|
||||
body = replace_with_newlines(soup)
|
||||
body = truncate_chars(body.strip(), 1200)
|
||||
if not body:
|
||||
body = " "
|
||||
|
||||
if not usersub.user.profile.is_premium:
|
||||
body = "Please upgrade to a premium subscription to receive full push notifications."
|
||||
|
||||
return title, subtitle, body
|
||||
|
||||
|
@ -225,9 +230,10 @@ class MUserFeedNotification(mongo.Document):
|
|||
def send_ios(self, story, user, usersub):
|
||||
if not self.is_ios: return
|
||||
|
||||
apns = APNs(use_sandbox=True,
|
||||
cert_file='/srv/newsblur/config/certificates/aps_development.pem',
|
||||
key_file='/srv/newsblur/config/certificates/aps_development.pem')
|
||||
apns = APNs(use_sandbox=False,
|
||||
cert_file='/srv/newsblur/config/certificates/aps.pem',
|
||||
key_file='/srv/newsblur/config/certificates/aps.pem',
|
||||
enhanced=True)
|
||||
|
||||
tokens = MUserNotificationTokens.get_tokens_for_user(self.user_id)
|
||||
title, subtitle, body = self.title_and_body(story, usersub)
|
||||
|
@ -236,6 +242,11 @@ class MUserFeedNotification(mongo.Document):
|
|||
image_url = story['image_urls'][0]
|
||||
# print image_url
|
||||
|
||||
def response_listener(error_response):
|
||||
logging.user(user, "~FRAPNS client get error-response: " + str(error_response))
|
||||
|
||||
apns.gateway_server.register_response_listener(response_listener)
|
||||
|
||||
for token in tokens.ios_tokens:
|
||||
logging.user(user, '~BMStory notification by iOS: ~FY~SB%s~SN~BM~FY/~SB%s' %
|
||||
(story['story_title'][:50], usersub.feed.feed_title[:50]))
|
||||
|
|
|
@ -5,7 +5,7 @@ from vendor.zebra.forms import StripePaymentForm
|
|||
from django.utils.safestring import mark_safe
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.models import User
|
||||
from apps.profile.models import change_password, blank_authenticate, MGiftCode
|
||||
from apps.profile.models import change_password, blank_authenticate, MGiftCode, MCustomStyling
|
||||
from apps.social.models import MSocialProfile
|
||||
|
||||
PLANS = [
|
||||
|
@ -113,7 +113,12 @@ class AccountSettingsForm(forms.Form):
|
|||
old_password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'NB-input'}),
|
||||
label='password',
|
||||
required=False)
|
||||
# error_messages={'required': 'Please enter a password.'})
|
||||
custom_js = forms.CharField(widget=forms.TextInput(attrs={'class': 'NB-input'}),
|
||||
label='custom_js',
|
||||
required=False)
|
||||
custom_css = forms.CharField(widget=forms.TextInput(attrs={'class': 'NB-input'}),
|
||||
label='custom_css',
|
||||
required=False)
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
self.user = user
|
||||
|
@ -161,6 +166,8 @@ class AccountSettingsForm(forms.Form):
|
|||
new_password = self.cleaned_data.get('new_password', None)
|
||||
old_password = self.cleaned_data.get('old_password', None)
|
||||
email = self.cleaned_data.get('email', None)
|
||||
custom_css = self.cleaned_data.get('custom_css', None)
|
||||
custom_js = self.cleaned_data.get('custom_js', None)
|
||||
|
||||
if username and self.user.username != username:
|
||||
change_password(self.user, self.user.username, username)
|
||||
|
@ -178,6 +185,8 @@ class AccountSettingsForm(forms.Form):
|
|||
if old_password or new_password:
|
||||
change_password(self.user, old_password, new_password)
|
||||
|
||||
MCustomStyling.save_user(self.user.pk, custom_css, custom_js)
|
||||
|
||||
class RedeemCodeForm(forms.Form):
|
||||
gift_code = forms.CharField(widget=forms.TextInput(),
|
||||
label="Gift code",
|
||||
|
|
|
@ -254,6 +254,9 @@ BANNED_USER_AGENTS = (
|
|||
'missing',
|
||||
)
|
||||
|
||||
BANNED_USERNAMES = (
|
||||
)
|
||||
|
||||
class UserAgentBanMiddleware:
|
||||
def process_request(self, request):
|
||||
user_agent = request.environ.get('HTTP_USER_AGENT', 'missing').lower()
|
||||
|
@ -274,3 +277,12 @@ class UserAgentBanMiddleware:
|
|||
|
||||
return HttpResponse(json.encode(data), status=403, mimetype='text/json')
|
||||
|
||||
if request.user.is_authenticated() and any(username == request.user.username for username in BANNED_USERNAMES):
|
||||
data = {
|
||||
'error': 'User banned: %s' % request.user.username,
|
||||
'code': -1
|
||||
}
|
||||
logging.user(request, "~FB~SN~BBBanned Username: ~SB%s / %s (%s)" % (request.user, request.path, request.META))
|
||||
|
||||
return HttpResponse(json.encode(data), status=403, mimetype='text/json')
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import re
|
|||
import redis
|
||||
import uuid
|
||||
import mongoengine as mongo
|
||||
from pprint import pprint
|
||||
from django.db import models
|
||||
from django.db import IntegrityError
|
||||
from django.db.utils import DatabaseError
|
||||
|
@ -16,7 +15,6 @@ from django.db.models import Sum, Avg, Count
|
|||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.mail import mail_admins
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.template.loader import render_to_string
|
||||
|
@ -515,7 +513,6 @@ class Profile(models.Model):
|
|||
|
||||
@classmethod
|
||||
def count_all_feed_subscribers_for_user(self, user):
|
||||
SUBSCRIBER_EXPIRE = datetime.datetime.now() - datetime.timedelta(days=settings.SUBSCRIBER_EXPIRE)
|
||||
r = redis.Redis(connection_pool=settings.REDIS_FEED_SUB_POOL)
|
||||
if not isinstance(user, User):
|
||||
user = User.objects.get(pk=user)
|
||||
|
@ -604,12 +601,12 @@ class Profile(models.Model):
|
|||
|
||||
params = dict(receiver_user_id=self.user.pk, email_type='first_share')
|
||||
try:
|
||||
sent_email = MSentEmail.objects.get(**params)
|
||||
MSentEmail.objects.get(**params)
|
||||
if not force:
|
||||
# Return if email already sent
|
||||
return
|
||||
except MSentEmail.DoesNotExist:
|
||||
sent_email = MSentEmail.objects.create(**params)
|
||||
MSentEmail.objects.create(**params)
|
||||
|
||||
social_profile = MSocialProfile.objects.get(user_id=self.user.pk)
|
||||
params = {
|
||||
|
@ -630,14 +627,14 @@ class Profile(models.Model):
|
|||
logging.user(self.user, "~BB~FM~SBSending first share to blurblog email to: %s" % self.user.email)
|
||||
|
||||
def send_new_premium_email(self, force=False):
|
||||
subs = UserSubscription.objects.filter(user=self.user)
|
||||
message = """Woohoo!
|
||||
|
||||
User: %(user)s
|
||||
Feeds: %(feeds)s
|
||||
|
||||
Sincerely,
|
||||
NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()}
|
||||
# subs = UserSubscription.objects.filter(user=self.user)
|
||||
# message = """Woohoo!
|
||||
#
|
||||
# User: %(user)s
|
||||
# Feeds: %(feeds)s
|
||||
#
|
||||
# Sincerely,
|
||||
# NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()}
|
||||
# mail_admins('New premium account', message, fail_silently=True)
|
||||
|
||||
if not self.user.email or not self.send_emails:
|
||||
|
@ -645,12 +642,12 @@ NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()}
|
|||
|
||||
params = dict(receiver_user_id=self.user.pk, email_type='new_premium')
|
||||
try:
|
||||
sent_email = MSentEmail.objects.get(**params)
|
||||
MSentEmail.objects.get(**params)
|
||||
if not force:
|
||||
# Return if email already sent
|
||||
return
|
||||
except MSentEmail.DoesNotExist:
|
||||
sent_email = MSentEmail.objects.create(**params)
|
||||
MSentEmail.objects.create(**params)
|
||||
|
||||
user = self.user
|
||||
text = render_to_string('mail/email_new_premium.txt', locals())
|
||||
|
@ -692,12 +689,12 @@ NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()}
|
|||
|
||||
params = dict(receiver_user_id=self.user.pk, email_type='new_user_queue')
|
||||
try:
|
||||
sent_email = MSentEmail.objects.get(**params)
|
||||
MSentEmail.objects.get(**params)
|
||||
if not force:
|
||||
# Return if email already sent
|
||||
return
|
||||
except MSentEmail.DoesNotExist:
|
||||
sent_email = MSentEmail.objects.create(**params)
|
||||
MSentEmail.objects.create(**params)
|
||||
|
||||
user = self.user
|
||||
text = render_to_string('mail/email_new_user_queue.txt', locals())
|
||||
|
@ -769,13 +766,13 @@ NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()}
|
|||
|
||||
params = dict(receiver_user_id=self.user.pk, email_type='launch_social')
|
||||
try:
|
||||
sent_email = MSentEmail.objects.get(**params)
|
||||
MSentEmail.objects.get(**params)
|
||||
if not force:
|
||||
# Return if email already sent
|
||||
logging.user(self.user, "~FM~SB~FRNot~FM sending launch social email for user, sent already: %s" % self.user.email)
|
||||
return
|
||||
except MSentEmail.DoesNotExist:
|
||||
sent_email = MSentEmail.objects.create(**params)
|
||||
MSentEmail.objects.create(**params)
|
||||
|
||||
delta = datetime.datetime.now() - self.last_seen_on
|
||||
months_ago = delta.days / 30
|
||||
|
@ -799,13 +796,13 @@ NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()}
|
|||
|
||||
params = dict(receiver_user_id=self.user.pk, email_type='launch_turntouch')
|
||||
try:
|
||||
sent_email = MSentEmail.objects.get(**params)
|
||||
MSentEmail.objects.get(**params)
|
||||
if not force:
|
||||
# Return if email already sent
|
||||
logging.user(self.user, "~FM~SB~FRNot~FM sending launch social email for user, sent already: %s" % self.user.email)
|
||||
return
|
||||
except MSentEmail.DoesNotExist:
|
||||
sent_email = MSentEmail.objects.create(**params)
|
||||
MSentEmail.objects.create(**params)
|
||||
|
||||
delta = datetime.datetime.now() - self.last_seen_on
|
||||
months_ago = delta.days / 30
|
||||
|
@ -829,13 +826,13 @@ NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()}
|
|||
|
||||
params = dict(receiver_user_id=self.user.pk, email_type='launch_turntouch_end')
|
||||
try:
|
||||
sent_email = MSentEmail.objects.get(**params)
|
||||
MSentEmail.objects.get(**params)
|
||||
if not force:
|
||||
# Return if email already sent
|
||||
logging.user(self.user, "~FM~SB~FRNot~FM sending launch TT end email for user, sent already: %s" % self.user.email)
|
||||
return
|
||||
except MSentEmail.DoesNotExist:
|
||||
sent_email = MSentEmail.objects.create(**params)
|
||||
MSentEmail.objects.create(**params)
|
||||
|
||||
delta = datetime.datetime.now() - self.last_seen_on
|
||||
months_ago = delta.days / 30
|
||||
|
@ -869,7 +866,7 @@ NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()}
|
|||
if self.grace_period_email_sent(force=force):
|
||||
return
|
||||
|
||||
if self.premium_expire < datetime.datetime.now():
|
||||
if self.premium_expire and self.premium_expire < datetime.datetime.now():
|
||||
self.premium_expire = datetime.datetime.now()
|
||||
self.save()
|
||||
|
||||
|
@ -1278,6 +1275,52 @@ class MRedeemedCode(mongo.Document):
|
|||
logging.user(user, "~FG~BBRedeeming gift code: %s~FW" % gift_code)
|
||||
|
||||
|
||||
class MCustomStyling(mongo.Document):
|
||||
user_id = mongo.IntField(unique=True)
|
||||
custom_css = mongo.StringField()
|
||||
custom_js = mongo.StringField()
|
||||
updated_date = mongo.DateTimeField(default=datetime.datetime.now)
|
||||
|
||||
meta = {
|
||||
'collection': 'custom_styling',
|
||||
'allow_inheritance': False,
|
||||
'indexes': ['user_id'],
|
||||
}
|
||||
|
||||
def __unicode__(self):
|
||||
return "%s custom style %s/%s %s" % (self.user_id, len(self.custom_css) if self.custom_css else "-",
|
||||
len(self.custom_js) if self.custom_js else "-", self.updated_date)
|
||||
|
||||
def canonical(self):
|
||||
return {
|
||||
'css': self.custom_css,
|
||||
'js': self.custom_js,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_user(cls, user_id):
|
||||
try:
|
||||
styling = cls.objects.get(user_id=user_id)
|
||||
except cls.DoesNotExist:
|
||||
return None
|
||||
|
||||
return styling
|
||||
|
||||
@classmethod
|
||||
def save_user(cls, user_id, css, js):
|
||||
styling = cls.get_user(user_id)
|
||||
if not css and not js:
|
||||
if styling:
|
||||
styling.delete()
|
||||
return
|
||||
|
||||
if not styling:
|
||||
styling = cls.objects.create(user_id=user_id)
|
||||
|
||||
styling.custom_css = css
|
||||
styling.custom_js = js
|
||||
styling.save()
|
||||
|
||||
class RNewUserQueue:
|
||||
|
||||
KEY = "new_user_queue"
|
||||
|
|
|
@ -417,6 +417,7 @@ class UserSubscription(models.Model):
|
|||
@classmethod
|
||||
def feeds_with_updated_counts(cls, user, feed_ids=None, check_fetch_status=False, force=False):
|
||||
feeds = {}
|
||||
silent = not getattr(settings, "TEST_DEBUG", False)
|
||||
|
||||
# Get subscriptions for user
|
||||
user_subs = cls.objects.select_related('feed').filter(user=user, active=True)
|
||||
|
@ -430,7 +431,7 @@ class UserSubscription(models.Model):
|
|||
sub.needs_unread_recalc or
|
||||
sub.unread_count_updated < user.profile.unread_cutoff or
|
||||
sub.oldest_unread_story_date < user.profile.unread_cutoff):
|
||||
sub = sub.calculate_feed_scores(silent=True, force=force)
|
||||
sub = sub.calculate_feed_scores(silent=silent, force=force)
|
||||
if not sub: continue # TODO: Figure out the correct sub and give it a new feed_id
|
||||
|
||||
feed_id = sub.feed_id
|
||||
|
@ -674,20 +675,24 @@ class UserSubscription(models.Model):
|
|||
|
||||
if not self.needs_unread_recalc:
|
||||
self.needs_unread_recalc = True
|
||||
self.save()
|
||||
self.save(update_fields=['needs_unread_recalc'])
|
||||
|
||||
if len(story_hashes) > 1:
|
||||
logging.user(request, "~FYRead %s stories in feed: %s" % (len(story_hashes), self.feed))
|
||||
else:
|
||||
logging.user(request, "~FYRead story in feed: %s" % (self.feed))
|
||||
logging.user(request, "~FYRead story (%s) in feed: %s" % (story_hashes, self.feed))
|
||||
RUserStory.aggregate_mark_read(self.feed_id)
|
||||
|
||||
for story_hash in set(story_hashes):
|
||||
for story_hash in set(story_hashes):
|
||||
# logging.user(request, "~FYRead story: %s" % (story_hash))
|
||||
RUserStory.mark_read(self.user_id, self.feed_id, story_hash, aggregated=aggregated)
|
||||
r.publish(self.user.username, 'story:read:%s' % story_hash)
|
||||
|
||||
r.publish(self.user.username, 'feed:%s' % self.feed_id)
|
||||
|
||||
|
||||
self.last_read_date = datetime.datetime.now()
|
||||
self.save(update_fields=['last_read_date'])
|
||||
|
||||
return data
|
||||
|
||||
def invert_read_stories_after_unread_story(self, story, request=None):
|
||||
|
@ -812,6 +817,7 @@ class UserSubscription(models.Model):
|
|||
else:
|
||||
feed_scores['neutral'] += 1
|
||||
else:
|
||||
# print " ---> Cutoff date: %s" % date_delta
|
||||
unread_story_hashes = self.story_hashes(user_id=self.user_id, feed_ids=[self.feed_id],
|
||||
usersubs=[self],
|
||||
read_filter='unread', group_by_feed=False,
|
||||
|
@ -1113,7 +1119,7 @@ class RUserStory:
|
|||
# r2 = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL2)
|
||||
|
||||
story_hash = MStory.ensure_story_hash(story_hash, story_feed_id=story_feed_id)
|
||||
|
||||
|
||||
if not story_hash: return
|
||||
|
||||
def redis_commands(key):
|
||||
|
@ -1242,17 +1248,16 @@ class RUserStory:
|
|||
logging.info(" ---> %s read stories" % len(story_hashes))
|
||||
|
||||
@classmethod
|
||||
def switch_hash(cls, feed_id, old_hash, new_hash):
|
||||
def switch_hash(cls, feed, old_hash, new_hash):
|
||||
r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL)
|
||||
# r2 = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL2)
|
||||
p = r.pipeline()
|
||||
# p2 = r2.pipeline()
|
||||
UNREAD_CUTOFF = datetime.datetime.now() - datetime.timedelta(days=settings.DAYS_OF_STORY_HASHES)
|
||||
|
||||
usersubs = UserSubscription.objects.filter(feed_id=feed_id, last_read_date__gte=UNREAD_CUTOFF)
|
||||
usersubs = UserSubscription.objects.filter(feed_id=feed.pk, last_read_date__gte=feed.unread_cutoff)
|
||||
logging.info(" ---> ~SB%s usersubs~SN to switch read story hashes..." % len(usersubs))
|
||||
for sub in usersubs:
|
||||
rs_key = "RS:%s:%s" % (sub.user.pk, feed_id)
|
||||
rs_key = "RS:%s:%s" % (sub.user.pk, feed.pk)
|
||||
read = r.sismember(rs_key, old_hash)
|
||||
if read:
|
||||
p.sadd(rs_key, new_hash)
|
||||
|
|
|
@ -32,7 +32,7 @@ from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifie
|
|||
from apps.analyzer.models import apply_classifier_titles, apply_classifier_feeds
|
||||
from apps.analyzer.models import apply_classifier_authors, apply_classifier_tags
|
||||
from apps.analyzer.models import get_classifiers_for_user, sort_classifiers_by_feed
|
||||
from apps.profile.models import Profile
|
||||
from apps.profile.models import Profile, MCustomStyling
|
||||
from apps.reader.models import UserSubscription, UserSubscriptionFolders, RUserStory, Feature
|
||||
from apps.reader.forms import SignupForm, LoginForm, FeatureForm
|
||||
from apps.rss_feeds.models import MFeedIcon, MStarredStoryCounts, MSavedSearch
|
||||
|
@ -102,6 +102,7 @@ def dashboard(request, **kwargs):
|
|||
).select_related('feed')[:2]
|
||||
statistics = MStatistics.all()
|
||||
social_profile = MSocialProfile.get_user(user.pk)
|
||||
custom_styling = MCustomStyling.get_user(user.pk)
|
||||
|
||||
start_import_from_google_reader = request.session.get('import_from_google_reader', False)
|
||||
if start_import_from_google_reader:
|
||||
|
@ -117,6 +118,7 @@ def dashboard(request, **kwargs):
|
|||
return {
|
||||
'user_profile' : user.profile,
|
||||
'feed_count' : feed_count,
|
||||
'custom_styling' : custom_styling,
|
||||
'account_images' : range(1, 4),
|
||||
'recommended_feeds' : recommended_feeds,
|
||||
'unmoderated_feeds' : unmoderated_feeds,
|
||||
|
@ -717,7 +719,7 @@ def load_single_feed(request, feed_id):
|
|||
|
||||
if include_feeds:
|
||||
feeds = Feed.objects.filter(pk__in=set([story['story_feed_id'] for story in stories]))
|
||||
feeds = [feed.canonical(include_favicon=False) for feed in feeds]
|
||||
feeds = [f.canonical(include_favicon=False) for f in feeds]
|
||||
|
||||
if usersub:
|
||||
usersub.feed_opens += 1
|
||||
|
@ -1288,6 +1290,7 @@ def load_river_stories__redis(request):
|
|||
"read_filter": read_filter,
|
||||
"usersubs": usersubs,
|
||||
"cutoff_date": user.profile.unread_cutoff,
|
||||
"cache_prefix": "dashboard:" if initial_dashboard else "",
|
||||
}
|
||||
story_hashes, unread_feed_story_hashes = UserSubscription.feed_stories(**params)
|
||||
else:
|
||||
|
@ -1406,9 +1409,11 @@ def load_river_stories__redis(request):
|
|||
# Clean stories to remove potentially old stories on dashboard
|
||||
if initial_dashboard:
|
||||
new_stories = []
|
||||
month_ago = datetime.datetime.utcnow() - datetime.timedelta(days=settings.DAYS_OF_UNREAD)
|
||||
now = datetime.datetime.utcnow()
|
||||
hour = now + datetime.timedelta(hours=1)
|
||||
month_ago = now - datetime.timedelta(days=settings.DAYS_OF_UNREAD)
|
||||
for story in stories:
|
||||
if story['story_date'] >= month_ago:
|
||||
if story['story_date'] >= month_ago and story['story_date'] < hour:
|
||||
new_stories.append(story)
|
||||
stories = new_stories
|
||||
|
||||
|
@ -1442,7 +1447,7 @@ def complete_river(request):
|
|||
read_filter=read_filter)
|
||||
feed_ids = [sub.feed_id for sub in usersubs]
|
||||
if feed_ids:
|
||||
stories_truncated = UserSubscription.truncate_river(user.pk, feed_ids, read_filter)
|
||||
stories_truncated = UserSubscription.truncate_river(user.pk, feed_ids, read_filter, cache_prefix="dashboard:")
|
||||
|
||||
logging.user(request, "~FC~BBRiver complete on page ~SB%s~SN, truncating ~SB%s~SN stories from ~SB%s~SN feeds" % (page, stories_truncated, len(feed_ids)))
|
||||
|
||||
|
@ -1594,9 +1599,12 @@ def mark_story_hashes_as_read(request):
|
|||
usersubs = UserSubscription.objects.filter(user=request.user.pk, feed=feed_id)
|
||||
if usersubs:
|
||||
usersub = usersubs[0]
|
||||
usersub.last_read_date = datetime.datetime.now()
|
||||
if not usersub.needs_unread_recalc:
|
||||
usersub.needs_unread_recalc = True
|
||||
usersub.save(update_fields=['needs_unread_recalc'])
|
||||
usersub.save(update_fields=['needs_unread_recalc', 'last_read_date'])
|
||||
else:
|
||||
usersub.save(update_fields=['last_read_date'])
|
||||
r.publish(request.user.username, 'feed:%s' % feed_id)
|
||||
|
||||
hash_count = len(story_hashes)
|
||||
|
@ -2315,7 +2323,12 @@ def _mark_story_as_starred(request):
|
|||
params.update(story_values)
|
||||
if params.has_key('story_latest_content_z'):
|
||||
params.pop('story_latest_content_z')
|
||||
starred_story = MStarredStory.objects.create(**params)
|
||||
try:
|
||||
starred_story = MStarredStory.objects.create(**params)
|
||||
except OperationError, e:
|
||||
logging.user(request, "~FCStarring ~FRfailed~FC: ~SB%s (~FM~SB%s~FC~SN)" % (story.story_title[:32], e))
|
||||
return {'code': -1, 'message': "Could not save story due to: %s" % e}
|
||||
|
||||
created = True
|
||||
MActivity.new_starred_story(user_id=request.user.pk,
|
||||
story_title=story.story_title,
|
||||
|
@ -2421,6 +2434,7 @@ def starred_counts(request):
|
|||
def send_story_email(request):
|
||||
code = 1
|
||||
message = 'OK'
|
||||
user = get_user(request)
|
||||
story_id = request.POST['story_id']
|
||||
feed_id = request.POST['feed_id']
|
||||
to_addresses = request.POST.get('to', '').replace(',', ' ').replace(' ', ' ').strip().split(' ')
|
||||
|
@ -2431,8 +2445,17 @@ def send_story_email(request):
|
|||
comments = comments[:2048] # Separated due to PyLint
|
||||
from_address = 'share@newsblur.com'
|
||||
share_user_profile = MSocialProfile.get_user(request.user.pk)
|
||||
|
||||
if not to_addresses:
|
||||
|
||||
quota = 20 if user.profile.is_premium else 1
|
||||
if share_user_profile.over_story_email_quota(quota=quota):
|
||||
code = -1
|
||||
if user.profile.is_premium:
|
||||
message = 'You can only send %s stories per day by email.' % quota
|
||||
else:
|
||||
message = 'Upgrade to a premium subscription to send more than one story per day by email.'
|
||||
logging.user(request, '~BRNOT ~BMSharing story by email to %s recipient, over quota: %s/%s' %
|
||||
(len(to_addresses), story_id, feed_id))
|
||||
elif not to_addresses:
|
||||
code = -1
|
||||
message = 'Please provide at least one email address.'
|
||||
elif not all(email_re.match(to_address) for to_address in to_addresses if to_addresses):
|
||||
|
@ -2477,6 +2500,9 @@ def send_story_email(request):
|
|||
except boto.ses.connection.ResponseError, e:
|
||||
code = -1
|
||||
message = "Email error: %s" % str(e)
|
||||
|
||||
share_user_profile.save_sent_email()
|
||||
|
||||
logging.user(request, '~BMSharing story by email to %s recipient%s: ~FY~SB%s~SN~BM~FY/~SB%s' %
|
||||
(len(to_addresses), '' if len(to_addresses) == 1 else 's',
|
||||
story['story_title'][:50], feed and feed.feed_title[:50]))
|
||||
|
|
24
apps/rss_feeds/fixtures/google1.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
[
|
||||
{
|
||||
"pk": 766,
|
||||
"model": "rss_feeds.feed",
|
||||
"fields": {
|
||||
"feed_address": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/google1.xml",
|
||||
"days_to_trim": 90,
|
||||
"feed_link": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/google.html",
|
||||
"feed_link_locked": true,
|
||||
"fetched_once": true,
|
||||
"num_subscribers": 1,
|
||||
"active_subscribers": 1,
|
||||
"creation": "2009-01-12",
|
||||
"feed_title": "The Official Google Blog",
|
||||
"last_update": "2009-07-06 22:30:03",
|
||||
"next_scheduled_update": "2009-07-06 22:30:03",
|
||||
"last_story_date": "2009-07-06 22:30:03",
|
||||
"min_to_decay": 1,
|
||||
"etag": "",
|
||||
"last_modified": "2009-07-06 22:30:03",
|
||||
"active": 1
|
||||
}
|
||||
}
|
||||
]
|
3102
apps/rss_feeds/fixtures/google1.xml
Normal file
24
apps/rss_feeds/fixtures/google2.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
[
|
||||
{
|
||||
"pk": 766,
|
||||
"model": "rss_feeds.feed",
|
||||
"fields": {
|
||||
"feed_address": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/google2.xml",
|
||||
"days_to_trim": 90,
|
||||
"feed_link": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/google.html",
|
||||
"feed_link_locked": true,
|
||||
"fetched_once": true,
|
||||
"num_subscribers": 1,
|
||||
"active_subscribers": 1,
|
||||
"creation": "2009-01-12",
|
||||
"feed_title": "The Official Google Blog",
|
||||
"last_update": "2009-07-06 22:30:03",
|
||||
"next_scheduled_update": "2009-07-06 22:30:03",
|
||||
"last_story_date": "2009-07-06 22:30:03",
|
||||
"min_to_decay": 1,
|
||||
"etag": "",
|
||||
"last_modified": "2009-07-06 22:30:03",
|
||||
"active": 1
|
||||
}
|
||||
}
|
||||
]
|
3102
apps/rss_feeds/fixtures/google2.xml
Normal file
|
@ -1,23 +1,9 @@
|
|||
[
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "reader.usersubscription",
|
||||
"fields": {
|
||||
"feed": 1,
|
||||
"unread_count_updated": "2009-08-01 00:23:42",
|
||||
"mark_read_date": "2009-07-28 23:17:27",
|
||||
"unread_count_neutral": 0,
|
||||
"unread_count_positive": 0,
|
||||
"unread_count_negative": 0,
|
||||
"user": 1,
|
||||
"last_read_date": "2009-07-28 23:17:27"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "reader.usersubscriptionfolders",
|
||||
"fields": {
|
||||
"folders": "[{\"Tech\": [4, 5]}, 1, 2, 3, 6]",
|
||||
"folders": "[{\"Tech\": [4, 5]}, 2, 3, 6, 766]",
|
||||
"user": 1
|
||||
}
|
||||
},
|
||||
|
@ -108,6 +94,21 @@
|
|||
"last_read_date": "2009-07-28 23:17:27"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 8,
|
||||
"model": "reader.usersubscription",
|
||||
"fields": {
|
||||
"feed": 766,
|
||||
"active": true,
|
||||
"unread_count_updated": "2009-08-01 00:23:42",
|
||||
"mark_read_date": "2009-07-01 22:30:03",
|
||||
"unread_count_neutral": 0,
|
||||
"unread_count_positive": 0,
|
||||
"unread_count_negative": 0,
|
||||
"user": 1,
|
||||
"last_read_date": "2009-07-28 23:17:27"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"pk": 2,
|
||||
|
@ -401,6 +402,38 @@
|
|||
"has_feed_exception": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 766,
|
||||
"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/google1.xml",
|
||||
"feed_link": "%(NEWSBLUR_DIR)s/apps/rss_feeds/fixtures/google.html",
|
||||
"hash_address_and_link": "766",
|
||||
"feed_link_locked": true,
|
||||
"last_update": "2011-08-27 02:45:21",
|
||||
"etag": null,
|
||||
"average_stories_per_month": 0,
|
||||
"feed_title": "Google Blog",
|
||||
"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,
|
||||
|
|
|
@ -269,6 +269,7 @@ class IconImporter(object):
|
|||
self.feed.permalink
|
||||
),
|
||||
'Connection': 'close',
|
||||
'Accept': 'image/png,image/x-icon,image/*;q=0.9,*/*;q=0.8'
|
||||
}
|
||||
try:
|
||||
request = urllib2.Request(url, headers=headers)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import difflib
|
||||
import requests
|
||||
import datetime
|
||||
import time
|
||||
import random
|
||||
|
@ -149,6 +150,15 @@ class Feed(models.Model):
|
|||
|
||||
return datetime.datetime.utcnow() - datetime.timedelta(days=settings.DAYS_OF_UNREAD_FREE)
|
||||
|
||||
@property
|
||||
def story_hashes_in_unread_cutoff(self):
|
||||
r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL)
|
||||
current_time = int(time.time() + 60*60*24)
|
||||
unread_cutoff = self.unread_cutoff.strftime('%s')
|
||||
print " ---> zrevrangebyscore zF:%s %s %s" % (self.pk, current_time, unread_cutoff)
|
||||
story_hashes = r.zrevrangebyscore('zF:%s' % self.pk, current_time, unread_cutoff)
|
||||
return story_hashes
|
||||
|
||||
@classmethod
|
||||
def generate_hash_address_and_link(cls, feed_address, feed_link):
|
||||
if not feed_address: feed_address = ""
|
||||
|
@ -453,6 +463,12 @@ class Feed(models.Model):
|
|||
feed = cls.objects.create(feed_address=url)
|
||||
feed = feed.update(requesting_user_id=user.pk if user else None)
|
||||
|
||||
# Check for JSON feed
|
||||
if not feed and fetch and create:
|
||||
r = requests.get(url)
|
||||
if 'application/json' in r.headers.get('Content-Type'):
|
||||
feed = cls.objects.create(feed_address=url)
|
||||
feed = feed.update()
|
||||
|
||||
# Still nothing? Maybe the URL has some clues.
|
||||
if not feed and fetch and len(found_feed_urls):
|
||||
|
@ -1073,15 +1089,6 @@ class Feed(models.Model):
|
|||
from utils import feed_fetcher
|
||||
r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
|
||||
original_feed_id = int(self.pk)
|
||||
|
||||
if getattr(settings, 'TEST_DEBUG', False):
|
||||
original_feed_address = self.feed_address
|
||||
original_feed_link = self.feed_link
|
||||
self.feed_address = self.feed_address.replace("%(NEWSBLUR_DIR)s", settings.NEWSBLUR_DIR)
|
||||
if self.feed_link:
|
||||
self.feed_link = self.feed_link.replace("%(NEWSBLUR_DIR)s", settings.NEWSBLUR_DIR)
|
||||
if self.feed_address != original_feed_address or self.feed_link != original_feed_link:
|
||||
self.save(update_fields=['feed_address', 'feed_link'])
|
||||
|
||||
options = {
|
||||
'verbose': kwargs.get('verbose'),
|
||||
|
@ -1099,6 +1106,19 @@ class Feed(models.Model):
|
|||
'feed_xml': kwargs.get('feed_xml'),
|
||||
'requesting_user_id': kwargs.get('requesting_user_id', None)
|
||||
}
|
||||
|
||||
if getattr(settings, 'TEST_DEBUG', False):
|
||||
print " ---> Testing feed fetch: %s" % self.log_title
|
||||
options['force'] = False
|
||||
# options['force_fp'] = True # No, why would this be needed?
|
||||
original_feed_address = self.feed_address
|
||||
original_feed_link = self.feed_link
|
||||
self.feed_address = self.feed_address.replace("%(NEWSBLUR_DIR)s", settings.NEWSBLUR_DIR)
|
||||
if self.feed_link:
|
||||
self.feed_link = self.feed_link.replace("%(NEWSBLUR_DIR)s", settings.NEWSBLUR_DIR)
|
||||
if self.feed_address != original_feed_address or self.feed_link != original_feed_link:
|
||||
self.save(update_fields=['feed_address', 'feed_link'])
|
||||
|
||||
if self.is_newsletter:
|
||||
feed = self.update_newsletter_icon()
|
||||
else:
|
||||
|
@ -1310,7 +1330,7 @@ class Feed(models.Model):
|
|||
|
||||
old_hash = existing_story.story_hash
|
||||
new_hash = MStory.ensure_story_hash(new_story_guid, self.pk)
|
||||
RUserStory.switch_hash(feed_id=self.pk, old_hash=old_hash, new_hash=new_hash)
|
||||
RUserStory.switch_hash(feed=self, old_hash=old_hash, new_hash=new_hash)
|
||||
|
||||
shared_stories = MSharedStory.objects.filter(story_feed_id=self.pk,
|
||||
story_hash=old_hash)
|
||||
|
@ -2168,6 +2188,7 @@ class Feed(models.Model):
|
|||
#
|
||||
# return phrases
|
||||
|
||||
|
||||
# class FeedCollocations(models.Model):
|
||||
# feed = models.ForeignKey(Feed)
|
||||
# phrase = models.CharField(max_length=500)
|
||||
|
@ -2430,6 +2451,9 @@ class MStory(mongo.Document):
|
|||
(unicode(feed)[:30], stories.count(), cutoff))
|
||||
try:
|
||||
story_trim_date = stories[cutoff].story_date
|
||||
if story_trim_date == stories[0].story_date:
|
||||
# Handle case where every story is the same time
|
||||
story_trim_date = story_trim_date - datetime.timedelta(seconds=1)
|
||||
except IndexError, e:
|
||||
logging.debug(' ***> [%-30s] ~BRError trimming feed: %s' % (unicode(feed)[:30], e))
|
||||
return extra_stories_count
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import redis
|
||||
from utils import json_functions as json
|
||||
from django.test.client import Client
|
||||
from django.test import TestCase
|
||||
|
@ -14,6 +15,16 @@ class FeedTest(TestCase):
|
|||
def setUp(self):
|
||||
disconnect()
|
||||
settings.MONGODB = connect('test_newsblur')
|
||||
settings.REDIS_STORY_HASH_POOL = redis.ConnectionPool(host=settings.REDIS_STORY['host'], port=6379, db=10)
|
||||
settings.REDIS_FEED_READ_POOL = redis.ConnectionPool(host=settings.SESSION_REDIS_HOST, port=6379, db=10)
|
||||
|
||||
r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL)
|
||||
r.delete('RS:1')
|
||||
r.delete('lRS:1')
|
||||
r.delete('RS:1:766')
|
||||
r.delete('zF:766')
|
||||
r.delete('F:766')
|
||||
|
||||
self.client = Client()
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -165,6 +176,54 @@ class FeedTest(TestCase):
|
|||
content = json.decode(response.content)
|
||||
self.assertEquals(content['feeds'][str(feed['feed_id'])]['nt'], 9)
|
||||
|
||||
def test_load_feeds__google(self):
|
||||
# Freezegun the date to 2017-04-30
|
||||
|
||||
self.client.login(username='conesus', password='test')
|
||||
old_story_guid = "blog.google:443/topics/inside-google/google-earths-incredible-3d-imagery-explained/"
|
||||
|
||||
management.call_command('loaddata', 'google1.json', verbosity=1)
|
||||
print Feed.objects.all()
|
||||
feed = Feed.objects.get(pk=766)
|
||||
print " Testing test_load_feeds__google: %s" % feed
|
||||
stories = MStory.objects(story_feed_id=feed.pk)
|
||||
self.assertEquals(stories.count(), 0)
|
||||
|
||||
management.call_command('refresh_feed', force=False, feed=766, single_threaded=True, daemonize=False)
|
||||
|
||||
stories = MStory.objects(story_feed_id=feed.pk)
|
||||
self.assertEquals(stories.count(), 20)
|
||||
|
||||
response = self.client.get(reverse('load-feeds')+"?update_counts=true")
|
||||
content = json.decode(response.content)
|
||||
self.assertEquals(content['feeds']['766']['nt'], 20)
|
||||
|
||||
old_story = MStory.objects.get(story_feed_id=feed.pk, story_guid__contains=old_story_guid)
|
||||
self.client.post(reverse('mark-story-hashes-as-read'), {'story_hash': old_story.story_hash})
|
||||
|
||||
response = self.client.get(reverse('refresh-feeds'))
|
||||
content = json.decode(response.content)
|
||||
self.assertEquals(content['feeds']['766']['nt'], 19)
|
||||
|
||||
management.call_command('loaddata', 'google2.json', verbosity=1)
|
||||
management.call_command('refresh_feed', force=False, feed=766, single_threaded=True, daemonize=False)
|
||||
|
||||
stories = MStory.objects(story_feed_id=feed.pk)
|
||||
self.assertEquals(stories.count(), 20)
|
||||
|
||||
url = reverse('load-single-feed', kwargs=dict(feed_id=766))
|
||||
response = self.client.get(url)
|
||||
|
||||
# pprint([c['story_title'] for c in json.decode(response.content)])
|
||||
feed = json.decode(response.content)
|
||||
|
||||
# Test: 1 changed char in title
|
||||
self.assertEquals(len(feed['stories']), 6)
|
||||
|
||||
response = self.client.get(reverse('refresh-feeds'))
|
||||
content = json.decode(response.content)
|
||||
self.assertEquals(content['feeds']['766']['nt'], 19)
|
||||
|
||||
def test_load_feeds__brokelyn__invalid_xml(self):
|
||||
self.client.login(username='conesus', password='test')
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ from utils.feed_functions import timelimit, TimeoutError
|
|||
from OpenSSL.SSL import Error as OpenSSLError
|
||||
from pyasn1.error import PyAsn1Error
|
||||
from django.utils.encoding import smart_str
|
||||
from django.conf import settings
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
|
||||
BROKEN_URLS = [
|
||||
|
@ -40,13 +41,43 @@ class TextImporter:
|
|||
),
|
||||
}
|
||||
|
||||
def fetch(self, skip_save=False, return_document=False):
|
||||
def fetch(self, skip_save=False, return_document=False, use_mercury=True):
|
||||
if self.story_url and any(broken_url in self.story_url for broken_url in BROKEN_URLS):
|
||||
logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: banned")
|
||||
return
|
||||
|
||||
|
||||
if use_mercury:
|
||||
results = self.fetch_mercury(skip_save=skip_save, return_document=return_document)
|
||||
|
||||
if not use_mercury or not results:
|
||||
logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY with Mercury, trying readability...", warn_color=False)
|
||||
results = self.fetch_manually(skip_save=skip_save, return_document=return_document)
|
||||
|
||||
return results
|
||||
|
||||
def fetch_mercury(self, skip_save=False, return_document=False):
|
||||
try:
|
||||
resp = self.fetch_request()
|
||||
resp = self.fetch_request(use_mercury=True)
|
||||
except TimeoutError:
|
||||
logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: timed out")
|
||||
resp = None
|
||||
except requests.exceptions.TooManyRedirects:
|
||||
logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: too many redirects")
|
||||
resp = None
|
||||
|
||||
if not resp:
|
||||
return
|
||||
|
||||
doc = resp.json()
|
||||
text = doc['content']
|
||||
title = doc['title']
|
||||
url = doc['url']
|
||||
|
||||
return self.process_content(text, title, url, skip_save=skip_save, return_document=return_document)
|
||||
|
||||
def fetch_manually(self, skip_save=False, return_document=False):
|
||||
try:
|
||||
resp = self.fetch_request(use_mercury=False)
|
||||
except TimeoutError:
|
||||
logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: timed out")
|
||||
resp = None
|
||||
|
@ -65,18 +96,18 @@ class TextImporter:
|
|||
# if self.debug:
|
||||
# logging.user(self.request, "~FBOriginal text's website: %s" % text)
|
||||
|
||||
if resp.encoding and resp.encoding != 'utf-8':
|
||||
try:
|
||||
text = text.encode(resp.encoding)
|
||||
except (LookupError, UnicodeEncodeError):
|
||||
pass
|
||||
# if resp.encoding and resp.encoding != 'utf-8':
|
||||
# try:
|
||||
# text = text.encode(resp.encoding)
|
||||
# except (LookupError, UnicodeEncodeError):
|
||||
# pass
|
||||
|
||||
if text:
|
||||
text = text.replace("\xc2\xa0", " ") # Non-breaking space, is mangled when encoding is not utf-8
|
||||
text = text.replace("\u00a0", " ") # Non-breaking space, is mangled when encoding is not utf-8
|
||||
|
||||
original_text_doc = readability.Document(text, url=resp.url,
|
||||
positive_keywords="postContent, postField")
|
||||
positive_keywords="post, entry, postProp, article, postContent, postField")
|
||||
try:
|
||||
content = original_text_doc.summary(html_partial=True)
|
||||
except (readability.Unparseable, ParserError), e:
|
||||
|
@ -91,8 +122,15 @@ class TextImporter:
|
|||
|
||||
if content:
|
||||
content = self.rewrite_content(content)
|
||||
|
||||
return self.process_content(content, title, url, skip_save=skip_save, return_document=return_document,
|
||||
original_text_doc=original_text_doc)
|
||||
|
||||
if content:
|
||||
def process_content(self, content, title, url, skip_save=False, return_document=False, original_text_doc=None):
|
||||
original_story_content = self.story and self.story.story_content_z and zlib.decompress(self.story.story_content_z)
|
||||
if not original_story_content:
|
||||
original_story_content = ""
|
||||
if content and len(content) > len(original_story_content):
|
||||
if self.story and not skip_save:
|
||||
self.story.original_text_z = zlib.compress(smart_str(content))
|
||||
try:
|
||||
|
@ -102,12 +140,13 @@ class TextImporter:
|
|||
pass
|
||||
logging.user(self.request, ("~SN~FYFetched ~FGoriginal text~FY: now ~SB%s bytes~SN vs. was ~SB%s bytes" % (
|
||||
len(content),
|
||||
self.story and self.story.story_content_z and len(zlib.decompress(self.story.story_content_z))
|
||||
len(original_story_content)
|
||||
)), warn_color=False)
|
||||
else:
|
||||
logging.user(self.request, ("~SN~FRFailed~FY to fetch ~FGoriginal text~FY: was ~SB%s bytes" % (
|
||||
self.story and self.story.story_content_z and len(zlib.decompress(self.story.story_content_z))
|
||||
len(original_story_content)
|
||||
)), warn_color=False)
|
||||
return
|
||||
|
||||
if return_document:
|
||||
return dict(content=content, title=title, url=url, doc=original_text_doc)
|
||||
|
@ -124,12 +163,20 @@ class TextImporter:
|
|||
return unicode(soup)
|
||||
|
||||
@timelimit(10)
|
||||
def fetch_request(self):
|
||||
def fetch_request(self, use_mercury=True):
|
||||
headers = self.headers
|
||||
url = self.story_url
|
||||
if self.story and not url:
|
||||
url = self.story.story_permalink
|
||||
|
||||
if use_mercury:
|
||||
mercury_api_key = getattr(settings, 'MERCURY_PARSER_API_KEY', 'abc123')
|
||||
headers["content-type"] = "application/json"
|
||||
headers["x-api-key"] = mercury_api_key
|
||||
url = "https://mercury.postlight.com/parser?url=%s" % url
|
||||
|
||||
try:
|
||||
r = requests.get(url, headers=self.headers, verify=False)
|
||||
r = requests.get(url, headers=headers, verify=False)
|
||||
r.connection.close()
|
||||
except (AttributeError, SocketError, requests.ConnectionError,
|
||||
requests.models.MissingSchema, requests.sessions.InvalidSchema,
|
||||
|
|
|
@ -135,6 +135,7 @@ class MSocialProfile(mongo.Document):
|
|||
story_count_history = mongo.ListField()
|
||||
story_days_history = mongo.DictField()
|
||||
story_hours_history = mongo.DictField()
|
||||
story_email_history = mongo.ListField()
|
||||
feed_classifier_counts = mongo.DictField()
|
||||
favicon_color = mongo.StringField(max_length=6)
|
||||
protected = mongo.BooleanField()
|
||||
|
@ -779,7 +780,33 @@ class MSocialProfile(mongo.Document):
|
|||
if scores:
|
||||
self.feed_classifier_counts = scores
|
||||
self.save()
|
||||
|
||||
def save_sent_email(self, max_quota=20):
|
||||
if not self.story_email_history:
|
||||
self.story_email_history = []
|
||||
|
||||
self.story_email_history.insert(0, datetime.datetime.now())
|
||||
self.story_email_history = self.story_email_history[:max_quota]
|
||||
|
||||
self.save()
|
||||
|
||||
def over_story_email_quota(self, quota=1, hours=24):
|
||||
counted = 0
|
||||
day_ago = datetime.datetime.now() - datetime.timedelta(hours=hours)
|
||||
sent_emails = self.story_email_history
|
||||
|
||||
if not sent_emails:
|
||||
sent_emails = []
|
||||
|
||||
for sent_date in sent_emails:
|
||||
if sent_date > day_ago:
|
||||
counted += 1
|
||||
|
||||
if counted >= quota:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
class MSocialSubscription(mongo.Document):
|
||||
UNREAD_CUTOFF = datetime.datetime.utcnow() - datetime.timedelta(days=settings.DAYS_OF_UNREAD)
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.newsblur"
|
||||
android:versionCode="136"
|
||||
android:versionName="5.2.0b1" >
|
||||
android:versionCode="145"
|
||||
android:versionName="6.1.0" >
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="16"
|
||||
android:targetSdkVersion="23" />
|
||||
android:minSdkVersion="19"
|
||||
android:targetSdkVersion="24" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
@ -145,6 +145,59 @@
|
|||
<receiver android:name=".util.NotifySaveReceiver" android:exported="false" />
|
||||
<receiver android:name=".util.NotifyMarkreadReceiver" android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="com.newsblur.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<activity android:name=".activity.AddFeedExternal">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="*"/>
|
||||
<data android:pathPattern=".*xml"/>
|
||||
<data android:pathPattern=".*rss"/>
|
||||
<data android:pathPattern=".*atom"/>
|
||||
<data android:pathPattern=".*json"/>
|
||||
<data android:pathPattern=".*/feed.*"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="feed" />
|
||||
<data android:scheme="rss" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="feeds.feedburner.com"/>
|
||||
<data android:host="feeds2.feedburner.com"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:mimeType="text/xml"/>
|
||||
<data android:mimeType="application/rss+xml"/>
|
||||
<data android:mimeType="application/atom+xml"/>
|
||||
<data android:mimeType="application/xml"/>
|
||||
<data android:mimeType="application/json"/>
|
||||
<data android:mimeType="application/feed+json"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
BIN
clients/android/NewsBlur/assets/fonts/ChronicleSSm-Book.otf
Executable file
BIN
clients/android/NewsBlur/assets/fonts/GothamNarrow-Book.otf
Executable file
BIN
clients/android/NewsBlur/assets/fonts/WhitneySSm-Book-Bas.otf
Normal file
|
@ -4,7 +4,7 @@ buildscript {
|
|||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.3.0'
|
||||
classpath 'com.android.tools.build:gradle:2.3.3'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,13 +17,13 @@ apply plugin: 'com.android.application'
|
|||
dependencies {
|
||||
compile 'com.android.support:support-v13:19.1.0'
|
||||
compile 'com.jakewharton:butterknife:7.0.1'
|
||||
compile 'com.squareup.okhttp3:okhttp:3.4.1'
|
||||
compile 'com.google.code.gson:gson:2.6.2'
|
||||
compile 'com.squareup.okhttp3:okhttp:3.8.1'
|
||||
compile 'com.google.code.gson:gson:2.7'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion '25.0.0'
|
||||
compileSdkVersion 24
|
||||
buildToolsVersion '25.0.2'
|
||||
compileOptions.with {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_7
|
||||
}
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
<module name="Checker">
|
||||
<property name="fileExtensions" value="java, properties, xml" />
|
||||
<module name="TreeWalker">
|
||||
<module name="ArrayTrailingComma"/>
|
||||
<module name="ArrayTypeStyle"/>
|
||||
<module name="EqualsHashCode"/>
|
||||
<module name="HideUtilityClassConstructor"/>
|
||||
<module name="NoFinalizer"/>
|
||||
<module name="OuterTypeFilename"/>
|
||||
<module name="OverloadMethodsDeclarationOrder"/>
|
||||
<module name="UnusedImports"/>
|
||||
<module name="UpperEll"/>
|
||||
<module name="ArrayTypeStyle"/>
|
||||
<module name="OuterTypeFilename"/>
|
||||
<module name="ArrayTrailingComma"/>
|
||||
<module name="EqualsHashCode"/>
|
||||
<module name="NoFinalizer"/>
|
||||
<module name="HideUtilityClassConstructor"/>
|
||||
</module>
|
||||
</module>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
-dontobfuscate
|
||||
-printusage
|
||||
|
||||
-keepattributes Exceptions,InnerClasses,Signature
|
||||
-keepattributes *Annotation*
|
||||
|
@ -20,6 +21,10 @@
|
|||
@butterknife.* <methods>;
|
||||
}
|
||||
|
||||
# these two seem to confuse ProGuard, so force keep them
|
||||
-keep class com.newsblur.util.StateFilter { *; }
|
||||
-keep class com.newsblur.view.StateToggleButton$StateChangedListener { *; }
|
||||
|
||||
# we use proguard only as an APK shrinker and many of our dependencies throw
|
||||
# all manner of gross warnings. kept silent by default, the following lines
|
||||
# can be commented out to help diagnose shrinkage errors.
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
target=android-23
|
||||
target=android-24
|
||||
proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
|
||||
|
|
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 2 KiB |
BIN
clients/android/NewsBlur/res/drawable/ic_edit_gray55.png
Executable file
After Width: | Height: | Size: 3.3 KiB |
BIN
clients/android/NewsBlur/res/drawable/ic_reply_gray55.png
Executable file
After Width: | Height: | Size: 3.6 KiB |
BIN
clients/android/NewsBlur/res/drawable/ic_shared.png
Executable file
After Width: | Height: | Size: 4 KiB |
BIN
clients/android/NewsBlur/res/drawable/ic_star_active.png
Executable file
After Width: | Height: | Size: 3.9 KiB |
BIN
clients/android/NewsBlur/res/drawable/ic_star_gray55.png
Executable file
After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
style="?itemBackground" >
|
||||
|
||||
<com.newsblur.view.ProgressThrobber
|
||||
android:id="@+id/loading_throb"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_height="6dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/progress_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginTop="25dp"
|
||||
android:gravity="center_horizontal"
|
||||
style="?explainerText"
|
||||
android:visibility="invisible"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -3,7 +3,8 @@
|
|||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true" >
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/main_top_bar"
|
||||
|
@ -151,12 +152,26 @@
|
|||
|
||||
</RelativeLayout>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/feedlist_search_query"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/main_top_bar"
|
||||
android:textSize="16sp"
|
||||
android:hint="@string/your_feeds_search_hint"
|
||||
android:drawableLeft="@android:drawable/ic_menu_search"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:imeOptions="actionSearch"
|
||||
android:visibility="gone"
|
||||
android:animateLayoutChanges="true"
|
||||
/>
|
||||
|
||||
<!-- The scrollable and pull-able feed list. -->
|
||||
<android.support.v4.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/main_top_bar"
|
||||
android:layout_below="@id/feedlist_search_query"
|
||||
android:layout_above="@id/fragment_feedintelligenceselector" >
|
||||
|
||||
<fragment
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="10dp"
|
||||
android:paddingLeft="25dip"
|
||||
android:paddingRight="25dip"
|
||||
android:paddingBottom="10dip"
|
||||
android:paddingTop="10dip"
|
||||
>
|
||||
|
||||
<RadioButton android:id="@+id/radio_story"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/story"/>
|
||||
<RadioButton android:id="@+id/radio_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/text"/>
|
||||
|
||||
</RadioGroup>
|
|
@ -51,7 +51,7 @@
|
|||
android:layout_alignParentRight="true"
|
||||
android:layout_marginRight="10dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/reply" />
|
||||
android:src="@drawable/ic_reply_gray55" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/comment_shareddate"
|
||||
|
@ -71,7 +71,7 @@
|
|||
android:layout_marginRight="8dp"
|
||||
android:layout_toLeftOf="@id/comment_shareddate"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/favourite" />
|
||||
android:src="@drawable/ic_star_gray55" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/comment_username"
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
android:orientation="horizontal"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingLeft="60dp"
|
||||
android:paddingRight="10dp"
|
||||
android:paddingTop="12dp" >
|
||||
|
||||
<View
|
||||
|
@ -28,13 +27,24 @@
|
|||
android:contentDescription="@string/description_comment_user"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/reply_edit_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginTop="3dp"
|
||||
android:padding="2dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_edit_gray55" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reply_shareddate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_marginRight="26dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_toLeftOf="@id/reply_edit_icon"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginTop="8dp"
|
||||
style="?defaultText"
|
||||
android:textSize="12sp" />
|
||||
|
||||
|
|
46
clients/android/NewsBlur/res/layout/readingfont_dialog.xml
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="10dp"
|
||||
android:paddingLeft="25dip"
|
||||
android:paddingRight="25dip"
|
||||
android:paddingBottom="10dip"
|
||||
android:paddingTop="10dip">
|
||||
|
||||
<RadioButton android:id="@+id/radio_anonymous"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/anonymous_pro_font"/>
|
||||
<RadioButton android:id="@+id/radio_chronicle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/chronicle_font"/>
|
||||
<RadioButton android:id="@+id/radio_default"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/default_font"/>
|
||||
<RadioButton android:id="@+id/radio_gotham"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/gotham_narrow_font"/>
|
||||
<RadioButton android:id="@+id/radio_noto_sans"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/noto_sans_font"/>
|
||||
<RadioButton android:id="@+id/radio_noto_serif"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/noto_serif_font"/>
|
||||
<RadioButton android:id="@+id/radio_open_sans_condensed"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/open_sans_condensed_font"/>
|
||||
<RadioButton android:id="@+id/radio_whitney"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/whitney_font"/>
|
||||
|
||||
</RadioGroup>
|
|
@ -66,13 +66,20 @@
|
|||
android:id="@+id/row_feedmuteicon"
|
||||
android:layout_width="19dp"
|
||||
android:layout_height="19dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginRight="3dp"
|
||||
android:paddingLeft="3dp"
|
||||
android:paddingRight="3dp"
|
||||
style="?muteicon"
|
||||
android:background="@drawable/neutral_count_rect"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/row_feedfetching"
|
||||
android:layout_width="21dp"
|
||||
android:layout_height="21dp"
|
||||
android:layout_marginRight="3dp"
|
||||
android:visibility="gone"
|
||||
android:indeterminate="true" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
|
|
|
@ -76,11 +76,24 @@
|
|||
android:layout_marginRight="8dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/row_item_shared_icon"
|
||||
android:src="@drawable/ic_shared"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_below="@id/row_item_feedtitle"
|
||||
android:layout_toLeftOf="@id/row_item_saved_icon"
|
||||
android:layout_alignWithParentIfMissing="true"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginLeft="2dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/row_item_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toLeftOf="@id/row_item_saved_icon"
|
||||
android:layout_toLeftOf="@id/row_item_shared_icon"
|
||||
android:layout_below="@id/row_item_feedicon"
|
||||
android:layout_marginLeft="33dp"
|
||||
android:paddingTop="6dp"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item android:id="@+id/menu_default_view"
|
||||
android:title="@string/menu_default_view"
|
||||
android:showAsAction="never" />
|
||||
</menu>
|
||||
<item
|
||||
android:id="@+id/menu_textsize"
|
||||
android:showAsAction="never"
|
||||
android:title="@string/menu_textsize"/>
|
||||
</menu>
|
||||
|
|
|
@ -9,13 +9,9 @@
|
|||
<item android:id="@+id/menu_read_filter"
|
||||
android:title="@string/menu_read_filter"
|
||||
android:showAsAction="never" />
|
||||
<item android:id="@+id/menu_default_view"
|
||||
android:title="@string/menu_default_view"
|
||||
android:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/menu_textsize"
|
||||
android:showAsAction="never"
|
||||
android:title="@string/menu_textsize"/>
|
||||
<item android:id="@+id/menu_textsize"
|
||||
android:showAsAction="never"
|
||||
android:title="@string/menu_textsize"/>
|
||||
<item android:id="@+id/menu_search_stories"
|
||||
android:title="@string/menu_search_stories"
|
||||
android:showAsAction="ifRoom" android:icon="@drawable/ic_menu_search_gray55" />
|
||||
|
|
|
@ -13,9 +13,29 @@
|
|||
<item android:id="@+id/menu_choose_folders"
|
||||
android:title="@string/menu_choose_folders" />
|
||||
|
||||
<item android:id="@+id/menu_notifications"
|
||||
android:title="@string/menu_notifications_choose" >
|
||||
<menu>
|
||||
<group android:checkableBehavior="single">
|
||||
<item android:id="@+id/menu_notifications_disable"
|
||||
android:title="@string/menu_notifications_disable"
|
||||
android:checkable="true" />
|
||||
<item android:id="@+id/menu_notifications_focus"
|
||||
android:title="@string/menu_notifications_focus"
|
||||
android:checkable="true" />
|
||||
<item android:id="@+id/menu_notifications_unread"
|
||||
android:title="@string/menu_notifications_unread"
|
||||
android:checkable="true" />
|
||||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
|
||||
<item android:id="@+id/menu_mute_feed"
|
||||
android:title="@string/menu_mute_feed" />
|
||||
|
||||
<item android:id="@+id/menu_unmute_feed"
|
||||
android:title="@string/menu_unmute_feed" />
|
||||
|
||||
<item android:id="@+id/menu_instafetch_feed"
|
||||
android:title="@string/menu_instafetch_feed" />
|
||||
</menu>
|
||||
|
|
|
@ -12,13 +12,27 @@
|
|||
<item android:id="@+id/menu_read_filter"
|
||||
android:title="@string/menu_read_filter"
|
||||
android:showAsAction="never" />
|
||||
<item android:id="@+id/menu_default_view"
|
||||
android:title="@string/menu_default_view"
|
||||
android:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/menu_textsize"
|
||||
android:showAsAction="never"
|
||||
android:title="@string/menu_textsize"/>
|
||||
<item android:id="@+id/menu_textsize"
|
||||
android:showAsAction="never"
|
||||
android:title="@string/menu_textsize"/>
|
||||
<item android:id="@+id/menu_notifications"
|
||||
android:title="@string/menu_notifications_choose" >
|
||||
<menu>
|
||||
<group android:checkableBehavior="single">
|
||||
<item android:id="@+id/menu_notifications_disable"
|
||||
android:title="@string/menu_notifications_disable"
|
||||
android:checkable="true" />
|
||||
<item android:id="@+id/menu_notifications_focus"
|
||||
android:title="@string/menu_notifications_focus"
|
||||
android:checkable="true" />
|
||||
<item android:id="@+id/menu_notifications_unread"
|
||||
android:title="@string/menu_notifications_unread"
|
||||
android:checkable="true" />
|
||||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
<item android:id="@+id/menu_instafetch_feed"
|
||||
android:title="@string/menu_instafetch_feed" />
|
||||
<item android:id="@+id/menu_search_stories"
|
||||
android:title="@string/menu_search_stories"
|
||||
android:showAsAction="ifRoom" android:icon="@drawable/ic_menu_search_gray55" />
|
||||
|
|
|
@ -9,13 +9,9 @@
|
|||
<item android:id="@+id/menu_read_filter"
|
||||
android:title="@string/menu_read_filter"
|
||||
android:showAsAction="never" />
|
||||
<item android:id="@+id/menu_default_view"
|
||||
android:title="@string/menu_default_view"
|
||||
android:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/menu_textsize"
|
||||
android:showAsAction="never"
|
||||
android:title="@string/menu_textsize"/>
|
||||
<item android:id="@+id/menu_textsize"
|
||||
android:showAsAction="never"
|
||||
android:title="@string/menu_textsize"/>
|
||||
<item android:id="@+id/menu_search_stories"
|
||||
android:title="@string/menu_search_stories"
|
||||
android:showAsAction="ifRoom" android:icon="@drawable/ic_menu_search_gray55" />
|
||||
|
|
|
@ -4,19 +4,18 @@
|
|||
<item android:id="@+id/menu_refresh"
|
||||
android:title="@string/menu_refresh"
|
||||
android:showAsAction="never" />
|
||||
|
||||
<item android:id="@+id/menu_profile"
|
||||
android:title="@string/menu_profile"
|
||||
|
||||
<item android:id="@+id/menu_search_feeds"
|
||||
android:title="@string/menu_search_feeds"
|
||||
android:showAsAction="never" />
|
||||
|
||||
<item android:id="@+id/menu_add_feed"
|
||||
android:title="@string/menu_add_feed"
|
||||
android:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_textsize"
|
||||
android:showAsAction="never"
|
||||
android:title="@string/menu_textsize"/>
|
||||
<item android:id="@+id/menu_textsize"
|
||||
android:showAsAction="never"
|
||||
android:title="@string/menu_textsize"/>
|
||||
|
||||
<item android:id="@+id/menu_settings"
|
||||
android:title="@string/settings"
|
||||
|
@ -45,4 +44,16 @@
|
|||
android:title="@string/menu_logout"
|
||||
android:showAsAction="never" />
|
||||
|
||||
<item android:id="@+id/menu_theme"
|
||||
android:title="@string/menu_theme_choose" >
|
||||
<menu>
|
||||
<group android:checkableBehavior="single">
|
||||
<item android:id="@+id/menu_theme_light"
|
||||
android:title="@string/light" />
|
||||
<item android:id="@+id/menu_theme_dark"
|
||||
android:title="@string/dark" />
|
||||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
|
||||
</menu>
|
||||
|
|
|
@ -27,6 +27,11 @@
|
|||
android:id="@+id/menu_textsize"
|
||||
android:showAsAction="never"
|
||||
android:title="@string/menu_textsize"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_font"
|
||||
android:showAsAction="never"
|
||||
android:title="@string/menu_font"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_reading_save"
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item android:id="@+id/menu_default_view"
|
||||
android:title="@string/menu_default_view"
|
||||
android:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/menu_textsize"
|
||||
android:showAsAction="never"
|
||||
|
|
|
@ -3,13 +3,9 @@
|
|||
<item android:id="@+id/menu_story_order"
|
||||
android:title="@string/menu_story_order"
|
||||
android:showAsAction="never" />
|
||||
<item android:id="@+id/menu_default_view"
|
||||
android:title="@string/menu_default_view"
|
||||
android:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/menu_textsize"
|
||||
android:showAsAction="never"
|
||||
android:title="@string/menu_textsize"/>
|
||||
<item android:id="@+id/menu_textsize"
|
||||
android:showAsAction="never"
|
||||
android:title="@string/menu_textsize"/>
|
||||
<item android:id="@+id/menu_search_stories"
|
||||
android:title="@string/menu_search_stories"
|
||||
android:showAsAction="ifRoom" android:icon="@drawable/ic_menu_search_gray55" />
|
||||
|
|
|
@ -54,7 +54,8 @@
|
|||
<string name="share_this">SHARE</string>
|
||||
<string name="already_shared">SHARED</string>
|
||||
<string name="share_this_story">SHARE</string>
|
||||
<string name="update_shared">Update comment</string>
|
||||
<string name="unshare">DELETE SHARE</string>
|
||||
<string name="update_shared">UPDATE COMMENT</string>
|
||||
|
||||
<string name="save_this">SAVE</string>
|
||||
<string name="unsave_this">REMOVE FROM SAVED</string>
|
||||
|
@ -65,6 +66,7 @@
|
|||
<string name="overlay_count_toast_1">1 unread story</string>
|
||||
<string name="overlay_text">TEXT</string>
|
||||
<string name="overlay_story">STORY</string>
|
||||
<string name="text_mode_unavailable">Sorry, the story\'s text could not be extracted.</string>
|
||||
|
||||
<string name="state_all">ALL</string>
|
||||
<string name="state_unread">UNREAD</string>
|
||||
|
@ -72,6 +74,9 @@
|
|||
<string name="state_saved">SAVED</string>
|
||||
|
||||
<string name="reply_to">Reply to \"%s\"</string>
|
||||
<string name="edit_reply">Edit reply</string>
|
||||
<string name="edit_reply_update">UPDATE REPLY</string>
|
||||
<string name="edit_reply_delete">DELTE REPLY</string>
|
||||
|
||||
<string name="alert_dialog_ok">OKAY</string>
|
||||
<string name="alert_dialog_cancel">CANCEL</string>
|
||||
|
@ -112,6 +117,7 @@
|
|||
|
||||
<string name="menu_profile">Profile</string>
|
||||
<string name="menu_refresh">Refresh</string>
|
||||
<string name="menu_search_feeds">Search your feeds</string>
|
||||
<string name="menu_original">View original</string>
|
||||
<string name="menu_send_story">Send link to…</string>
|
||||
<string name="menu_send_story_full">Send story to…</string>
|
||||
|
@ -119,9 +125,14 @@
|
|||
<string name="menu_delete_feed">Delete feed</string>
|
||||
<string name="menu_unfollow">Unfollow user</string>
|
||||
<string name="menu_choose_folders">Choose folders</string>
|
||||
<string name="menu_notifications_choose">Notifications…</string>
|
||||
<string name="menu_notifications_disable">Disable notifications</string>
|
||||
<string name="menu_notifications_focus">Notify on new focus stories</string>
|
||||
<string name="menu_notifications_unread">Notify on all new stories</string>
|
||||
<string name="menu_mark_folder_as_read">Mark folder as read</string>
|
||||
<string name="menu_sharenewsblur">Share this story</string>
|
||||
<string name="menu_textsize">Adjust text size</string>
|
||||
<string name="menu_font">Change font</string>
|
||||
<string name="menu_save_story">Save this story</string>
|
||||
<string name="menu_unsave_story">Unsave this story</string>
|
||||
<string name="menu_newest_mark_older_stories_as_read">\u21E3 Mark older as read</string>
|
||||
|
@ -136,21 +147,28 @@
|
|||
<string name="menu_unmute_feed">Unmute feed</string>
|
||||
<string name="menu_mute_folder">Mute folder</string>
|
||||
<string name="menu_unmute_folder">Unmute folder</string>
|
||||
<string name="menu_instafetch_feed">Insta-fetch stories</string>
|
||||
|
||||
<string name="toast_story_unread">Story marked as unread</string>
|
||||
|
||||
<string name="logout_warning">Are you sure you want to log out?</string>
|
||||
<string name="search_mark_read_warning">Look out! All stories in feed/folder would be marked read, not just search results. If you really want to mark all stories read, please clear your search query to confirm.</string>
|
||||
|
||||
<string name="feed_search_hint">Search for new feeds</string>
|
||||
<string name="your_feeds_search_hint">Search your feeds</string>
|
||||
<string name="feed_search_hint">Search term or feed URL</string>
|
||||
<string name="story_search_hint">Search for stories</string>
|
||||
<string name="empty_search_notice">Type a search term to begin</string>
|
||||
<string name="menu_add_feed">Add new feed</string>
|
||||
<string name="menu_search">Search</string>
|
||||
<string name="adding_feed_progress">Adding feed…</string>
|
||||
<string name="add_feed_error">Could not add feed</string>
|
||||
<string name="menu_mark_all_as_read">Mark all as read</string>
|
||||
<string name="menu_logout">Log out</string>
|
||||
<string name="menu_feedback">Send app feedback</string>
|
||||
<string name="menu_feedback_post">Create a feedback post</string>
|
||||
<string name="menu_feedback_email">Email a bug report</string>
|
||||
<string name="menu_theme_choose">Theme…</string>
|
||||
|
||||
<string name="menu_loginas">Login as...</string>
|
||||
|
||||
<string name="loginas_title">Login As User</string>
|
||||
|
@ -183,8 +201,6 @@
|
|||
<string name="delete_feed_message">Delete feed \"%s\"?</string>
|
||||
<string name="unfollow_message">Unfollow \"%s\"?</string>
|
||||
|
||||
<string name="menu_default_view">Default View</string>
|
||||
|
||||
<string name="settings">Preferences</string>
|
||||
|
||||
<string name="settings_cat_offline">Offline</string>
|
||||
|
@ -256,6 +272,9 @@
|
|||
<string name="settings_auto_open_first_unread">Auto-Open First Story</string>
|
||||
<string name="settings_auto_open_first_unread_sum">Automatically open first unread story from story list</string>
|
||||
|
||||
<string name="settings_mark_read_on_scroll">Mark Stories Read On Scroll</string>
|
||||
<string name="settings_mark_read_on_scroll_sum">Automatically mark stories as read when scrolled past</string>
|
||||
|
||||
<string name="settings_social">Social</string>
|
||||
<string name="settings_show_public_comments">Show Public Comments</string>
|
||||
<string name="settings_reading">Reading</string>
|
||||
|
@ -366,4 +385,43 @@
|
|||
<item>GEST_ACTION_UNSAVE</item>
|
||||
</string-array>
|
||||
<string name="rtl_gesture_action_value">GEST_ACTION_MARKUNREAD</string>
|
||||
|
||||
<string name="font">Font</string>
|
||||
<string name="default_font">Default</string>
|
||||
<string name="whitney_font">Whitney</string>
|
||||
<string name="gotham_narrow_font">Gotham Narrow</string>
|
||||
<string name="chronicle_font">Chronicle</string>
|
||||
<string name="noto_sans_font">Noto Sans</string>
|
||||
<string name="noto_serif_font">Noto Serif</string>
|
||||
<string name="open_sans_condensed_font">Open Sans Condensed</string>
|
||||
<string name="anonymous_pro_font">Anonymous Pro</string>
|
||||
<string name="anonymous_pro_font_prefvalue">ANONYMOUS_PRO</string>
|
||||
<string name="default_font_prefvalue">DEFAULT</string>
|
||||
<string name="whitney_font_prefvalue">WHITNEY</string>
|
||||
<string name="gotham_narrow_font_prefvalue">GOTHAM_NARROW</string>
|
||||
<string name="chronicle_font_prefvalue">CHRONICLE</string>
|
||||
<string name="noto_sans_font_prefvalue">NOTO_SANS</string>
|
||||
<string name="noto_serif_font_prefvalue">NOTO_SERIF</string>
|
||||
<string name="open_sans_condensed_font_prefvalue">OPEN_SANS_CONDENSED</string>
|
||||
<string-array name="default_font_entries">
|
||||
<item>@string/anonymous_pro_font</item>
|
||||
<item>@string/chronicle_font</item>
|
||||
<item>@string/default_font</item>
|
||||
<item>@string/gotham_narrow_font</item>
|
||||
<item>@string/noto_sans_font</item>
|
||||
<item>@string/noto_serif_font</item>
|
||||
<item>@string/open_sans_condensed_font</item>
|
||||
<item>@string/whitney_font</item>
|
||||
</string-array>
|
||||
<string-array name="default_font_values">
|
||||
<item>@string/anonymous_pro_font_prefvalue</item>
|
||||
<item>@string/chronicle_font_prefvalue</item>
|
||||
<item>@string/default_font_prefvalue</item>
|
||||
<item>@string/gotham_narrow_font_prefvalue</item>
|
||||
<item>@string/noto_sans_font_prefvalue</item>
|
||||
<item>@string/noto_sans_font_prefvalue</item>
|
||||
<item>@string/open_sans_condensed_font_prefvalue</item>
|
||||
<item>@string/whitney_font_prefvalue</item>
|
||||
</string-array>
|
||||
<string name="default_font_value">DEFAULT</string>
|
||||
</resources>
|
||||
|
|
|
@ -340,4 +340,14 @@
|
|||
<style name="muteicon.dark">
|
||||
<item name="android:src">@drawable/mute_black</item>
|
||||
</style>
|
||||
|
||||
<!-- these fix the Android framework bug that causes context menus to get badly
|
||||
truncated when the Holo theme is used on a v7+ device:
|
||||
https://issuetracker.google.com/issues/37118658 -->
|
||||
<style name="contextPopupStyle" parent="@android:style/Widget.Holo.Light.PopupMenu">
|
||||
<item name="android:overlapAnchor">true</item>
|
||||
</style>
|
||||
<style name="contextPopupStyle.dark" parent="@android:style/Widget.Holo.PopupMenu">
|
||||
<item name="android:overlapAnchor">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
<item name="selectorOverlayBackgroundStory">@style/selectorOverlayBackgroundStory</item>
|
||||
<item name="selectorOverlayBackgroundText">@style/selectorOverlayBackgroundText</item>
|
||||
<item name="muteicon">@style/muteicon</item>
|
||||
<item name="android:contextPopupMenuStyle">@style/contextPopupStyle</item>
|
||||
</style>
|
||||
|
||||
<style name="NewsBlurDarkTheme" parent="@android:style/Theme.Holo" >
|
||||
|
@ -82,5 +83,6 @@
|
|||
<item name="selectorOverlayBackgroundStory">@style/selectorOverlayBackgroundStory.dark</item>
|
||||
<item name="selectorOverlayBackgroundText">@style/selectorOverlayBackgroundText.dark</item>
|
||||
<item name="muteicon">@style/muteicon.dark</item>
|
||||
<item name="android:contextPopupMenuStyle">@style/contextPopupStyle.dark</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
|
@ -68,11 +68,23 @@
|
|||
android:defaultValue="true"
|
||||
android:key="pref_show_thumbnails"
|
||||
android:title="@string/settings_show_thumbnails" />
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="false"
|
||||
android:key="pref_mark_read_on_scroll"
|
||||
android:title="@string/settings_mark_read_on_scroll"
|
||||
android:summary="@string/settings_mark_read_on_scroll_sum" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/settings_reading"
|
||||
android:key="reading">
|
||||
<ListPreference
|
||||
android:key="reading_font"
|
||||
android:title="@string/font"
|
||||
android:dialogTitle="@string/font"
|
||||
android:entries="@array/default_font_entries"
|
||||
android:entryValues="@array/default_font_values"
|
||||
android:defaultValue="@string/default_font_value" />
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="false"
|
||||
android:key="immersive_enter_single_tap"
|
||||
|
|
5
clients/android/NewsBlur/res/xml/file_paths.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-cache-path name="external_cache" />
|
||||
<external-path name="external_files" />
|
||||
</paths>
|
|
@ -22,6 +22,9 @@ public class AddFacebook extends NbActivity {
|
|||
webview.getSettings().setJavaScriptEnabled(true);
|
||||
|
||||
webview.setWebViewClient(new WebViewClient() {
|
||||
// this was deprecated in API 24 but the replacement only added in the same release.
|
||||
// the suppression can be removed when we move past 24
|
||||
@SuppressWarnings("deprecation")
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url){
|
||||
if (TextUtils.equals(url, APIConstants.buildUrl("/"))) {
|
||||
AddFacebook.this.setResult(FACEBOOK_AUTHED);
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.app.DialogFragment;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.Bind;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.fragment.AddFeedFragment;
|
||||
import com.newsblur.util.UIUtils;
|
||||
import com.newsblur.util.ViewUtils;
|
||||
import com.newsblur.view.ProgressThrobber;
|
||||
|
||||
public class AddFeedExternal extends NbActivity implements AddFeedFragment.AddFeedProgressListener {
|
||||
|
||||
@Bind(R.id.loading_throb) ProgressThrobber progressView;
|
||||
@Bind(R.id.progress_text) TextView progressText;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
setContentView(R.layout.activity_addfeedexternal);
|
||||
ButterKnife.bind(this);
|
||||
|
||||
progressView.setEnabled(!ViewUtils.isPowerSaveMode(this));
|
||||
progressView.setColors(UIUtils.getColor(this, R.color.refresh_1),
|
||||
UIUtils.getColor(this, R.color.refresh_2),
|
||||
UIUtils.getColor(this, R.color.refresh_3),
|
||||
UIUtils.getColor(this, R.color.refresh_4));
|
||||
|
||||
Intent intent = getIntent();
|
||||
Uri uri = intent.getData();
|
||||
|
||||
com.newsblur.util.Log.d(this, "intent filter caught feed-like URI: " + uri);
|
||||
|
||||
DialogFragment addFeedFragment = AddFeedFragment.newInstance(uri.toString(), uri.toString());
|
||||
addFeedFragment.show(getFragmentManager(), "dialog");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addFeedStarted() {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
progressText.setText(R.string.adding_feed_progress);
|
||||
progressText.setVisibility(View.VISIBLE);
|
||||
progressView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleUpdate(int updateType) {
|
||||
; // we don't care about anything but completion
|
||||
}
|
||||
|
||||
}
|
|
@ -22,6 +22,9 @@ public class AddTwitter extends NbActivity {
|
|||
webview.getSettings().setJavaScriptEnabled(true);
|
||||
|
||||
webview.setWebViewClient(new WebViewClient() {
|
||||
// this was deprecated in API 24 but the replacement only added in the same release.
|
||||
// the suppression can be removed when we move past 24
|
||||
@SuppressWarnings("deprecation")
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url){
|
||||
if (TextUtils.equals(url, APIConstants.buildUrl("/"))) {
|
||||
AddTwitter.this.setResult(TWITTER_AUTHED);
|
||||
|
|
|
@ -7,7 +7,6 @@ import android.view.MenuInflater;
|
|||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.fragment.AllSharedStoriesItemListFragment;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
import com.newsblur.util.PrefConstants;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.ReadFilter;
|
||||
|
@ -48,12 +47,4 @@ public class AllSharedStoriesItemsList extends ItemsList {
|
|||
return PrefsUtils.getReadFilterForFolder(this, PrefConstants.ALL_SHARED_STORIES_FOLDER_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void defaultFeedViewChanged(DefaultFeedView value) {
|
||||
PrefsUtils.setDefaultFeedViewForFolder(this, PrefConstants.ALL_SHARED_STORIES_FOLDER_NAME, value);
|
||||
if (itemListFragment != null) {
|
||||
itemListFragment.setDefaultFeedView(value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ public class AllSharedStoriesReading extends Reading {
|
|||
UIUtils.setCustomActionBar(this, R.drawable.ak_icon_blurblogs, getResources().getString(R.string.all_shared_stories));
|
||||
|
||||
// No sourceUserId since this is all shared stories. The sourceUsedId for each story will be used.
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), defaultFeedView, null);
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), null);
|
||||
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import android.view.MenuInflater;
|
|||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.fragment.AllStoriesItemListFragment;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
import com.newsblur.util.PrefConstants;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.ReadFilter;
|
||||
|
@ -49,11 +48,4 @@ public class AllStoriesItemsList extends ItemsList {
|
|||
return PrefsUtils.getReadFilterForFolder(this, PrefConstants.ALL_STORIES_FOLDER_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void defaultFeedViewChanged(DefaultFeedView value) {
|
||||
PrefsUtils.setDefaultFeedViewForFolder(this, PrefConstants.ALL_STORIES_FOLDER_NAME, value);
|
||||
if (itemListFragment != null) {
|
||||
itemListFragment.setDefaultFeedView(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ public class AllStoriesReading extends Reading {
|
|||
|
||||
UIUtils.setCustomActionBar(this, R.drawable.ak_icon_allstories, getResources().getString(R.string.all_stories_row_title));
|
||||
setTitle(getResources().getString(R.string.all_stories_row_title));
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), defaultFeedView, null);
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), null);
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import com.newsblur.R;
|
|||
import com.newsblur.domain.Feed;
|
||||
import com.newsblur.fragment.DeleteFeedFragment;
|
||||
import com.newsblur.fragment.FeedItemListFragment;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.ReadFilter;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
@ -49,23 +49,75 @@ public class FeedItemsList extends ItemsList {
|
|||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (!super.onOptionsItemSelected(item)) {
|
||||
if (item.getItemId() == R.id.menu_delete_feed) {
|
||||
deleteFeed();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
if (super.onOptionsItemSelected(item)) {
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.menu_delete_feed) {
|
||||
deleteFeed();
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.menu_notifications_disable) {
|
||||
FeedUtils.disableNotifications(this, feed);
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.menu_notifications_focus) {
|
||||
FeedUtils.enableFocusNotifications(this, feed);
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.menu_notifications_unread) {
|
||||
FeedUtils.enableUnreadNotifications(this, feed);
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.menu_instafetch_feed) {
|
||||
FeedUtils.instaFetchFeed(this, feed.feedId);
|
||||
this.finish();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
if (!feed.active) {
|
||||
// there is currently no way for a feed to be un-muted while in this activity, so
|
||||
// don't bother creating the menu, which contains no valid options for a muted feed
|
||||
return false;
|
||||
}
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.feed_itemslist, menu);
|
||||
if (feed.isNotifyUnread()) {
|
||||
menu.findItem(R.id.menu_notifications_disable).setChecked(false);
|
||||
menu.findItem(R.id.menu_notifications_unread).setChecked(true);
|
||||
menu.findItem(R.id.menu_notifications_focus).setChecked(false);
|
||||
} else if (feed.isNotifyFocus()) {
|
||||
menu.findItem(R.id.menu_notifications_disable).setChecked(false);
|
||||
menu.findItem(R.id.menu_notifications_unread).setChecked(false);
|
||||
menu.findItem(R.id.menu_notifications_focus).setChecked(true);
|
||||
} else {
|
||||
menu.findItem(R.id.menu_notifications_disable).setChecked(true);
|
||||
menu.findItem(R.id.menu_notifications_unread).setChecked(false);
|
||||
menu.findItem(R.id.menu_notifications_focus).setChecked(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
if (feed.isNotifyUnread()) {
|
||||
menu.findItem(R.id.menu_notifications_disable).setChecked(false);
|
||||
menu.findItem(R.id.menu_notifications_unread).setChecked(true);
|
||||
menu.findItem(R.id.menu_notifications_focus).setChecked(false);
|
||||
} else if (feed.isNotifyFocus()) {
|
||||
menu.findItem(R.id.menu_notifications_disable).setChecked(false);
|
||||
menu.findItem(R.id.menu_notifications_unread).setChecked(false);
|
||||
menu.findItem(R.id.menu_notifications_focus).setChecked(true);
|
||||
} else {
|
||||
menu.findItem(R.id.menu_notifications_disable).setChecked(true);
|
||||
menu.findItem(R.id.menu_notifications_unread).setChecked(false);
|
||||
menu.findItem(R.id.menu_notifications_focus).setChecked(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -79,11 +131,4 @@ public class FeedItemsList extends ItemsList {
|
|||
return PrefsUtils.getReadFilterForFeed(this, feed.feedId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void defaultFeedViewChanged(DefaultFeedView value) {
|
||||
PrefsUtils.setDefaultFeedViewForFeed(this, feed.feedId, value);
|
||||
if (itemListFragment != null) {
|
||||
itemListFragment.setDefaultFeedView(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ public class FeedReading extends Reading {
|
|||
|
||||
UIUtils.setCustomActionBar(this, feed.faviconUrl, feed.title);
|
||||
|
||||
readingAdapter = new FeedReadingAdapter(fragmentManager, feed, classifier, defaultFeedView);
|
||||
readingAdapter = new FeedReadingAdapter(fragmentManager, feed, classifier);
|
||||
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import android.view.MenuInflater;
|
|||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.fragment.FolderItemListFragment;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.ReadFilter;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
@ -52,11 +51,4 @@ public class FolderItemsList extends ItemsList {
|
|||
return PrefsUtils.getReadFilterForFolder(this, folderName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void defaultFeedViewChanged(DefaultFeedView value) {
|
||||
PrefsUtils.setDefaultFeedViewForFolder(this, folderName, value);
|
||||
if (itemListFragment != null) {
|
||||
itemListFragment.setDefaultFeedView(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ public class FolderReading extends Reading {
|
|||
|
||||
UIUtils.setCustomActionBar(this, R.drawable.g_icn_folder_rss, fs.getFolderName());
|
||||
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), defaultFeedView, null);
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), null);
|
||||
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
|
|
@ -7,9 +7,6 @@ import android.view.MenuInflater;
|
|||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.fragment.GlobalSharedStoriesItemListFragment;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
import com.newsblur.util.PrefConstants;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.ReadFilter;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
||||
|
@ -38,14 +35,6 @@ public class GlobalSharedStoriesItemsList extends ItemsList {
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void defaultFeedViewChanged(DefaultFeedView value) {
|
||||
PrefsUtils.setDefaultFeedViewForFolder(this, PrefConstants.GLOBAL_SHARED_STORIES_FOLDER_NAME, value);
|
||||
if (itemListFragment != null) {
|
||||
itemListFragment.setDefaultFeedView(value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateReadFilterPreference(ReadFilter newValue) {
|
||||
// Not supported for global shared stories
|
||||
|
|
|
@ -14,7 +14,7 @@ public class GlobalSharedStoriesReading extends Reading {
|
|||
super.onCreate(savedInstanceBundle);
|
||||
|
||||
UIUtils.setCustomActionBar(this, R.drawable.ak_icon_global, getResources().getString(R.string.global_shared_stories));
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), defaultFeedView, null);
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), null);
|
||||
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
|
|
@ -34,6 +34,8 @@ public class InitActivity extends Activity {
|
|||
}
|
||||
}.execute();
|
||||
|
||||
com.newsblur.util.Log.i(this, "cold launching version " + PrefsUtils.getVersion(this));
|
||||
|
||||
}
|
||||
|
||||
private void start() {
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.app.FragmentManager;
|
|||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnKeyListener;
|
||||
|
@ -17,18 +18,14 @@ import butterknife.ButterKnife;
|
|||
import butterknife.Bind;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.fragment.DefaultFeedViewDialogFragment;
|
||||
import com.newsblur.fragment.ItemListFragment;
|
||||
import com.newsblur.fragment.ReadFilterDialogFragment;
|
||||
import com.newsblur.fragment.StoryOrderDialogFragment;
|
||||
import com.newsblur.fragment.TextSizeDialogFragment;
|
||||
import com.newsblur.service.NBSyncService;
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
import com.newsblur.util.DefaultFeedViewChangedListener;
|
||||
import com.newsblur.util.FeedSet;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.MarkAllReadConfirmation;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.ReadFilter;
|
||||
import com.newsblur.util.ReadFilterChangedListener;
|
||||
|
@ -37,7 +34,7 @@ import com.newsblur.util.StoryOrder;
|
|||
import com.newsblur.util.StoryOrderChangedListener;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
||||
public abstract class ItemsList extends NbActivity implements StoryOrderChangedListener, ReadFilterChangedListener, DefaultFeedViewChangedListener, OnSeekBarChangeListener {
|
||||
public abstract class ItemsList extends NbActivity implements StoryOrderChangedListener, ReadFilterChangedListener, OnSeekBarChangeListener {
|
||||
|
||||
public static final String EXTRA_FEED_SET = "feed_set";
|
||||
|
||||
|
@ -121,6 +118,10 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
|
|||
super.onResume();
|
||||
if (NBSyncService.isHousekeepingRunning()) finish();
|
||||
updateStatusIndicators();
|
||||
// this is not strictly necessary, since our first refresh with the fs will swap in
|
||||
// the correct session, but that can be delayed by sync backup, so we try here to
|
||||
// reduce UI lag, or in case somehow we got redisplayed in a zero-story state
|
||||
FeedUtils.prepareReadingSession(fs);
|
||||
// Reading activities almost certainly changed the read/unread state of some stories. Ensure
|
||||
// we reflect those changes promptly.
|
||||
itemListFragment.hasUpdated();
|
||||
|
@ -132,6 +133,15 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
|
|||
NBSyncService.addRecountCandidates(fs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
if (fs.isFilterSaved()) {
|
||||
menu.findItem(R.id.menu_mark_all_as_read).setVisible(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
|
@ -149,11 +159,6 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
|
|||
ReadFilter currentValue = getReadFilter();
|
||||
ReadFilterDialogFragment readFilter = ReadFilterDialogFragment.newInstance(currentValue);
|
||||
readFilter.show(getFragmentManager(), READ_FILTER);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_default_view) {
|
||||
DefaultFeedView currentValue = PrefsUtils.getDefaultFeedView(this, fs);
|
||||
DefaultFeedViewDialogFragment readFilter = DefaultFeedViewDialogFragment.newInstance(currentValue);
|
||||
readFilter.show(getFragmentManager(), DEFAULT_FEED_VIEW);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_textsize) {
|
||||
TextSizeDialogFragment textSize = TextSizeDialogFragment.newInstance(PrefsUtils.getListTextSize(this), TextSizeDialogFragment.TextSizeType.ListText);
|
||||
|
@ -165,6 +170,7 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
|
|||
searchQueryInput.requestFocus();
|
||||
} else {
|
||||
searchQueryInput.setVisibility(View.GONE);
|
||||
checkSearchQuery();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,13 +8,17 @@ import android.app.DialogFragment;
|
|||
import android.app.FragmentManager;
|
||||
import android.net.Uri;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.View.OnKeyListener;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.SeekBar;
|
||||
|
@ -34,8 +38,6 @@ import com.newsblur.fragment.TextSizeDialogFragment;
|
|||
import com.newsblur.service.BootReceiver;
|
||||
import com.newsblur.service.NBSyncService;
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.util.FeedSet;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.StateFilter;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
@ -43,6 +45,8 @@ import com.newsblur.view.StateToggleButton.StateChangedListener;
|
|||
|
||||
public class Main extends NbActivity implements StateChangedListener, SwipeRefreshLayout.OnRefreshListener, AbsListView.OnScrollListener, PopupMenu.OnMenuItemClickListener, OnSeekBarChangeListener {
|
||||
|
||||
public static final String EXTRA_FORCE_SHOW_FEED_ID = "force_show_feed_id";
|
||||
|
||||
private FolderListFragment folderFeedList;
|
||||
private FragmentManager fragmentManager;
|
||||
private boolean isLightTheme;
|
||||
|
@ -56,6 +60,7 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
@Bind(R.id.main_user_name) TextView userName;
|
||||
@Bind(R.id.main_unread_count_neut_text) TextView unreadCountNeutText;
|
||||
@Bind(R.id.main_unread_count_posi_text) TextView unreadCountPosiText;
|
||||
@Bind(R.id.feedlist_search_query) EditText searchQueryInput;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
|
@ -63,8 +68,6 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
|
||||
isLightTheme = PrefsUtils.isLightThemeSelected(this);
|
||||
|
||||
requestWindowFeature(Window.FEATURE_PROGRESS);
|
||||
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
|
||||
super.onCreate(savedInstanceState);
|
||||
getWindow().setBackgroundDrawableResource(android.R.color.transparent);
|
||||
|
||||
|
@ -96,17 +99,63 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
userImage.setImageBitmap(userPicture);
|
||||
}
|
||||
userName.setText(PrefsUtils.getUserDetails(this).username);
|
||||
searchQueryInput.setOnKeyListener(new OnKeyListener() {
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if ((keyCode == KeyEvent.KEYCODE_BACK) && (event.getAction() == KeyEvent.ACTION_DOWN)) {
|
||||
searchQueryInput.setVisibility(View.GONE);
|
||||
searchQueryInput.setText("");
|
||||
checkSearchQuery();
|
||||
return true;
|
||||
}
|
||||
if ((keyCode == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) {
|
||||
checkSearchQuery();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
searchQueryInput.addTextChangedListener(new TextWatcher() {
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
checkSearchQuery();
|
||||
}
|
||||
public void afterTextChanged(Editable s) {}
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
try {
|
||||
// due to weird backstack operations coming from notified reading activities,
|
||||
// sometimes we fail to reload. do everything in our power to log
|
||||
super.onResume();
|
||||
} catch (Exception e) {
|
||||
com.newsblur.util.Log.e(getClass().getName(), "error resuming Main", e);
|
||||
finish();
|
||||
}
|
||||
|
||||
String forceShowFeedId = getIntent().getStringExtra(EXTRA_FORCE_SHOW_FEED_ID);
|
||||
if (forceShowFeedId != null) {
|
||||
folderFeedList.forceShowFeed(forceShowFeedId);
|
||||
}
|
||||
|
||||
if (folderFeedList.getSearchQuery() != null) {
|
||||
searchQueryInput.setText(folderFeedList.getSearchQuery());
|
||||
searchQueryInput.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// triggerSync() might not actually do enough to push a UI update if background sync has been
|
||||
// behaving itself. because the system will re-use the activity, at least one update on resume
|
||||
// will be required, however inefficient
|
||||
folderFeedList.hasUpdated();
|
||||
|
||||
// immediately clear the story session to prevent bleed-over into the next
|
||||
FeedUtils.clearStorySession();
|
||||
// also queue a clear right before the feedset switches, so no in-flight stoires bleed
|
||||
NBSyncService.resetReadingSession();
|
||||
|
||||
NBSyncService.flushRecounts();
|
||||
|
||||
updateStatusIndicators();
|
||||
|
@ -121,6 +170,14 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
|
||||
@Override
|
||||
public void changedState(StateFilter state) {
|
||||
if ( !( (state == StateFilter.ALL) ||
|
||||
(state == StateFilter.SOME) ||
|
||||
(state == StateFilter.BEST) ) ) {
|
||||
searchQueryInput.setText("");
|
||||
searchQueryInput.setVisibility(View.GONE);
|
||||
checkSearchQuery();
|
||||
}
|
||||
|
||||
folderFeedList.changeState(state);
|
||||
}
|
||||
|
||||
|
@ -147,7 +204,6 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
public void updateUnreadCounts(int neutCount, int posiCount) {
|
||||
unreadCountNeutText.setText(Integer.toString(neutCount));
|
||||
unreadCountPosiText.setText(Integer.toString(posiCount));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -202,6 +258,7 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
public void onRefresh() {
|
||||
NBSyncService.forceFeedsFolders();
|
||||
triggerSync();
|
||||
folderFeedList.clearRecents();
|
||||
}
|
||||
|
||||
@OnClick(R.id.main_menu_button) void onClickMenuButton() {
|
||||
|
@ -223,20 +280,38 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
feedbackItem.setVisible(false);
|
||||
}
|
||||
|
||||
if ( (folderFeedList.currentState == StateFilter.ALL) ||
|
||||
(folderFeedList.currentState == StateFilter.SOME) ||
|
||||
(folderFeedList.currentState == StateFilter.BEST) ) {
|
||||
menu.findItem(R.id.menu_search_feeds).setVisible(true);
|
||||
} else {
|
||||
menu.findItem(R.id.menu_search_feeds).setVisible(false);
|
||||
}
|
||||
|
||||
if (PrefsUtils.isLightThemeSelected(this)) {
|
||||
menu.findItem(R.id.menu_theme_light).setChecked(true);
|
||||
} else {
|
||||
menu.findItem(R.id.menu_theme_dark).setChecked(true);
|
||||
}
|
||||
|
||||
pm.setOnMenuItemClickListener(this);
|
||||
pm.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
if (item.getItemId() == R.id.menu_profile) {
|
||||
Intent i = new Intent(this, Profile.class);
|
||||
startActivity(i);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_refresh) {
|
||||
NBSyncService.forceFeedsFolders();
|
||||
triggerSync();
|
||||
if (item.getItemId() == R.id.menu_refresh) {
|
||||
onRefresh();
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_search_feeds) {
|
||||
if (searchQueryInput.getVisibility() != View.VISIBLE) {
|
||||
searchQueryInput.setVisibility(View.VISIBLE);
|
||||
searchQueryInput.requestFocus();
|
||||
} else {
|
||||
searchQueryInput.setText("");
|
||||
searchQueryInput.setVisibility(View.GONE);
|
||||
checkSearchQuery();
|
||||
}
|
||||
} else if (item.getItemId() == R.id.menu_add_feed) {
|
||||
Intent i = new Intent(this, SearchForFeeds.class);
|
||||
startActivity(i);
|
||||
|
@ -268,6 +343,12 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
DialogFragment newFragment = new LoginAsDialogFragment();
|
||||
newFragment.show(getFragmentManager(), "dialog");
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_theme_light) {
|
||||
PrefsUtils.setLightThemeSelected(this, true);
|
||||
UIUtils.restartActivity(this);
|
||||
} else if (item.getItemId() == R.id.menu_theme_dark) {
|
||||
PrefsUtils.setLightThemeSelected(this, false);
|
||||
UIUtils.restartActivity(this);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -311,6 +392,14 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
if (folderFeedList != null) folderFeedList.setTextSize(size);
|
||||
}
|
||||
|
||||
private void checkSearchQuery() {
|
||||
String q = searchQueryInput.getText().toString().trim();
|
||||
if (q.length() < 1) {
|
||||
q = null;
|
||||
}
|
||||
folderFeedList.setSearchQuery(q);
|
||||
}
|
||||
|
||||
// unused OnSeekBarChangeListener method
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
|
|
|
@ -40,6 +40,7 @@ public class NbActivity extends Activity {
|
|||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
com.newsblur.util.Log.offerContext(this);
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onCreate");
|
||||
|
||||
PrefsUtils.applyThemePreference(this);
|
||||
|
|
|
@ -7,9 +7,6 @@ import android.view.MenuInflater;
|
|||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.fragment.ReadStoriesItemListFragment;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
import com.newsblur.util.PrefConstants;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.ReadFilter;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
||||
|
@ -38,14 +35,6 @@ public class ReadStoriesItemsList extends ItemsList {
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void defaultFeedViewChanged(DefaultFeedView value) {
|
||||
PrefsUtils.setDefaultFeedViewForFolder(this, PrefConstants.READ_STORIES_FOLDER_NAME, value);
|
||||
if (itemListFragment != null) {
|
||||
itemListFragment.setDefaultFeedView(value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateReadFilterPreference(ReadFilter newValue) {
|
||||
// dummy method. read stories don't have an order option
|
||||
|
|
|
@ -15,7 +15,7 @@ public class ReadStoriesReading extends Reading {
|
|||
super.onCreate(savedInstanceBundle);
|
||||
|
||||
UIUtils.setCustomActionBar(this, R.drawable.g_icn_unread, getResources().getString(R.string.read_stories_title));
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), defaultFeedView, null);
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), null);
|
||||
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import com.newsblur.R;
|
|||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.fragment.ReadingItemFragment;
|
||||
import com.newsblur.fragment.ShareDialogFragment;
|
||||
import com.newsblur.fragment.ReadingFontDialogFragment;
|
||||
import com.newsblur.fragment.TextSizeDialogFragment;
|
||||
import com.newsblur.service.NBSyncService;
|
||||
import com.newsblur.util.AppConstants;
|
||||
|
@ -43,15 +44,15 @@ import com.newsblur.util.FeedSet;
|
|||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.ReadFilter;
|
||||
import com.newsblur.util.ReadingFontChangedListener;
|
||||
import com.newsblur.util.StoryOrder;
|
||||
import com.newsblur.util.StateFilter;
|
||||
import com.newsblur.util.ThemeUtils;
|
||||
import com.newsblur.util.UIUtils;
|
||||
import com.newsblur.util.ViewUtils;
|
||||
import com.newsblur.util.VolumeKeyNavigation;
|
||||
import com.newsblur.view.ReadingScrollView.ScrollChangeListener;
|
||||
|
||||
public abstract class Reading extends NbActivity implements OnPageChangeListener, OnSeekBarChangeListener, ScrollChangeListener, LoaderManager.LoaderCallbacks<Cursor> {
|
||||
public abstract class Reading extends NbActivity implements OnPageChangeListener, OnSeekBarChangeListener, ScrollChangeListener, LoaderManager.LoaderCallbacks<Cursor>, ReadingFontChangedListener {
|
||||
|
||||
public static final String EXTRA_FEEDSET = "feed_set";
|
||||
public static final String EXTRA_POSITION = "feed_position";
|
||||
|
@ -72,8 +73,6 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
private static final int OVERLAY_MIN_WIDTH_DP = 355;
|
||||
|
||||
protected StateFilter intelState;
|
||||
protected StoryOrder storyOrder;
|
||||
protected ReadFilter readFilter;
|
||||
|
||||
// Activities navigate to a particular story by hash.
|
||||
// We can find it once we have the cursor.
|
||||
|
@ -109,11 +108,9 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
private int lastVScrollPos = 0;
|
||||
|
||||
private boolean unreadSearchActive = false;
|
||||
private boolean unreadSearchStarted = false;
|
||||
|
||||
private List<Story> pageHistory;
|
||||
|
||||
protected DefaultFeedView defaultFeedView;
|
||||
private VolumeKeyNavigation volumeKeyNavigation;
|
||||
|
||||
@Override
|
||||
|
@ -127,6 +124,12 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
|
||||
fs = (FeedSet)getIntent().getSerializableExtra(EXTRA_FEEDSET);
|
||||
|
||||
if (fs == null) {
|
||||
com.newsblur.util.Log.w(this.getClass().getName(), "reading view had no FeedSet");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((savedInstanceBundle != null) && savedInstanceBundle.containsKey(BUNDLE_STARTING_UNREAD)) {
|
||||
startingUnreadCount = savedInstanceBundle.getInt(BUNDLE_STARTING_UNREAD);
|
||||
}
|
||||
|
@ -140,16 +143,8 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
}
|
||||
|
||||
intelState = PrefsUtils.getStateFilter(this);
|
||||
storyOrder = PrefsUtils.getStoryOrder(this, fs);
|
||||
readFilter = PrefsUtils.getReadFilter(this, fs);
|
||||
volumeKeyNavigation = PrefsUtils.getVolumeKeyNavigation(this);
|
||||
|
||||
if ((savedInstanceBundle != null) && savedInstanceBundle.containsKey(BUNDLE_SELECTED_FEED_VIEW)) {
|
||||
defaultFeedView = (DefaultFeedView)savedInstanceBundle.getSerializable(BUNDLE_SELECTED_FEED_VIEW);
|
||||
} else {
|
||||
defaultFeedView = PrefsUtils.getDefaultFeedView(this, fs);
|
||||
}
|
||||
|
||||
// were we fullscreen before rotation?
|
||||
if ((savedInstanceBundle != null) && savedInstanceBundle.containsKey(BUNDLE_IS_FULLSCREEN)) {
|
||||
boolean isFullscreen = savedInstanceBundle.getBoolean(BUNDLE_IS_FULLSCREEN, false);
|
||||
|
@ -195,11 +190,6 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
outState.putInt(BUNDLE_STARTING_UNREAD, startingUnreadCount);
|
||||
}
|
||||
|
||||
ReadingItemFragment item = getReadingFragment();
|
||||
if (item != null) {
|
||||
outState.putSerializable(BUNDLE_SELECTED_FEED_VIEW, item.getSelectedFeedView());
|
||||
}
|
||||
|
||||
if (ViewUtils.isSystemUIHidden(getWindow().getDecorView())) {
|
||||
outState.putBoolean(BUNDLE_IS_FULLSCREEN, true);
|
||||
}
|
||||
|
@ -214,6 +204,10 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
// onCreate() in our subclass should have called createLoader(), but sometimes the callback never makes it.
|
||||
// this ensures that at least one callback happens after activity re-create.
|
||||
getLoaderManager().restartLoader(0, null, this);
|
||||
// this is not strictly necessary, since our first refresh with the fs will swap in
|
||||
// the correct session, but that can be delayed by sync backup, so we try here to
|
||||
// reduce UI lag, or in case somehow we got redisplayed in a zero-story state
|
||||
FeedUtils.prepareReadingSession(fs);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -243,31 +237,51 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
synchronized (STORIES_MUTEX) {
|
||||
if (cursor == null) return;
|
||||
|
||||
//NB: this implicitly calls readingAdapter.notifyDataSetChanged();
|
||||
readingAdapter.swapCursor(cursor);
|
||||
stories = cursor;
|
||||
|
||||
// if this is the first time we've found a cursor, we know the onCreate chain is done
|
||||
if (this.pager == null) {
|
||||
setupPager();
|
||||
}
|
||||
|
||||
try {
|
||||
readingAdapter.notifyDataSetChanged();
|
||||
} catch (IllegalStateException ise) {
|
||||
// sometimes the pager is already shutting down by the time the callback finishes
|
||||
finish();
|
||||
if (! NBSyncService.isFeedSetReady(fs)) {
|
||||
// not only is the session table stale, our fragment is being re-used by the system
|
||||
// to show a totally different feedset. trash anything that might have stale story
|
||||
// data and let them get recreated when a good cursor comes in
|
||||
com.newsblur.util.Log.i(this.getClass().getName(), "stale load");
|
||||
pager.setVisibility(View.INVISIBLE);
|
||||
stories = null;
|
||||
triggerRefresh(AppConstants.READING_STORY_PRELOAD);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean lastCursorWasStale = (stories == null);
|
||||
|
||||
stories = cursor;
|
||||
|
||||
// if the pager previously showed a stale set of stories, it is *not* sufficent to just
|
||||
// swap out the cursor and invalidate. no number of calls to notifyDataSetChanged() or
|
||||
// setCurrentItem() will ever get a pager to refresh the currently displayed fragment.
|
||||
// however, the pager can be tricked into wiping all fragments and recreating them from
|
||||
// the adapter by setting the adapter again, even if it is the same one.
|
||||
if (lastCursorWasStale) {
|
||||
pager.setAdapter(readingAdapter);
|
||||
}
|
||||
|
||||
com.newsblur.util.Log.d(this.getClass().getName(), "loaded cursor with count: " + cursor.getCount());
|
||||
if (cursor.getCount() < 1) {
|
||||
triggerRefresh(AppConstants.READING_STORY_PRELOAD);
|
||||
}
|
||||
|
||||
// see if we are just starting and need to jump to a target story
|
||||
skipPagerToStoryHash();
|
||||
|
||||
|
||||
if (unreadSearchActive) {
|
||||
// if we left this flag high, we were looking for an unread, but didn't find one;
|
||||
// now that we have more stories, look again.
|
||||
nextUnread();
|
||||
}
|
||||
checkStoryCount(pager.getCurrentItem());
|
||||
updateOverlayNav();
|
||||
updateOverlayText();
|
||||
}
|
||||
|
@ -283,11 +297,13 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
if ( (story.storyHash.equals(storyHash)) ||
|
||||
((storyHash.equals(FIND_FIRST_UNREAD)) && (!story.read))
|
||||
) {
|
||||
// see above note about re-setting the adapter to force the pager to reload fragments
|
||||
pager.setAdapter(readingAdapter);
|
||||
pager.setCurrentItem(stories.getPosition(), false);
|
||||
this.onPageSelected(stories.getPosition());
|
||||
// now that the pager is getting the right story, make it visible
|
||||
pager.setVisibility(View.VISIBLE);
|
||||
emptyViewText.setVisibility(View.INVISIBLE);
|
||||
pager.setCurrentItem(stories.getPosition(), false);
|
||||
this.onPageSelected(stories.getPosition());
|
||||
storyHash = null;
|
||||
return;
|
||||
}
|
||||
|
@ -384,7 +400,11 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
TextSizeDialogFragment textSize = TextSizeDialogFragment.newInstance(PrefsUtils.getTextSize(this), TextSizeDialogFragment.TextSizeType.ReadingText);
|
||||
textSize.show(getFragmentManager(), TextSizeDialogFragment.class.getName());
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_reading_save) {
|
||||
} else if (item.getItemId() == R.id.menu_font) {
|
||||
ReadingFontDialogFragment storyFont = ReadingFontDialogFragment.newInstance(PrefsUtils.getFontString(this));
|
||||
storyFont.show(getFragmentManager(), ReadingFontDialogFragment.class.getName());
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_reading_save) {
|
||||
if (story.starred) {
|
||||
FeedUtils.setStorySaved(story, false, Reading.this);
|
||||
} else {
|
||||
|
@ -550,7 +570,11 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
}
|
||||
this.overlayLeft.setEnabled(this.getLastReadPosition(false) != -1);
|
||||
this.overlayRight.setText((currentUnreadCount > 0) ? R.string.overlay_next : R.string.overlay_done);
|
||||
this.overlayRight.setBackgroundResource((currentUnreadCount > 0) ? ThemeUtils.getSelectorOverlayBackgroundRight(this) : ThemeUtils.getSelectorOverlayBackgroundRightDone(this));
|
||||
if (currentUnreadCount > 0) {
|
||||
this.overlayRight.setBackgroundResource(UIUtils.getThemedResource(this, R.attr.selectorOverlayBackgroundRight, android.R.attr.background));
|
||||
} else {
|
||||
this.overlayRight.setBackgroundResource(UIUtils.getThemedResource(this, R.attr.selectorOverlayBackgroundRightDone, android.R.attr.background));
|
||||
}
|
||||
|
||||
if (this.startingUnreadCount == 0 ) {
|
||||
// sessions with no unreads just show a full progress bar
|
||||
|
@ -572,11 +596,11 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
public void run() {
|
||||
ReadingItemFragment item = getReadingFragment();
|
||||
if (item == null) return;
|
||||
if (item.getSelectedFeedView() == DefaultFeedView.STORY) {
|
||||
overlayText.setBackgroundResource(ThemeUtils.getSelectorOverlayBackgroundText(Reading.this));
|
||||
if (item.getSelectedViewMode() == DefaultFeedView.STORY) {
|
||||
overlayText.setBackgroundResource(UIUtils.getThemedResource(Reading.this, R.attr.selectorOverlayBackgroundText, android.R.attr.background));
|
||||
overlayText.setText(R.string.overlay_text);
|
||||
} else {
|
||||
overlayText.setBackgroundResource(ThemeUtils.getSelectorOverlayBackgroundStory(Reading.this));
|
||||
overlayText.setBackgroundResource(UIUtils.getThemedResource(Reading.this, R.attr.selectorOverlayBackgroundStory, android.R.attr.background));
|
||||
overlayText.setText(R.string.overlay_story);
|
||||
}
|
||||
}
|
||||
|
@ -671,6 +695,12 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readingFontChanged(String newValue) {
|
||||
PrefsUtils.setFontString(this, newValue);
|
||||
sendBroadcast(new Intent(ReadingItemFragment.READING_FONT_CHANGED));
|
||||
}
|
||||
|
||||
/**
|
||||
* Click handler for the righthand overlay nav button.
|
||||
*/
|
||||
|
@ -680,6 +710,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
Intent i = new Intent(this, Main.class);
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
startActivity(i);
|
||||
finish();
|
||||
} else {
|
||||
// if there are unreads, go to the next one
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
|
@ -697,7 +728,6 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
*/
|
||||
private void nextUnread() {
|
||||
unreadSearchActive = true;
|
||||
unreadSearchStarted = true;
|
||||
|
||||
// if we somehow got tapped before construction or are running during destruction, stop and
|
||||
// let either finish. search will happen when the cursor is pushed.
|
||||
|
@ -814,10 +844,15 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
}
|
||||
|
||||
public void overlayText(View v) {
|
||||
ReadingItemFragment item = getReadingFragment();
|
||||
final ReadingItemFragment item = getReadingFragment();
|
||||
if (item == null) return;
|
||||
item.switchSelectedFeedView();
|
||||
updateOverlayText();
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
item.switchSelectedViewMode();
|
||||
return null;
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
private ReadingItemFragment getReadingFragment() {
|
||||
|
@ -825,6 +860,21 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
return readingAdapter.getExistingItem(pager.getCurrentItem());
|
||||
}
|
||||
|
||||
public FeedSet getFeedSet() {
|
||||
return this.fs;
|
||||
}
|
||||
|
||||
public void viewModeChanged() {
|
||||
ReadingItemFragment frag = readingAdapter.getExistingItem(pager.getCurrentItem());
|
||||
frag.viewModeChanged();
|
||||
// fragments to the left or the right may have already preloaded content and need to also switch
|
||||
frag = readingAdapter.getExistingItem(pager.getCurrentItem()-1);
|
||||
if (frag != null) frag.viewModeChanged();
|
||||
frag = readingAdapter.getExistingItem(pager.getCurrentItem()+1);
|
||||
if (frag != null) frag.viewModeChanged();
|
||||
updateOverlayText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (isVolumeKeyNavigationEvent(keyCode)) {
|
||||
|
|
|
@ -4,28 +4,24 @@ import android.database.Cursor;
|
|||
import android.app.Fragment;
|
||||
import android.app.FragmentManager;
|
||||
import android.support.v13.app.FragmentStatePagerAdapter;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.fragment.LoadingFragment;
|
||||
import com.newsblur.fragment.ReadingItemFragment;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public abstract class ReadingAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
protected Cursor stories;
|
||||
protected DefaultFeedView defaultFeedView;
|
||||
protected String sourceUserId;
|
||||
private SparseArray<WeakReference<ReadingItemFragment>> cachedFragments;
|
||||
|
||||
public ReadingAdapter(FragmentManager fm, DefaultFeedView defaultFeedView, String sourceUserId) {
|
||||
public ReadingAdapter(FragmentManager fm, String sourceUserId) {
|
||||
super(fm);
|
||||
this.cachedFragments = new SparseArray<WeakReference<ReadingItemFragment>>();
|
||||
this.defaultFeedView = defaultFeedView;
|
||||
this.sourceUserId = sourceUserId;
|
||||
}
|
||||
|
||||
|
@ -36,7 +32,6 @@ public abstract class ReadingAdapter extends FragmentStatePagerAdapter {
|
|||
} else {
|
||||
stories.moveToPosition(position);
|
||||
Story story = Story.fromCursor(stories);
|
||||
String tag = this.getClass().getName() + story.storyHash;
|
||||
ReadingItemFragment frag = getReadingItemFragment(story);
|
||||
return frag;
|
||||
}
|
||||
|
@ -117,7 +112,7 @@ public abstract class ReadingAdapter extends FragmentStatePagerAdapter {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void notifyDataSetChanged() {
|
||||
public synchronized void notifyDataSetChanged() {
|
||||
super.notifyDataSetChanged();
|
||||
|
||||
// go one step further than the default pageradapter and also refresh the
|
||||
|
|
|
@ -7,7 +7,6 @@ import android.view.MenuInflater;
|
|||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.fragment.SavedStoriesItemListFragment;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
import com.newsblur.util.PrefConstants;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.ReadFilter;
|
||||
|
@ -42,14 +41,6 @@ public class SavedStoriesItemsList extends ItemsList {
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void defaultFeedViewChanged(DefaultFeedView value) {
|
||||
PrefsUtils.setDefaultFeedViewForFolder(this, PrefConstants.SAVED_STORIES_FOLDER_NAME, value);
|
||||
if (itemListFragment != null) {
|
||||
itemListFragment.setDefaultFeedView(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: the following two methods are required by our parent spec but are not
|
||||
// relevant since saved stories have no read/unread status.
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ public class SavedStoriesReading extends Reading {
|
|||
title = title + " - " + fs.getSingleSavedTag();
|
||||
}
|
||||
UIUtils.setCustomActionBar(this, R.drawable.clock, title);
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), defaultFeedView, null);
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), null);
|
||||
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import android.content.Loader;
|
|||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.Window;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
|
@ -28,7 +27,10 @@ import com.newsblur.fragment.AddFeedFragment;
|
|||
import com.newsblur.network.SearchAsyncTaskLoader;
|
||||
import com.newsblur.network.SearchLoaderResponse;
|
||||
|
||||
public class SearchForFeeds extends NbActivity implements LoaderCallbacks<SearchLoaderResponse>, OnItemClickListener {
|
||||
// TODO: this activity's use of the inbuilt activity search facility as well as an improper use of a loader to
|
||||
// make network requests makes it easily lose state, lack non-legacy progress indication, and generally
|
||||
// buggy. a normal layout and a proper use of sync for search results should be implemented.
|
||||
public class SearchForFeeds extends NbActivity implements LoaderCallbacks<SearchLoaderResponse>, OnItemClickListener, AddFeedFragment.AddFeedProgressListener {
|
||||
|
||||
private static final Set<String> SUPPORTED_URL_PROTOCOLS = new HashSet<String>();
|
||||
static {
|
||||
|
@ -42,8 +44,6 @@ public class SearchForFeeds extends NbActivity implements LoaderCallbacks<Search
|
|||
|
||||
@Override
|
||||
protected void onCreate(Bundle arg0) {
|
||||
requestWindowFeature(Window.FEATURE_PROGRESS);
|
||||
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
|
||||
super.onCreate(arg0);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
|
@ -80,8 +80,6 @@ public class SearchForFeeds extends NbActivity implements LoaderCallbacks<Search
|
|||
|
||||
// test to see if a feed URL was passed rather than a search term
|
||||
if (tryAddByURL(query)) { return; }
|
||||
|
||||
setProgressBarIndeterminateVisibility(true);
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(SearchAsyncTaskLoader.SEARCH_TERM, query);
|
||||
|
@ -131,7 +129,6 @@ public class SearchForFeeds extends NbActivity implements LoaderCallbacks<Search
|
|||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<SearchLoaderResponse> loader, SearchLoaderResponse results) {
|
||||
setProgressBarIndeterminateVisibility(false);
|
||||
if(!results.hasError()) {
|
||||
adapter = new FeedSearchResultAdapter(this, 0, 0, results.getResults());
|
||||
resultsList.setAdapter(adapter);
|
||||
|
@ -153,4 +150,13 @@ public class SearchForFeeds extends NbActivity implements LoaderCallbacks<Search
|
|||
addFeedFragment.show(getFragmentManager(), "dialog");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addFeedStarted() {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
// TODO: this UI should offer some progress indication, since the add API call can block for several seconds
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import android.view.MenuInflater;
|
|||
import com.newsblur.R;
|
||||
import com.newsblur.domain.SocialFeed;
|
||||
import com.newsblur.fragment.SocialFeedItemListFragment;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.ReadFilter;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
@ -54,11 +53,4 @@ public class SocialFeedItemsList extends ItemsList {
|
|||
return PrefsUtils.getReadFilterForFeed(this, socialFeed.userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void defaultFeedViewChanged(DefaultFeedView value) {
|
||||
PrefsUtils.setDefaultFeedViewForFeed(this, socialFeed.userId, value);
|
||||
if (itemListFragment != null) {
|
||||
itemListFragment.setDefaultFeedView(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ public class SocialFeedReading extends Reading {
|
|||
SocialFeed socialFeed = FeedUtils.dbHelper.getSocialFeed(fs.getSingleSocialFeed().getKey());
|
||||
if (socialFeed == null) finish(); // don't open fatally stale intents
|
||||
UIUtils.setCustomActionBar(this, socialFeed.photoUrl, socialFeed.feedTitle);
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), defaultFeedView, socialFeed.userId);
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), socialFeed.userId);
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import com.newsblur.domain.SocialFeed;
|
|||
import com.newsblur.domain.StarredCount;
|
||||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.domain.UserProfile;
|
||||
import com.newsblur.network.domain.CommentResponse;
|
||||
import com.newsblur.network.domain.StoriesResponse;
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.util.FeedSet;
|
||||
|
@ -200,6 +201,12 @@ public class BlurDatabaseHelper {
|
|||
return result;
|
||||
}
|
||||
|
||||
public void updateFeed(Feed feed) {
|
||||
synchronized (RW_MUTEX) {
|
||||
dbRW.insertWithOnConflict(DatabaseConstants.FEED_TABLE, null, feed.getValues(), SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
}
|
||||
|
||||
private void bulkInsertValues(String table, List<ContentValues> valuesList) {
|
||||
if (valuesList.size() < 1) return;
|
||||
synchronized (RW_MUTEX) {
|
||||
|
@ -248,32 +255,6 @@ public class BlurDatabaseHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public void setStarredCounts(List<ContentValues> values) {
|
||||
synchronized (RW_MUTEX) {
|
||||
dbRW.beginTransaction();
|
||||
try {
|
||||
dbRW.delete(DatabaseConstants.STARREDCOUNTS_TABLE, null, null);
|
||||
bulkInsertValuesExtSync(DatabaseConstants.STARREDCOUNTS_TABLE, values);
|
||||
dbRW.setTransactionSuccessful();
|
||||
} finally {
|
||||
dbRW.endTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getStoryHashesForFeed(String feedId) {
|
||||
String q = "SELECT " + DatabaseConstants.STORY_HASH +
|
||||
" FROM " + DatabaseConstants.STORY_TABLE +
|
||||
" WHERE " + DatabaseConstants.STORY_FEED_ID + " = ?";
|
||||
Cursor c = dbRO.rawQuery(q, new String[]{feedId});
|
||||
List<String> hashes = new ArrayList<String>(c.getCount());
|
||||
while (c.moveToNext()) {
|
||||
hashes.add(c.getString(c.getColumnIndexOrThrow(DatabaseConstants.STORY_HASH)));
|
||||
}
|
||||
c.close();
|
||||
return hashes;
|
||||
}
|
||||
|
||||
// note method name: this gets a set rather than a list, in case the caller wants to
|
||||
// spend the up-front cost of hashing for better lookup speed rather than iteration!
|
||||
public Set<String> getUnreadStoryHashesAsSet() {
|
||||
|
@ -346,32 +327,30 @@ public class BlurDatabaseHelper {
|
|||
}
|
||||
|
||||
// handle story content
|
||||
List<ContentValues> socialStoryValues = new ArrayList<ContentValues>();
|
||||
for (Story story : apiResponse.stories) {
|
||||
// pick a thumbnail for the story
|
||||
story.thumbnailUrl = Story.guessStoryThumbnailURL(story);
|
||||
// insert the story data
|
||||
ContentValues values = story.getValues();
|
||||
dbRW.insertWithOnConflict(DatabaseConstants.STORY_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
// if a story was shared by a user, also insert it into the social table under their userid, too
|
||||
for (String sharedUserId : story.sharedUserIds) {
|
||||
ContentValues socialValues = new ContentValues();
|
||||
socialValues.put(DatabaseConstants.SOCIALFEED_STORY_USER_ID, sharedUserId);
|
||||
socialValues.put(DatabaseConstants.SOCIALFEED_STORY_STORYID, values.getAsString(DatabaseConstants.STORY_ID));
|
||||
socialStoryValues.add(socialValues);
|
||||
if (apiResponse.stories != null) {
|
||||
storiesloop: for (Story story : apiResponse.stories) {
|
||||
if ((story.storyHash == null) || (story.storyHash.length() < 1)) {
|
||||
// this is incredibly rare, but has been seen in crash reports at least twice.
|
||||
com.newsblur.util.Log.e(this, "story received without story hash: " + story.id);
|
||||
continue storiesloop;
|
||||
}
|
||||
insertSingleStoryExtSync(story);
|
||||
// if the story is being fetched for the immediate session, also add the hash to the session table
|
||||
if (forImmediateReading && story.isStoryVisibileInState(intelState)) {
|
||||
ContentValues sessionHashValues = new ContentValues();
|
||||
sessionHashValues.put(DatabaseConstants.READING_SESSION_STORY_HASH, story.storyHash);
|
||||
dbRW.insert(DatabaseConstants.READING_SESSION_TABLE, null, sessionHashValues);
|
||||
}
|
||||
impliedFeedId = story.feedId;
|
||||
}
|
||||
// if the story is being fetched for the immediate session, also add the hash to the session table
|
||||
if (forImmediateReading && story.isStoryVisibileInState(intelState)) {
|
||||
ContentValues sessionHashValues = new ContentValues();
|
||||
sessionHashValues.put(DatabaseConstants.READING_SESSION_STORY_HASH, story.storyHash);
|
||||
dbRW.insert(DatabaseConstants.READING_SESSION_TABLE, null, sessionHashValues);
|
||||
}
|
||||
impliedFeedId = story.feedId;
|
||||
}
|
||||
if (socialStoryValues.size() > 0) {
|
||||
for(ContentValues values: socialStoryValues) {
|
||||
dbRW.insertWithOnConflict(DatabaseConstants.SOCIALFEED_STORY_MAP_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
if (apiResponse.story != null) {
|
||||
if ((apiResponse.story.storyHash == null) || (apiResponse.story.storyHash.length() < 1)) {
|
||||
com.newsblur.util.Log.e(this, "story received without story hash: " + apiResponse.story.id);
|
||||
return;
|
||||
}
|
||||
insertSingleStoryExtSync(apiResponse.story);
|
||||
impliedFeedId = apiResponse.story.feedId;
|
||||
}
|
||||
|
||||
// handle classifiers
|
||||
|
@ -391,61 +370,106 @@ public class BlurDatabaseHelper {
|
|||
}
|
||||
}
|
||||
|
||||
// handle comments
|
||||
List<ContentValues> commentValues = new ArrayList<ContentValues>();
|
||||
List<ContentValues> replyValues = new ArrayList<ContentValues>();
|
||||
// track which comments were seen, so replies can be cleared before re-insertion. there isn't
|
||||
// enough data to de-dupe them for an insert/update operation
|
||||
List<String> freshCommentIds = new ArrayList<String>();
|
||||
for (Story story : apiResponse.stories) {
|
||||
for (Comment comment : story.publicComments) {
|
||||
comment.storyId = story.id;
|
||||
// we need a primary key for comments, so construct one
|
||||
comment.id = Comment.constructId(story.id, story.feedId, comment.userId);
|
||||
commentValues.add(comment.getValues());
|
||||
for (Reply reply : comment.replies) {
|
||||
reply.commentId = comment.id;
|
||||
reply.id = reply.constructId();
|
||||
replyValues.add(reply.getValues());
|
||||
}
|
||||
freshCommentIds.add(comment.id);
|
||||
}
|
||||
for (Comment comment : story.friendsComments) {
|
||||
comment.storyId = story.id;
|
||||
// we need a primary key for comments, so construct one
|
||||
comment.id = Comment.constructId(story.id, story.feedId, comment.userId);
|
||||
comment.byFriend = true;
|
||||
commentValues.add(comment.getValues());
|
||||
for (Reply reply : comment.replies) {
|
||||
reply.commentId = comment.id;
|
||||
reply.id = reply.constructId();
|
||||
replyValues.add(reply.getValues());
|
||||
}
|
||||
freshCommentIds.add(comment.id);
|
||||
}
|
||||
for (Comment comment : story.friendsShares) {
|
||||
comment.isPseudo = true;
|
||||
comment.storyId = story.id;
|
||||
// we need a primary key for comments, so construct one
|
||||
comment.id = Comment.constructId(story.id, story.feedId, comment.userId);
|
||||
comment.byFriend = true;
|
||||
commentValues.add(comment.getValues());
|
||||
for (Reply reply : comment.replies) {
|
||||
reply.commentId = comment.id;
|
||||
reply.id = reply.constructId();
|
||||
replyValues.add(reply.getValues());
|
||||
}
|
||||
freshCommentIds.add(comment.id);
|
||||
}
|
||||
}
|
||||
// before inserting new replies, remove existing ones for the fetched comments
|
||||
// NB: attempting to do this with a "WHERE col IN (vector)" for speed can cause errors on some versions of sqlite
|
||||
for (String commentId : freshCommentIds) {
|
||||
dbRW.delete(DatabaseConstants.REPLY_TABLE, DatabaseConstants.REPLY_COMMENTID + " = ?", new String[]{commentId});
|
||||
}
|
||||
bulkInsertValuesExtSync(DatabaseConstants.COMMENT_TABLE, commentValues);
|
||||
bulkInsertValuesExtSync(DatabaseConstants.REPLY_TABLE, replyValues);
|
||||
dbRW.setTransactionSuccessful();
|
||||
} finally {
|
||||
dbRW.endTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void insertSingleStoryExtSync(Story story) {
|
||||
// pick a thumbnail for the story
|
||||
story.thumbnailUrl = Story.guessStoryThumbnailURL(story);
|
||||
// insert the story data
|
||||
ContentValues values = story.getValues();
|
||||
dbRW.insertWithOnConflict(DatabaseConstants.STORY_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
// if a story was shared by a user, also insert it into the social table under their userid, too
|
||||
for (String sharedUserId : story.sharedUserIds) {
|
||||
ContentValues socialValues = new ContentValues();
|
||||
socialValues.put(DatabaseConstants.SOCIALFEED_STORY_USER_ID, sharedUserId);
|
||||
socialValues.put(DatabaseConstants.SOCIALFEED_STORY_STORYID, values.getAsString(DatabaseConstants.STORY_ID));
|
||||
dbRW.insertWithOnConflict(DatabaseConstants.SOCIALFEED_STORY_MAP_TABLE, null, socialValues, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
// handle comments
|
||||
for (Comment comment : story.publicComments) {
|
||||
comment.storyId = story.id;
|
||||
insertSingleCommentExtSync(comment);
|
||||
}
|
||||
for (Comment comment : story.friendsComments) {
|
||||
comment.storyId = story.id;
|
||||
comment.byFriend = true;
|
||||
insertSingleCommentExtSync(comment);
|
||||
}
|
||||
for (Comment comment : story.friendsShares) {
|
||||
comment.isPseudo = true;
|
||||
comment.storyId = story.id;
|
||||
comment.byFriend = true;
|
||||
insertSingleCommentExtSync(comment);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertSingleCommentExtSync(Comment comment) {
|
||||
// real comments replace placeholders
|
||||
int count = dbRW.delete(DatabaseConstants.COMMENT_TABLE, DatabaseConstants.COMMENT_ISPLACEHOLDER + " = ?", new String[]{"true"});
|
||||
// comments always come with an updated set of replies, so remove old ones first
|
||||
dbRW.delete(DatabaseConstants.REPLY_TABLE, DatabaseConstants.REPLY_COMMENTID + " = ?", new String[]{comment.id});
|
||||
dbRW.insertWithOnConflict(DatabaseConstants.COMMENT_TABLE, null, comment.getValues(), SQLiteDatabase.CONFLICT_REPLACE);
|
||||
for (Reply reply : comment.replies) {
|
||||
reply.commentId = comment.id;
|
||||
dbRW.insertWithOnConflict(DatabaseConstants.REPLY_TABLE, null, reply.getValues(), SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing story based upon a new copy received from a social API. This handles the fact
|
||||
* that some social APIs helpfully vend updated copies of stories with social-related fields updated
|
||||
* to reflect a social action, but that the new copy is missing some fields. Attempt to merge the
|
||||
* new story with the old one.
|
||||
*/
|
||||
public void updateStory(StoriesResponse apiResponse, boolean forImmediateReading) {
|
||||
if (apiResponse.story == null) {
|
||||
com.newsblur.util.Log.e(this, "updateStory called on response with missing single story");
|
||||
return;
|
||||
}
|
||||
Cursor c = dbRO.query(DatabaseConstants.STORY_TABLE,
|
||||
null,
|
||||
DatabaseConstants.STORY_HASH + " = ?",
|
||||
new String[]{apiResponse.story.storyHash},
|
||||
null, null, null);
|
||||
if (c.getCount() < 1) {
|
||||
com.newsblur.util.Log.w(this, "updateStory can't find old copy; new story may be missing fields.");
|
||||
} else {
|
||||
Story oldStory = Story.fromCursor(c);
|
||||
c.close();
|
||||
apiResponse.story.starred = oldStory.starred;
|
||||
apiResponse.story.starredTimestamp = oldStory.starredTimestamp;
|
||||
apiResponse.story.read = oldStory.read;
|
||||
}
|
||||
insertStories(apiResponse, forImmediateReading);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing comment and associated replies based upon a new copy received from a social
|
||||
* API. Most social APIs vend an updated view that replaces any old or placeholder records.
|
||||
*/
|
||||
public void updateComment(CommentResponse apiResponse, String storyId) {
|
||||
synchronized (RW_MUTEX) {
|
||||
// comments often contain enclosed replies, so batch them.
|
||||
dbRW.beginTransaction();
|
||||
try {
|
||||
// the API might include new supplemental user metadata if new replies have shown up.
|
||||
if (apiResponse.users != null) {
|
||||
List<ContentValues> userValues = new ArrayList<ContentValues>(apiResponse.users.length);
|
||||
for (UserProfile user : apiResponse.users) {
|
||||
userValues.add(user.getValues());
|
||||
}
|
||||
bulkInsertValuesExtSync(DatabaseConstants.USER_TABLE, userValues);
|
||||
}
|
||||
|
||||
// we store all comments in the context of the associated story, but the social API doesn't
|
||||
// reference the story when responding, so fix that from our context
|
||||
apiResponse.comment.storyId = storyId;
|
||||
insertSingleCommentExtSync(apiResponse.comment);
|
||||
dbRW.setTransactionSuccessful();
|
||||
} finally {
|
||||
dbRW.endTransaction();
|
||||
|
@ -540,6 +564,29 @@ public class BlurDatabaseHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public void setFeedFetchPending(String feedId) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(DatabaseConstants.FEED_FETCH_PENDING, true);
|
||||
synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId});}
|
||||
}
|
||||
|
||||
public boolean isFeedSetFetchPending(FeedSet fs) {
|
||||
if (fs.getSingleFeed() != null) {
|
||||
String feedId = fs.getSingleFeed();
|
||||
Cursor c = dbRO.query(DatabaseConstants.FEED_TABLE,
|
||||
new String[]{DatabaseConstants.FEED_FETCH_PENDING},
|
||||
DatabaseConstants.FEED_ID + " = ? AND " + DatabaseConstants.FEED_FETCH_PENDING + " = ?",
|
||||
new String[]{feedId, "1"},
|
||||
null, null, null);
|
||||
try {
|
||||
if (c.getCount() > 0) return true;
|
||||
} finally {
|
||||
closeQuietly(c);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a story (un)read but does not adjust counts. Must stay idempotent an time-insensitive.
|
||||
*/
|
||||
|
@ -678,7 +725,7 @@ public class BlurDatabaseHelper {
|
|||
return getFeedsUnreadCount(stateFilter, selection.toString(), null);
|
||||
} else if (fs.getMultipleSocialFeeds() != null) {
|
||||
StringBuilder selection = new StringBuilder(DatabaseConstants.SOCIAL_FEED_ID + " IN ( ");
|
||||
selection.append(TextUtils.join(",", fs.getMultipleFeeds())).append(")");
|
||||
selection.append(TextUtils.join(",", fs.getMultipleSocialFeeds().keySet())).append(")");
|
||||
return getSocialFeedsUnreadCount(stateFilter, selection.toString(), null);
|
||||
} else if (fs.getSingleFeed() != null) {
|
||||
return getFeedsUnreadCount(stateFilter, DatabaseConstants.FEED_ID + " = ?", new String[]{fs.getSingleFeed()});
|
||||
|
@ -852,7 +899,7 @@ public class BlurDatabaseHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public void setStoryShared(String hash) {
|
||||
public void setStoryShared(String hash, boolean shared) {
|
||||
// get a fresh copy of the story from the DB so we can append to the shared ID set
|
||||
Cursor c = dbRO.query(DatabaseConstants.STORY_TABLE,
|
||||
new String[]{DatabaseConstants.STORY_SHARED_USER_IDS},
|
||||
|
@ -868,12 +915,16 @@ public class BlurDatabaseHelper {
|
|||
String[] sharedUserIds = TextUtils.split(c.getString(c.getColumnIndex(DatabaseConstants.STORY_SHARED_USER_IDS)), ",");
|
||||
closeQuietly(c);
|
||||
|
||||
// the new id to append to the shared list (the current user)
|
||||
// the id to append to or remove from the shared list (the current user)
|
||||
String currentUser = PrefsUtils.getUserDetails(context).id;
|
||||
|
||||
// append to set and update DB
|
||||
Set<String> newIds = new HashSet<String>(Arrays.asList(sharedUserIds));
|
||||
newIds.add(currentUser);
|
||||
if (shared) {
|
||||
newIds.add(currentUser);
|
||||
} else {
|
||||
newIds.remove(currentUser);
|
||||
}
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(DatabaseConstants.STORY_SHARED_USER_IDS, TextUtils.join(",", newIds));
|
||||
synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash});}
|
||||
|
@ -918,14 +969,14 @@ public class BlurDatabaseHelper {
|
|||
synchronized (RW_MUTEX) {dbRW.insertOrThrow(DatabaseConstants.STORY_TEXT_TABLE, null, values);}
|
||||
}
|
||||
|
||||
public Loader<Cursor> getSocialFeedsLoader(final StateFilter stateFilter) {
|
||||
public Loader<Cursor> getSocialFeedsLoader() {
|
||||
return new QueryCursorLoader(context) {
|
||||
protected Cursor createCursor() {return getSocialFeedsCursor(stateFilter, cancellationSignal);}
|
||||
protected Cursor createCursor() {return getSocialFeedsCursor(cancellationSignal);}
|
||||
};
|
||||
}
|
||||
|
||||
public Cursor getSocialFeedsCursor(StateFilter stateFilter, CancellationSignal cancellationSignal) {
|
||||
return query(false, DatabaseConstants.SOCIALFEED_TABLE, null, DatabaseConstants.getBlogSelectionFromState(stateFilter), null, null, null, "UPPER(" + DatabaseConstants.SOCIAL_FEED_TITLE + ") ASC", null, cancellationSignal);
|
||||
public Cursor getSocialFeedsCursor(CancellationSignal cancellationSignal) {
|
||||
return query(false, DatabaseConstants.SOCIALFEED_TABLE, null, null, null, null, null, "UPPER(" + DatabaseConstants.SOCIAL_FEED_TITLE + ") ASC", null, cancellationSignal);
|
||||
}
|
||||
|
||||
public SocialFeed getSocialFeed(String feedId) {
|
||||
|
@ -958,14 +1009,14 @@ public class BlurDatabaseHelper {
|
|||
return query(false, DatabaseConstants.FOLDER_TABLE, null, null, null, null, null, null, null, cancellationSignal);
|
||||
}
|
||||
|
||||
public Loader<Cursor> getFeedsLoader(final StateFilter stateFilter) {
|
||||
public Loader<Cursor> getFeedsLoader() {
|
||||
return new QueryCursorLoader(context) {
|
||||
protected Cursor createCursor() {return getFeedsCursor(stateFilter, cancellationSignal);}
|
||||
protected Cursor createCursor() {return getFeedsCursor(cancellationSignal);}
|
||||
};
|
||||
}
|
||||
|
||||
public Cursor getFeedsCursor(StateFilter stateFilter, CancellationSignal cancellationSignal) {
|
||||
return query(false, DatabaseConstants.FEED_TABLE, null, DatabaseConstants.getFeedSelectionFromState(stateFilter), null, null, null, "UPPER(" + DatabaseConstants.FEED_TITLE + ") ASC", null, cancellationSignal);
|
||||
public Cursor getFeedsCursor(CancellationSignal cancellationSignal) {
|
||||
return query(false, DatabaseConstants.FEED_TABLE, null, null, null, null, null, "UPPER(" + DatabaseConstants.FEED_TITLE + ") ASC", null, cancellationSignal);
|
||||
}
|
||||
|
||||
public Loader<Cursor> getSavedStoryCountsLoader() {
|
||||
|
@ -1033,6 +1084,8 @@ public class BlurDatabaseHelper {
|
|||
|
||||
if (fs.isAllRead()) {
|
||||
q.append(" ORDER BY " + DatabaseConstants.READ_STORY_ORDER);
|
||||
} else if (fs.isGlobalShared()) {
|
||||
q.append(" ORDER BY " + DatabaseConstants.SHARED_STORY_ORDER);
|
||||
} else if (fs.isAllSaved()) {
|
||||
q.append(" ORDER BY " + DatabaseConstants.getSavedStoriesSortOrder(order));
|
||||
} else {
|
||||
|
@ -1191,12 +1244,16 @@ public class BlurDatabaseHelper {
|
|||
return comment;
|
||||
}
|
||||
|
||||
public void insertUpdateComment(String storyId, String feedId, String commentText) {
|
||||
// we can only insert comments as the currently logged-in user
|
||||
/**
|
||||
* Insert brand new comment for which we do not yet have a server-assigned ID. This comment
|
||||
* will show up in the UI with reduced functionality until the server gets back to us with
|
||||
* an ID at which time the placeholder will be removed.
|
||||
*/
|
||||
public void insertCommentPlaceholder(String storyId, String feedId, String commentText) {
|
||||
String userId = PrefsUtils.getUserDetails(context).id;
|
||||
|
||||
Comment comment = new Comment();
|
||||
comment.id = Comment.constructId(storyId, feedId, userId);
|
||||
comment.isPlaceholder = true;
|
||||
comment.id = Comment.PLACEHOLDER_COMMENT_ID + storyId + userId;
|
||||
comment.storyId = storyId;
|
||||
comment.userId = userId;
|
||||
comment.commentText = commentText;
|
||||
|
@ -1204,31 +1261,55 @@ public class BlurDatabaseHelper {
|
|||
if (TextUtils.isEmpty(commentText)) {
|
||||
comment.isPseudo = true;
|
||||
}
|
||||
synchronized (RW_MUTEX) {dbRW.insertWithOnConflict(DatabaseConstants.COMMENT_TABLE, null, comment.getValues(), SQLiteDatabase.CONFLICT_REPLACE);}
|
||||
synchronized (RW_MUTEX) {
|
||||
// in order to make this method idempotent (so it can be attempted before, during, or after
|
||||
// the real comment is done, we have to check for a real one
|
||||
if (getComment(storyId, userId) != null) {
|
||||
com.newsblur.util.Log.w(this.getClass().getName(), "failing to insert placeholder comment over live one");
|
||||
return;
|
||||
}
|
||||
dbRW.insertWithOnConflict(DatabaseConstants.COMMENT_TABLE, null, comment.getValues(), SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
}
|
||||
|
||||
public void editReply(String replyId, String replyText) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(DatabaseConstants.REPLY_TEXT, replyText);
|
||||
synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.REPLY_TABLE, values, DatabaseConstants.REPLY_ID + " = ?", new String[]{replyId});}
|
||||
}
|
||||
|
||||
public void deleteReply(String replyId) {
|
||||
synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.REPLY_TABLE, DatabaseConstants.REPLY_ID + " = ?", new String[]{replyId});}
|
||||
}
|
||||
|
||||
public void clearSelfComments(String storyId) {
|
||||
String userId = PrefsUtils.getUserDetails(context).id;
|
||||
synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.COMMENT_TABLE,
|
||||
DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?",
|
||||
new String[]{storyId, userId});}
|
||||
}
|
||||
|
||||
public void setCommentLiked(String storyId, String userId, String feedId, boolean liked) {
|
||||
String commentKey = Comment.constructId(storyId, feedId, userId);
|
||||
// get a fresh copy of the story from the DB so we can append to the shared ID set
|
||||
Cursor c = dbRO.query(DatabaseConstants.COMMENT_TABLE,
|
||||
new String[]{DatabaseConstants.COMMENT_LIKING_USERS},
|
||||
DatabaseConstants.COMMENT_ID + " = ?",
|
||||
new String[]{commentKey},
|
||||
null,
|
||||
DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?",
|
||||
new String[]{storyId, userId},
|
||||
null, null, null);
|
||||
if ((c == null)||(c.getCount() < 1)) {
|
||||
Log.w(this.getClass().getName(), "story removed before finishing mark-shared");
|
||||
Log.w(this.getClass().getName(), "comment removed before finishing mark-liked");
|
||||
closeQuietly(c);
|
||||
return;
|
||||
}
|
||||
c.moveToFirst();
|
||||
String[] likingUserIds = TextUtils.split(c.getString(c.getColumnIndex(DatabaseConstants.COMMENT_LIKING_USERS)), ",");
|
||||
Comment comment = Comment.fromCursor(c);
|
||||
closeQuietly(c);
|
||||
|
||||
// the new id to append/remove from the liking list (the current user)
|
||||
String currentUser = PrefsUtils.getUserDetails(context).id;
|
||||
|
||||
// append to set and update DB
|
||||
Set<String> newIds = new HashSet<String>(Arrays.asList(likingUserIds));
|
||||
Set<String> newIds = new HashSet<String>(Arrays.asList(comment.likingUsers));
|
||||
if (liked) {
|
||||
newIds.add(currentUser);
|
||||
} else {
|
||||
|
@ -1236,7 +1317,7 @@ public class BlurDatabaseHelper {
|
|||
}
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(DatabaseConstants.COMMENT_LIKING_USERS, TextUtils.join(",", newIds));
|
||||
synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.COMMENT_TABLE, values, DatabaseConstants.COMMENT_ID + " = ?", new String[]{commentKey});}
|
||||
synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.COMMENT_TABLE, values, DatabaseConstants.COMMENT_ID + " = ?", new String[]{comment.id});}
|
||||
}
|
||||
|
||||
public UserProfile getUserProfile(String userId) {
|
||||
|
@ -1260,13 +1341,28 @@ public class BlurDatabaseHelper {
|
|||
return replies;
|
||||
}
|
||||
|
||||
public void replyToComment(String storyId, String feedId, String commentUserId, String replyText, long replyCreateTime) {
|
||||
public void insertReplyPlaceholder(String storyId, String feedId, String commentUserId, String replyText) {
|
||||
// get a fresh copy of the comment so we can discover the ID
|
||||
Cursor c = dbRO.query(DatabaseConstants.COMMENT_TABLE,
|
||||
null,
|
||||
DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?",
|
||||
new String[]{storyId, commentUserId},
|
||||
null, null, null);
|
||||
if ((c == null)||(c.getCount() < 1)) {
|
||||
com.newsblur.util.Log.w(this, "comment removed before reply could be processed");
|
||||
closeQuietly(c);
|
||||
return;
|
||||
}
|
||||
c.moveToFirst();
|
||||
Comment comment = Comment.fromCursor(c);
|
||||
closeQuietly(c);
|
||||
|
||||
Reply reply = new Reply();
|
||||
reply.commentId = Comment.constructId(storyId, feedId, commentUserId);
|
||||
reply.commentId = comment.id;
|
||||
reply.text = replyText;
|
||||
reply.userId = PrefsUtils.getUserDetails(context).id;
|
||||
reply.date = new Date(replyCreateTime);
|
||||
reply.id = reply.constructId();
|
||||
reply.date = new Date();
|
||||
reply.id = Reply.PLACEHOLDER_COMMENT_ID + storyId + comment.id + reply.userId;
|
||||
synchronized (RW_MUTEX) {dbRW.insertWithOnConflict(DatabaseConstants.REPLY_TABLE, null, reply.getValues(), SQLiteDatabase.CONFLICT_REPLACE);}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ public class DatabaseConstants {
|
|||
public static final String FEED_NEGATIVE_COUNT = "ng";
|
||||
public static final String FEED_NOTIFICATION_TYPES = "notification_types";
|
||||
public static final String FEED_NOTIFICATION_FILTER = "notification_filter";
|
||||
public static final String FEED_FETCH_PENDING = "fetch_pending";
|
||||
|
||||
public static final String SOCIALFEED_TABLE = "social_feeds";
|
||||
public static final String SOCIAL_FEED_ID = BaseColumns._ID;
|
||||
|
@ -118,6 +119,7 @@ public class DatabaseConstants {
|
|||
public static final String COMMENT_BYFRIEND = "comment_byfriend";
|
||||
public static final String COMMENT_USERID = "comment_userid";
|
||||
public static final String COMMENT_ISPSEUDO = "comment_ispseudo";
|
||||
public static final String COMMENT_ISPLACEHOLDER = "comment_isplaceholder";
|
||||
|
||||
public static final String REPLY_TABLE = "comment_replies";
|
||||
public static final String REPLY_ID = BaseColumns._ID;
|
||||
|
@ -126,6 +128,7 @@ public class DatabaseConstants {
|
|||
public static final String REPLY_USERID = "reply_userid";
|
||||
public static final String REPLY_DATE = "reply_date";
|
||||
public static final String REPLY_SHORTDATE = "reply_shortdate";
|
||||
public static final String REPLY_ISPLACEHOLDER = "reply_isplaceholder";
|
||||
|
||||
public static final String ACTION_TABLE = "story_actions";
|
||||
public static final String ACTION_ID = BaseColumns._ID;
|
||||
|
@ -133,6 +136,7 @@ public class DatabaseConstants {
|
|||
public static final String ACTION_TRIED = "tried";
|
||||
public static final String ACTION_TYPE = "action_type";
|
||||
public static final String ACTION_COMMENT_TEXT = "comment_text";
|
||||
public static final String ACTION_REPLY_ID = "reply_id";
|
||||
public static final String ACTION_STORY_HASH = "story_hash";
|
||||
public static final String ACTION_FEED_ID = "feed_id";
|
||||
public static final String ACTION_MODIFIED_FEED_IDS = "modified_feed_ids";
|
||||
|
@ -141,6 +145,8 @@ public class DatabaseConstants {
|
|||
public static final String ACTION_STORY_ID = "story_id";
|
||||
public static final String ACTION_SOURCE_USER_ID = "source_user_id";
|
||||
public static final String ACTION_COMMENT_ID = "comment_id";
|
||||
public static final String ACTION_NOTIFY_FILTER = "notify_filter";
|
||||
public static final String ACTION_NOTIFY_TYPES = "notify_types";
|
||||
|
||||
public static final String STARREDCOUNTS_TABLE = "starred_counts";
|
||||
public static final String STARREDCOUNTS_COUNT = "count";
|
||||
|
@ -176,7 +182,8 @@ public class DatabaseConstants {
|
|||
FEED_TITLE + TEXT + ", " +
|
||||
FEED_UPDATED_SECONDS + INTEGER + ", " +
|
||||
FEED_NOTIFICATION_TYPES + TEXT + ", " +
|
||||
FEED_NOTIFICATION_FILTER + TEXT +
|
||||
FEED_NOTIFICATION_FILTER + TEXT + ", " +
|
||||
FEED_FETCH_PENDING + TEXT +
|
||||
")";
|
||||
|
||||
static final String USER_SQL = "CREATE TABLE " + USER_TABLE + " (" +
|
||||
|
@ -206,7 +213,8 @@ public class DatabaseConstants {
|
|||
COMMENT_STORYID + TEXT + ", " +
|
||||
COMMENT_TEXT + TEXT + ", " +
|
||||
COMMENT_USERID + TEXT + ", " +
|
||||
COMMENT_ISPSEUDO + TEXT +
|
||||
COMMENT_ISPSEUDO + TEXT + ", " +
|
||||
COMMENT_ISPLACEHOLDER + TEXT +
|
||||
")";
|
||||
|
||||
static final String REPLY_SQL = "CREATE TABLE " + REPLY_TABLE + " (" +
|
||||
|
@ -215,7 +223,8 @@ public class DatabaseConstants {
|
|||
REPLY_ID + TEXT + " PRIMARY KEY, " +
|
||||
REPLY_COMMENTID + TEXT + ", " +
|
||||
REPLY_TEXT + TEXT + ", " +
|
||||
REPLY_USERID + TEXT +
|
||||
REPLY_USERID + TEXT + ", " +
|
||||
REPLY_ISPLACEHOLDER + TEXT +
|
||||
")";
|
||||
|
||||
static final String STORY_SQL = "CREATE TABLE " + STORY_TABLE + " (" +
|
||||
|
@ -284,7 +293,10 @@ public class DatabaseConstants {
|
|||
ACTION_STORY_ID + TEXT + ", " +
|
||||
ACTION_SOURCE_USER_ID + TEXT + ", " +
|
||||
ACTION_COMMENT_ID + TEXT + ", " +
|
||||
ACTION_MODIFIED_FEED_IDS + TEXT +
|
||||
ACTION_REPLY_ID + TEXT + ", " +
|
||||
ACTION_MODIFIED_FEED_IDS + TEXT + ", " +
|
||||
ACTION_NOTIFY_FILTER + TEXT + ", " +
|
||||
ACTION_NOTIFY_TYPES + TEXT +
|
||||
")";
|
||||
|
||||
static final String STARREDCOUNTS_SQL = "CREATE TABLE " + STARREDCOUNTS_TABLE + " (" +
|
||||
|
@ -348,6 +360,8 @@ public class DatabaseConstants {
|
|||
|
||||
public static final String READ_STORY_ORDER = STORY_LAST_READ_DATE + " DESC";
|
||||
|
||||
public static final String SHARED_STORY_ORDER = STORY_SHARED_DATE + " DESC";
|
||||
|
||||
/**
|
||||
* Appends to the given story query any and all selection statements that are required to satisfy the specified
|
||||
* filtration parameters.
|
||||
|
@ -390,42 +404,6 @@ public class DatabaseConstants {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selection args to filter feeds.
|
||||
*/
|
||||
public static String getFeedSelectionFromState(StateFilter state) {
|
||||
switch (state) {
|
||||
case ALL:
|
||||
return null; // don't filter
|
||||
case SOME:
|
||||
return FEED_ACTIVE + " = '1' AND ((" + FEED_NEUTRAL_COUNT + " + " + FEED_POSITIVE_COUNT + ") > 0)";
|
||||
case BEST:
|
||||
return FEED_ACTIVE + " = '1' AND (" + FEED_POSITIVE_COUNT + " > 0)";
|
||||
case SAVED:
|
||||
return FEED_ACTIVE + " = '1'"; // due to API structure, we can't filter for saveds, so the caller will have to sort that out
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selection args to filter social feeds.
|
||||
*/
|
||||
public static String getBlogSelectionFromState(StateFilter state) {
|
||||
switch (state) {
|
||||
case ALL:
|
||||
return null;
|
||||
case SOME:
|
||||
return "((" + SOCIAL_FEED_NEUTRAL_COUNT + " + " + SOCIAL_FEED_POSITIVE_COUNT + ") > 0)";
|
||||
case BEST:
|
||||
return "(" + SOCIAL_FEED_POSITIVE_COUNT + " > 0)";
|
||||
case SAVED:
|
||||
return "0";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getStorySortOrder(StoryOrder storyOrder) {
|
||||
// it is not uncommon for a feed to have multiple stories with exactly the same timestamp. we
|
||||
// arbitrarily pick a second sort column so sortation is stable.
|
||||
|
|
|
@ -7,23 +7,22 @@ import com.newsblur.domain.Classifier;
|
|||
import com.newsblur.domain.Feed;
|
||||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.fragment.ReadingItemFragment;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
|
||||
public class FeedReadingAdapter extends ReadingAdapter {
|
||||
|
||||
private final Feed feed;
|
||||
private Classifier classifier;
|
||||
|
||||
public FeedReadingAdapter(FragmentManager fm, Feed feed, Classifier classifier, DefaultFeedView defaultFeedView) {
|
||||
public FeedReadingAdapter(FragmentManager fm, Feed feed, Classifier classifier) {
|
||||
// sourceUserId not required for feed reading
|
||||
super(fm, defaultFeedView, null);
|
||||
super(fm, null);
|
||||
this.feed = feed;
|
||||
this.classifier = classifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized ReadingItemFragment getReadingItemFragment(Story story) {
|
||||
return ReadingItemFragment.newInstance(story, feed.title, feed.faviconColor, feed.faviconFade, feed.faviconBorder, feed.faviconText, feed.faviconUrl, classifier, false, defaultFeedView, sourceUserId);
|
||||
return ReadingItemFragment.newInstance(story, feed.title, feed.faviconColor, feed.faviconFade, feed.faviconBorder, feed.faviconText, feed.faviconUrl, classifier, false, sourceUserId);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.view.ViewGroup;
|
|||
import android.widget.BaseExpandableListAdapter;
|
||||
import android.widget.ExpandableListView;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.newsblur.R;
|
||||
|
@ -49,10 +50,13 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
private final static float defaultTextSize_groupName = 13;
|
||||
private final static float defaultTextSize_count = 14;
|
||||
|
||||
/** Social feeds, indexed by feed ID. */
|
||||
private Map<String,SocialFeed> socialFeeds = Collections.emptyMap();
|
||||
private final static float NONZERO_UNREADS_ALPHA = 0.87f;
|
||||
private final static float ZERO_UNREADS_ALPHA = 0.70f;
|
||||
|
||||
/** Social feed in display order. */
|
||||
private List<SocialFeed> socialFeedsOrdered = Collections.emptyList();
|
||||
/** Active social feed in display order. */
|
||||
private List<SocialFeed> socialFeedsActive = Collections.emptyList();
|
||||
/** Total neutral unreads for all social feeds. */
|
||||
public int totalSocialNeutCount = 0;
|
||||
/** Total positive unreads for all social feeds. */
|
||||
|
@ -95,7 +99,6 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
/** Flat names of folders explicity closed by the user. */
|
||||
private Set<String> closedFolders = new HashSet<String>();
|
||||
|
||||
private Context context;
|
||||
private LayoutInflater inflater;
|
||||
private StateFilter currentState;
|
||||
|
||||
|
@ -106,8 +109,14 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
|
||||
private float textSize;
|
||||
|
||||
// in order to implement the laggy disappearance of marked-read feeds, preserve the ID of
|
||||
// the last feed or folder viewed and force the DB to include it in the selection
|
||||
public String lastFeedViewedId;
|
||||
public String lastFolderViewed;
|
||||
|
||||
public String activeSearchQuery;
|
||||
|
||||
public FolderListAdapter(Context context, StateFilter currentState) {
|
||||
this.context = context;
|
||||
this.currentState = currentState;
|
||||
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
|
||||
|
@ -206,11 +215,12 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
View v = convertView;
|
||||
if (groupPosition == ALL_SHARED_STORIES_GROUP_POSITION) {
|
||||
if (v == null) v = inflater.inflate(R.layout.row_socialfeed, parent, false);
|
||||
SocialFeed f = socialFeedsOrdered.get(childPosition);
|
||||
SocialFeed f = socialFeedsActive.get(childPosition);
|
||||
TextView nameView = ((TextView) v.findViewById(R.id.row_socialfeed_name));
|
||||
nameView.setText(f.feedTitle);
|
||||
nameView.setTextSize(textSize * defaultTextSize_childName);
|
||||
FeedUtils.iconLoader.displayImage(f.photoUrl, ((ImageView) v.findViewById(R.id.row_socialfeed_icon)), 0, false);
|
||||
ImageView iconView = (ImageView) v.findViewById(R.id.row_socialfeed_icon);
|
||||
FeedUtils.iconLoader.displayImage(f.photoUrl, iconView, 0, false);
|
||||
TextView neutCounter = ((TextView) v.findViewById(R.id.row_socialsumneu));
|
||||
if (f.neutralCount > 0 && currentState != StateFilter.BEST) {
|
||||
neutCounter.setVisibility(View.VISIBLE);
|
||||
|
@ -227,12 +237,20 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
}
|
||||
neutCounter.setTextSize(textSize * defaultTextSize_count);
|
||||
posCounter.setTextSize(textSize * defaultTextSize_count);
|
||||
if ((f.neutralCount <= 0) && (f.positiveCount <= 0)) {
|
||||
nameView.setAlpha(ZERO_UNREADS_ALPHA);
|
||||
iconView.setAlpha(ZERO_UNREADS_ALPHA);
|
||||
} else {
|
||||
nameView.setAlpha(NONZERO_UNREADS_ALPHA);
|
||||
iconView.setAlpha(NONZERO_UNREADS_ALPHA);
|
||||
}
|
||||
} else if (isRowSavedStories(groupPosition)) {
|
||||
if (v == null) v = inflater.inflate(R.layout.row_saved_tag, parent, false);
|
||||
StarredCount sc = starredCountsByTag.get(childPosition);
|
||||
TextView nameView =((TextView) v.findViewById(R.id.row_tag_name));
|
||||
nameView.setText(sc.tag);
|
||||
nameView.setTextSize(textSize * defaultTextSize_childName);
|
||||
nameView.setAlpha(NONZERO_UNREADS_ALPHA);
|
||||
TextView savedCounter =((TextView) v.findViewById(R.id.row_saved_tag_sum));
|
||||
savedCounter.setText(Integer.toString(checkNegativeUnreads(sc.count)));
|
||||
savedCounter.setTextSize(textSize * defaultTextSize_count);
|
||||
|
@ -242,28 +260,57 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
TextView nameView =((TextView) v.findViewById(R.id.row_feedname));
|
||||
nameView.setText(f.title);
|
||||
nameView.setTextSize(textSize * defaultTextSize_childName);
|
||||
FeedUtils.iconLoader.displayImage(f.faviconUrl, ((ImageView) v.findViewById(R.id.row_feedfavicon)), 0, false);
|
||||
ImageView iconView = (ImageView) v.findViewById(R.id.row_feedfavicon);
|
||||
FeedUtils.iconLoader.displayImage(f.faviconUrl, iconView, 0, false);
|
||||
TextView neutCounter = ((TextView) v.findViewById(R.id.row_feedneutral));
|
||||
TextView posCounter = ((TextView) v.findViewById(R.id.row_feedpositive));
|
||||
TextView savedCounter = ((TextView) v.findViewById(R.id.row_feedsaved));
|
||||
ImageView muteIcon = ((ImageView) v.findViewById(R.id.row_feedmuteicon));
|
||||
ProgressBar fetchingIcon = ((ProgressBar) v.findViewById(R.id.row_feedfetching));
|
||||
if (!f.active) {
|
||||
muteIcon.setVisibility(View.VISIBLE);
|
||||
neutCounter.setVisibility(View.GONE);
|
||||
posCounter.setVisibility(View.GONE);
|
||||
savedCounter.setVisibility(View.GONE);
|
||||
fetchingIcon.setVisibility(View.GONE);
|
||||
fetchingIcon.setProgress(100);
|
||||
nameView.setAlpha(ZERO_UNREADS_ALPHA);
|
||||
iconView.setAlpha(ZERO_UNREADS_ALPHA);
|
||||
} else if (f.fetchPending) {
|
||||
muteIcon.setVisibility(View.GONE);
|
||||
neutCounter.setVisibility(View.GONE);
|
||||
posCounter.setVisibility(View.GONE);
|
||||
savedCounter.setVisibility(View.GONE);
|
||||
fetchingIcon.setVisibility(View.VISIBLE);
|
||||
fetchingIcon.setProgress(0);
|
||||
nameView.setAlpha(NONZERO_UNREADS_ALPHA);
|
||||
iconView.setAlpha(NONZERO_UNREADS_ALPHA);
|
||||
} else if (currentState == StateFilter.SAVED) {
|
||||
muteIcon.setVisibility(View.GONE);
|
||||
neutCounter.setVisibility(View.GONE);
|
||||
posCounter.setVisibility(View.GONE);
|
||||
savedCounter.setVisibility(View.VISIBLE);
|
||||
savedCounter.setText(Integer.toString(zeroForNull(feedSavedCounts.get(f.feedId))));
|
||||
fetchingIcon.setVisibility(View.GONE);
|
||||
fetchingIcon.setProgress(100);
|
||||
nameView.setAlpha(NONZERO_UNREADS_ALPHA);
|
||||
iconView.setAlpha(NONZERO_UNREADS_ALPHA);
|
||||
} else if (currentState == StateFilter.BEST) {
|
||||
muteIcon.setVisibility(View.GONE);
|
||||
neutCounter.setVisibility(View.GONE);
|
||||
savedCounter.setVisibility(View.GONE);
|
||||
posCounter.setVisibility(View.VISIBLE);
|
||||
posCounter.setText(Integer.toString(checkNegativeUnreads(f.positiveCount)));
|
||||
fetchingIcon.setVisibility(View.GONE);
|
||||
fetchingIcon.setProgress(100);
|
||||
if (f.positiveCount <= 0) {
|
||||
posCounter.setVisibility(View.GONE);
|
||||
nameView.setAlpha(ZERO_UNREADS_ALPHA);
|
||||
iconView.setAlpha(ZERO_UNREADS_ALPHA);
|
||||
} else {
|
||||
posCounter.setText(Integer.toString(checkNegativeUnreads(f.positiveCount)));
|
||||
nameView.setAlpha(NONZERO_UNREADS_ALPHA);
|
||||
iconView.setAlpha(NONZERO_UNREADS_ALPHA);
|
||||
}
|
||||
} else {
|
||||
muteIcon.setVisibility(View.GONE);
|
||||
savedCounter.setVisibility(View.GONE);
|
||||
|
@ -279,6 +326,15 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
} else {
|
||||
posCounter.setVisibility(View.GONE);
|
||||
}
|
||||
fetchingIcon.setVisibility(View.GONE);
|
||||
fetchingIcon.setProgress(100);
|
||||
if ((f.neutralCount <= 0) && (f.positiveCount <= 0)) {
|
||||
nameView.setAlpha(ZERO_UNREADS_ALPHA);
|
||||
iconView.setAlpha(ZERO_UNREADS_ALPHA);
|
||||
} else {
|
||||
nameView.setAlpha(NONZERO_UNREADS_ALPHA);
|
||||
iconView.setAlpha(NONZERO_UNREADS_ALPHA);
|
||||
}
|
||||
}
|
||||
neutCounter.setTextSize(textSize * defaultTextSize_count);
|
||||
posCounter.setTextSize(textSize * defaultTextSize_count);
|
||||
|
@ -302,7 +358,6 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
return FeedSet.allSaved();
|
||||
} else {
|
||||
String folderName = getGroupFolderName(groupPosition);
|
||||
// TODO: technically we have the data this util method gives us, could we save a DB call?
|
||||
FeedSet fs = FeedUtils.feedSetFromFolderName(folderName);
|
||||
if (currentState == StateFilter.SAVED) fs.setFilterSaved(true);
|
||||
return fs;
|
||||
|
@ -355,7 +410,7 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
@Override
|
||||
public synchronized int getChildrenCount(int groupPosition) {
|
||||
if (groupPosition == ALL_SHARED_STORIES_GROUP_POSITION) {
|
||||
return socialFeedsOrdered.size();
|
||||
return socialFeedsActive.size();
|
||||
} else if (isRowSavedStories(groupPosition)) {
|
||||
return starredCountsByTag.size();
|
||||
} else if (isRowReadStories(groupPosition) || groupPosition == GLOBAL_SHARED_STORIES_GROUP_POSITION) {
|
||||
|
@ -368,7 +423,7 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
@Override
|
||||
public synchronized FeedSet getChild(int groupPosition, int childPosition) {
|
||||
if (groupPosition == ALL_SHARED_STORIES_GROUP_POSITION) {
|
||||
SocialFeed socialFeed = socialFeedsOrdered.get(childPosition);
|
||||
SocialFeed socialFeed = socialFeedsActive.get(childPosition);
|
||||
return FeedSet.singleSocialFeed(socialFeed.userId, socialFeed.username);
|
||||
} else if (isRowSavedStories(groupPosition)) {
|
||||
return FeedSet.singleSavedTag(starredCountsByTag.get(childPosition).tag);
|
||||
|
@ -431,17 +486,30 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
|
||||
public synchronized void setSocialFeedCursor(Cursor cursor) {
|
||||
if (!cursor.isBeforeFirst()) return;
|
||||
socialFeeds = new HashMap<String,SocialFeed>(cursor.getCount());
|
||||
socialFeedsOrdered = new ArrayList<SocialFeed>(cursor.getCount());
|
||||
totalSocialNeutCount = 0;
|
||||
totalSocialPosiCount = 0;
|
||||
while (cursor.moveToNext()) {
|
||||
SocialFeed f = SocialFeed.fromCursor(cursor);
|
||||
socialFeedsOrdered.add(f);
|
||||
socialFeeds.put(f.userId, f);
|
||||
}
|
||||
recountSocialFeeds();
|
||||
}
|
||||
|
||||
private void recountSocialFeeds() {
|
||||
socialFeedsActive = new ArrayList<SocialFeed>();
|
||||
totalSocialNeutCount = 0;
|
||||
totalSocialPosiCount = 0;
|
||||
for (SocialFeed f : socialFeedsOrdered) {
|
||||
totalSocialNeutCount += checkNegativeUnreads(f.neutralCount);
|
||||
totalSocialPosiCount += checkNegativeUnreads(f.positiveCount);
|
||||
if ( (currentState == StateFilter.ALL) ||
|
||||
((currentState == StateFilter.SOME) && (f.neutralCount > 0 || f.positiveCount > 0)) ||
|
||||
((currentState == StateFilter.BEST) && (f.positiveCount > 0)) ) {
|
||||
if ((activeSearchQuery == null) || (f.feedTitle.toLowerCase().indexOf(activeSearchQuery.toLowerCase()) >= 0)) {
|
||||
socialFeedsActive.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recountChildren();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
@ -531,11 +599,14 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
if ( (currentState == StateFilter.ALL) ||
|
||||
((currentState == StateFilter.SOME) && (feedNeutCounts.containsKey(feedId) || feedPosCounts.containsKey(feedId))) ||
|
||||
((currentState == StateFilter.BEST) && feedPosCounts.containsKey(feedId)) ||
|
||||
((currentState == StateFilter.SAVED) && feedSavedCounts.containsKey(feedId)) ) {
|
||||
activeFeeds.add(f);
|
||||
((currentState == StateFilter.SAVED) && feedSavedCounts.containsKey(feedId)) ||
|
||||
f.feedId.equals(lastFeedViewedId) ) {
|
||||
if ((activeSearchQuery == null) || (f.title.toLowerCase().indexOf(activeSearchQuery.toLowerCase()) >= 0)) {
|
||||
activeFeeds.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((activeFeeds.size() > 0) || (folderName.equals(AppConstants.ROOT_FOLDER))) {
|
||||
if ((activeFeeds.size() > 0) || (folderName.equals(AppConstants.ROOT_FOLDER)) || folder.name.equals(lastFolderViewed)) {
|
||||
activeFolderNames.add(folderName);
|
||||
Collections.sort(activeFeeds);
|
||||
activeFolderChildren.add(activeFeeds);
|
||||
|
@ -547,9 +618,14 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
}
|
||||
|
||||
private void recountChildren() {
|
||||
if (activeFolderChildren == null) return;
|
||||
int newFeedCount = 0;
|
||||
newFeedCount += socialFeedsOrdered.size();
|
||||
newFeedCount += starredCountsByTag.size();
|
||||
newFeedCount += socialFeedsActive.size();
|
||||
if (currentState == StateFilter.SAVED) {
|
||||
// only count saved feeds if in saved mode, since the expectation is that we are
|
||||
// counting to detect a zero-feeds-in-this-mode situation
|
||||
newFeedCount += starredCountsByTag.size();
|
||||
}
|
||||
for (List<Feed> folder : activeFolderChildren) {
|
||||
newFeedCount += folder.size();
|
||||
}
|
||||
|
@ -606,8 +682,9 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
return count;
|
||||
}
|
||||
|
||||
private synchronized void forceRecount() {
|
||||
public synchronized void forceRecount() {
|
||||
recountFeeds();
|
||||
recountSocialFeeds();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
|
@ -615,8 +692,8 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
notifyDataSetInvalidated();
|
||||
|
||||
synchronized (this) {
|
||||
socialFeeds = Collections.emptyMap();
|
||||
socialFeedsOrdered = Collections.emptyList();
|
||||
socialFeedsActive = Collections.emptyList();
|
||||
totalSocialNeutCount = 0;
|
||||
totalSocialPosiCount = 0;
|
||||
|
||||
|
@ -655,11 +732,13 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
|
||||
/** Get the cached SocialFeed object for the feed at the given list location. */
|
||||
public SocialFeed getSocialFeed(int groupPosition, int childPosition) {
|
||||
return socialFeedsOrdered.get(childPosition);
|
||||
return socialFeedsActive.get(childPosition);
|
||||
}
|
||||
|
||||
public synchronized void changeState(StateFilter state) {
|
||||
currentState = state;
|
||||
lastFeedViewedId = null; // clear when changing modes
|
||||
lastFolderViewed = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -759,16 +838,6 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
private int sumIntRows(Cursor c, int columnIndex) {
|
||||
if (c == null) return 0;
|
||||
int i = 0;
|
||||
c.moveToPosition(-1);
|
||||
while (c.moveToNext()) {
|
||||
i += c.getInt(columnIndex);
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to filter out and carp about negative unread counts. These tend to indicate
|
||||
* a problem in the app or API, but are very confusing to users.
|
||||
|
|
|
@ -6,13 +6,12 @@ import com.newsblur.activity.ReadingAdapter;
|
|||
import com.newsblur.domain.Classifier;
|
||||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.fragment.ReadingItemFragment;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
|
||||
public class MixedFeedsReadingAdapter extends ReadingAdapter {
|
||||
|
||||
public MixedFeedsReadingAdapter(FragmentManager fragmentManager, DefaultFeedView defaultFeedView, String sourceUserId) {
|
||||
super(fragmentManager, defaultFeedView, sourceUserId);
|
||||
public MixedFeedsReadingAdapter(FragmentManager fragmentManager, String sourceUserId) {
|
||||
super(fragmentManager, sourceUserId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -28,7 +27,7 @@ public class MixedFeedsReadingAdapter extends ReadingAdapter {
|
|||
// be loaded async by the fragment itself
|
||||
Classifier classifier = FeedUtils.dbHelper.getClassifierForFeed(story.feedId);
|
||||
|
||||
return ReadingItemFragment.newInstance(story, feedTitle, feedFaviconColor, feedFaviconFade, feedFaviconBorder, feedFaviconText, feedFaviconUrl, classifier, true, defaultFeedView, sourceUserId);
|
||||
return ReadingItemFragment.newInstance(story, feedTitle, feedFaviconColor, feedFaviconFade, feedFaviconBorder, feedFaviconText, feedFaviconUrl, classifier, true, sourceUserId);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@ import android.content.Context;
|
|||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.text.Html;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.SimpleCursorAdapter;
|
||||
|
@ -16,9 +16,11 @@ import java.util.Date;
|
|||
import com.newsblur.R;
|
||||
import com.newsblur.database.DatabaseConstants;
|
||||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.domain.UserDetails;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.StoryUtils;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
||||
/**
|
||||
* Story list adapter. Uses SimpleCursorAdapter behaviour for text elements and custom
|
||||
|
@ -51,12 +53,14 @@ public class StoryItemsAdapter extends SimpleCursorAdapter {
|
|||
private final static int READ_STORY_ALPHA_B255 = (int) (255f * READ_STORY_ALPHA);
|
||||
|
||||
protected Cursor cursor;
|
||||
private boolean showNone = false;
|
||||
|
||||
private final Context context;
|
||||
private boolean ignoreReadStatus;
|
||||
private boolean ignoreIntel;
|
||||
private boolean singleFeed;
|
||||
private float textSize;
|
||||
private UserDetails user;
|
||||
|
||||
public StoryItemsAdapter(Context context, Cursor c, boolean ignoreReadStatus, boolean ignoreIntel, boolean singleFeed) {
|
||||
super(context, R.layout.row_story, c, COL_NAME_MAPPINGS, RES_ID_MAPPINGS, 0);
|
||||
|
@ -71,31 +75,73 @@ public class StoryItemsAdapter extends SimpleCursorAdapter {
|
|||
|
||||
textSize = PrefsUtils.getListTextSize(context);
|
||||
|
||||
user = PrefsUtils.getUserDetails(context);
|
||||
|
||||
this.setViewBinder(new StoryItemViewBinder());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
public synchronized int getCount() {
|
||||
if (showNone) return 0;
|
||||
return cursor.getCount();
|
||||
}
|
||||
|
||||
public synchronized boolean isStale() {
|
||||
return cursor.isClosed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor swapCursor(Cursor c) {
|
||||
public synchronized Cursor swapCursor(Cursor c) {
|
||||
this.cursor = c;
|
||||
return super.swapCursor(c);
|
||||
}
|
||||
|
||||
public Story getStory(int position) {
|
||||
cursor.moveToPosition(position);
|
||||
return Story.fromCursor(cursor);
|
||||
public synchronized void setShowNone(boolean showNone) {
|
||||
this.showNone = showNone;
|
||||
}
|
||||
|
||||
public synchronized Story getStory(int position) {
|
||||
if (cursor == null || cursor.isClosed() || cursor.getColumnCount() == 0 || position >= cursor.getCount() || position < 0) {
|
||||
return null;
|
||||
} else {
|
||||
cursor.moveToPosition(position);
|
||||
return Story.fromCursor(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
public void setTextSize(float textSize) {
|
||||
this.textSize = textSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized long getItemId(int position) {
|
||||
if (cursor == null || cursor.isClosed() || cursor.getColumnCount() == 0 || position >= cursor.getCount() || position < 0) return 0;
|
||||
try {
|
||||
return super.getItemId(position);
|
||||
} catch (IllegalStateException ise) {
|
||||
// despite all the checks above, this can still async fail if the curor is closed by the loader outside of our control
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (cursor == null || cursor.isClosed() || cursor.getColumnCount() == 0 || position >= cursor.getCount() || position < 0) return new View(context);
|
||||
try {
|
||||
return super.getView(position, convertView, parent);
|
||||
} catch (IllegalStateException ise) {
|
||||
// despite all the checks above, this can still async fail if the curor is closed by the loader outside of our control
|
||||
return new View(context);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(View v, Context context, Cursor cursor) {
|
||||
public synchronized void bindView(View v, Context context, Cursor cursor) {
|
||||
// see if this is a valid view for us to bind
|
||||
if (v.findViewById(R.id.row_item_title) == null) {
|
||||
com.newsblur.util.Log.w(this, "asked to bind wrong type of view");
|
||||
return;
|
||||
}
|
||||
super.bindView(v, context, cursor);
|
||||
|
||||
TextView itemTitle = (TextView) v.findViewById(R.id.row_item_title);
|
||||
|
@ -119,13 +165,23 @@ public class StoryItemsAdapter extends SimpleCursorAdapter {
|
|||
View borderTwo = v.findViewById(R.id.row_item_favicon_borderbar_2);
|
||||
String feedColor = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_FAVICON_COLOR));
|
||||
String feedFade = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_FAVICON_FADE));
|
||||
if (!TextUtils.equals(feedColor, "#null") && !TextUtils.equals(feedFade, "#null")) {
|
||||
borderOne.setBackgroundColor(Color.parseColor(feedColor));
|
||||
borderTwo.setBackgroundColor(Color.parseColor(feedFade));
|
||||
int feedColorVal = Color.GRAY;
|
||||
int feedFadeVal = Color.LTGRAY;
|
||||
if ((feedColor == null) ||
|
||||
(feedFade == null) ||
|
||||
TextUtils.equals(feedColor, "null") ||
|
||||
TextUtils.equals(feedFade, "null")) {
|
||||
// feed didn't supply color info, leave at default grey
|
||||
} else {
|
||||
borderOne.setBackgroundColor(Color.GRAY);
|
||||
borderTwo.setBackgroundColor(Color.LTGRAY);
|
||||
try {
|
||||
feedColorVal = Color.parseColor("#" + feedColor);
|
||||
feedFadeVal = Color.parseColor("#" + feedFade);
|
||||
} catch (NumberFormatException nfe) {
|
||||
com.newsblur.util.Log.e(this, "feed supplied bad color info: " + nfe.getMessage());
|
||||
}
|
||||
}
|
||||
borderOne.setBackgroundColor(feedColorVal);
|
||||
borderTwo.setBackgroundColor(feedFadeVal);
|
||||
|
||||
// dynamic text sizing
|
||||
itemTitle.setTextSize(textSize * defaultTextSize_row_item_title);
|
||||
|
@ -172,6 +228,19 @@ public class StoryItemsAdapter extends SimpleCursorAdapter {
|
|||
v.findViewById(R.id.row_item_saved_icon).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
boolean shared = false;
|
||||
findshareloop: for (String userId : story.sharedUserIds) {
|
||||
if (TextUtils.equals(userId, user.id)) {
|
||||
shared = true;
|
||||
break findshareloop;
|
||||
}
|
||||
}
|
||||
if (shared) {
|
||||
v.findViewById(R.id.row_item_shared_icon).setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
v.findViewById(R.id.row_item_shared_icon).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (!PrefsUtils.isShowContentPreviews(context)) {
|
||||
itemContent.setVisibility(View.GONE);
|
||||
}
|
||||
|
@ -195,34 +264,41 @@ public class StoryItemsAdapter extends SimpleCursorAdapter {
|
|||
|
||||
@Override
|
||||
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
|
||||
String columnName = cursor.getColumnName(columnIndex);
|
||||
if (TextUtils.equals(columnName, DatabaseConstants.STORY_AUTHORS)) {
|
||||
if (TextUtils.isEmpty(cursor.getString(columnIndex))) {
|
||||
view.setVisibility(View.GONE);
|
||||
} else {
|
||||
view.setVisibility(View.VISIBLE);
|
||||
((TextView) view).setText(cursor.getString(columnIndex).toUpperCase());
|
||||
}
|
||||
return true;
|
||||
} else if (TextUtils.equals(columnName, DatabaseConstants.STORY_INTELLIGENCE_TOTAL)) {
|
||||
if (! ignoreIntel) {
|
||||
int score = cursor.getInt(columnIndex);
|
||||
if (score > 0) {
|
||||
((ImageView) view).setImageResource(R.drawable.g_icn_focus);
|
||||
} else if (score == 0) {
|
||||
((ImageView) view).setImageResource(R.drawable.g_icn_unread);
|
||||
// some devices keep binding after the loadermanager swaps. fail fast.
|
||||
if (cursor.isClosed()) return true;
|
||||
try {
|
||||
String columnName = cursor.getColumnName(columnIndex);
|
||||
if (TextUtils.equals(columnName, DatabaseConstants.STORY_AUTHORS)) {
|
||||
if (TextUtils.isEmpty(cursor.getString(columnIndex))) {
|
||||
view.setVisibility(View.GONE);
|
||||
} else {
|
||||
((ImageView) view).setImageResource(R.drawable.g_icn_hidden);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
((TextView) view).setText(cursor.getString(columnIndex).toUpperCase());
|
||||
}
|
||||
} else {
|
||||
((ImageView) view).setImageResource(android.R.color.transparent);
|
||||
return true;
|
||||
} else if (TextUtils.equals(columnName, DatabaseConstants.STORY_INTELLIGENCE_TOTAL)) {
|
||||
if (! ignoreIntel) {
|
||||
int score = cursor.getInt(columnIndex);
|
||||
if (score > 0) {
|
||||
((ImageView) view).setImageResource(R.drawable.g_icn_focus);
|
||||
} else if (score == 0) {
|
||||
((ImageView) view).setImageResource(R.drawable.g_icn_unread);
|
||||
} else {
|
||||
((ImageView) view).setImageResource(R.drawable.g_icn_hidden);
|
||||
}
|
||||
} else {
|
||||
((ImageView) view).setImageResource(android.R.color.transparent);
|
||||
}
|
||||
return true;
|
||||
} else if (TextUtils.equals(columnName, DatabaseConstants.STORY_TITLE)) {
|
||||
((TextView) view).setText(UIUtils.fromHtml(cursor.getString(columnIndex)));
|
||||
return true;
|
||||
} else if (TextUtils.equals(columnName, DatabaseConstants.STORY_TIMESTAMP)) {
|
||||
((TextView) view).setText(StoryUtils.formatShortDate(context, new Date(cursor.getLong(columnIndex))));
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
} else if (TextUtils.equals(columnName, DatabaseConstants.STORY_TITLE)) {
|
||||
((TextView) view).setText(Html.fromHtml(cursor.getString(columnIndex)));
|
||||
return true;
|
||||
} else if (TextUtils.equals(columnName, DatabaseConstants.STORY_TIMESTAMP)) {
|
||||
((TextView) view).setText(StoryUtils.formatShortDate(context, new Date(cursor.getLong(columnIndex))));
|
||||
} catch (android.database.StaleDataException sdex) {
|
||||
com.newsblur.util.Log.d(getClass().getName(), "view bound after loader reset");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,9 +10,13 @@ import com.google.gson.annotations.SerializedName;
|
|||
import com.newsblur.database.DatabaseConstants;
|
||||
|
||||
public class Comment implements Serializable {
|
||||
|
||||
// new comments cannot possibly have the server-generated ID, so are inserted with partial info until reconciled
|
||||
public static final String PLACEHOLDER_COMMENT_ID = "__PLACEHOLDER_ID__";
|
||||
|
||||
private static final long serialVersionUID = -2018705258520565390L;
|
||||
|
||||
// we almost always override API version with sensible PK constructed by concating story, feed, and user IDs
|
||||
@SerializedName("id")
|
||||
public String id;
|
||||
|
||||
@SerializedName("comments")
|
||||
|
@ -35,6 +39,7 @@ public class Comment implements Serializable {
|
|||
|
||||
public Reply[] replies;
|
||||
|
||||
// not vended by API directly, but comments always appear in the context of a story
|
||||
public String storyId;
|
||||
|
||||
// not vended by API, but we set it depending on which comment block of the response in which it appeared
|
||||
|
@ -43,6 +48,9 @@ public class Comment implements Serializable {
|
|||
// means this "comment" is actually a text-less share, which is identical to a comment, but included in a different list in the story member
|
||||
public boolean isPseudo = false;
|
||||
|
||||
// not vended by API, indicates this is a client-side placeholder for until we can get an ID from the server
|
||||
public boolean isPlaceholder = false;
|
||||
|
||||
public ContentValues getValues() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(DatabaseConstants.COMMENT_DATE, date);
|
||||
|
@ -55,6 +63,7 @@ public class Comment implements Serializable {
|
|||
values.put(DatabaseConstants.COMMENT_USERID, userId);
|
||||
values.put(DatabaseConstants.COMMENT_ID, id);
|
||||
values.put(DatabaseConstants.COMMENT_ISPSEUDO, isPseudo ? "true" : "false");
|
||||
values.put(DatabaseConstants.COMMENT_ISPLACEHOLDER, isPlaceholder ? "true" : "false");
|
||||
return values;
|
||||
}
|
||||
|
||||
|
@ -71,11 +80,8 @@ public class Comment implements Serializable {
|
|||
comment.sourceUserId = cursor.getString(cursor.getColumnIndex(DatabaseConstants.COMMENT_SOURCE_USERID));
|
||||
comment.id = cursor.getString(cursor.getColumnIndex(DatabaseConstants.COMMENT_ID));
|
||||
comment.isPseudo = Boolean.parseBoolean(cursor.getString(cursor.getColumnIndex(DatabaseConstants.COMMENT_ISPSEUDO)));
|
||||
comment.isPlaceholder = Boolean.parseBoolean(cursor.getString(cursor.getColumnIndex(DatabaseConstants.COMMENT_ISPLACEHOLDER)));
|
||||
return comment;
|
||||
}
|
||||
|
||||
public static String constructId(String storyId, String feedId, String userId) {
|
||||
return TextUtils.concat(feedId, storyId, userId).toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.database.Cursor;
|
|||
import android.text.TextUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
@ -59,7 +60,6 @@ public class Feed implements Comparable<Feed>, Serializable {
|
|||
@SerializedName("updated_seconds_ago")
|
||||
public int lastUpdated;
|
||||
|
||||
// NB: deserialized but not stored
|
||||
@SerializedName("notification_types")
|
||||
public List<String> notificationTypes;
|
||||
|
||||
|
@ -67,26 +67,31 @@ public class Feed implements Comparable<Feed>, Serializable {
|
|||
@SerializedName("notification_filter")
|
||||
public String notificationFilter;
|
||||
|
||||
// not vended by API, but used locally for UI
|
||||
public boolean fetchPending;
|
||||
|
||||
public ContentValues getValues() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(DatabaseConstants.FEED_ID, feedId);
|
||||
values.put(DatabaseConstants.FEED_ACTIVE, active);
|
||||
values.put(DatabaseConstants.FEED_ADDRESS, address);
|
||||
values.put(DatabaseConstants.FEED_FAVICON_COLOR, "#" + faviconColor);
|
||||
values.put(DatabaseConstants.FEED_FAVICON_BORDER, "#" + faviconBorder);
|
||||
values.put(DatabaseConstants.FEED_FAVICON_COLOR, faviconColor);
|
||||
values.put(DatabaseConstants.FEED_FAVICON_BORDER, faviconBorder);
|
||||
values.put(DatabaseConstants.FEED_POSITIVE_COUNT, positiveCount);
|
||||
values.put(DatabaseConstants.FEED_NEUTRAL_COUNT, neutralCount);
|
||||
values.put(DatabaseConstants.FEED_NEGATIVE_COUNT, negativeCount);
|
||||
values.put(DatabaseConstants.FEED_FAVICON_FADE, "#" + faviconFade);
|
||||
values.put(DatabaseConstants.FEED_FAVICON_FADE, faviconFade);
|
||||
values.put(DatabaseConstants.FEED_FAVICON_TEXT, faviconText);
|
||||
values.put(DatabaseConstants.FEED_FAVICON_URL, faviconUrl);
|
||||
values.put(DatabaseConstants.FEED_LINK, feedLink);
|
||||
values.put(DatabaseConstants.FEED_SUBSCRIBERS, subscribers);
|
||||
values.put(DatabaseConstants.FEED_TITLE, title);
|
||||
values.put(DatabaseConstants.FEED_UPDATED_SECONDS, lastUpdated);
|
||||
values.put(DatabaseConstants.FEED_NOTIFICATION_TYPES, DatabaseConstants.flattenStringList(notificationTypes));
|
||||
if (isNotifyAndroid()) {
|
||||
values.put(DatabaseConstants.FEED_NOTIFICATION_FILTER, notificationFilter);
|
||||
}
|
||||
values.put(DatabaseConstants.FEED_FETCH_PENDING, fetchPending);
|
||||
return values;
|
||||
}
|
||||
|
||||
|
@ -110,7 +115,9 @@ public class Feed implements Comparable<Feed>, Serializable {
|
|||
feed.subscribers = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_SUBSCRIBERS));
|
||||
feed.title = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_TITLE));
|
||||
feed.lastUpdated = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.FEED_UPDATED_SECONDS));
|
||||
feed.notificationTypes = DatabaseConstants.unflattenStringList(cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_NOTIFICATION_TYPES)));
|
||||
feed.notificationFilter = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_NOTIFICATION_FILTER));
|
||||
feed.fetchPending = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_FETCH_PENDING)).equals("1");
|
||||
return feed;
|
||||
}
|
||||
|
||||
|
@ -130,14 +137,16 @@ public class Feed implements Comparable<Feed>, Serializable {
|
|||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (! (o instanceof Feed)) return false;
|
||||
Feed otherFeed = (Feed) o;
|
||||
boolean isEquals = (TextUtils.equals(feedId, otherFeed.feedId) &&
|
||||
negativeCount == otherFeed.negativeCount &&
|
||||
neutralCount == otherFeed.neutralCount &&
|
||||
positiveCount == otherFeed.positiveCount);
|
||||
return isEquals;
|
||||
return (TextUtils.equals(feedId, otherFeed.feedId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return feedId.hashCode();
|
||||
}
|
||||
|
||||
public int compareTo(Feed f) {
|
||||
return title.compareToIgnoreCase(f.title);
|
||||
}
|
||||
|
@ -150,6 +159,35 @@ public class Feed implements Comparable<Feed>, Serializable {
|
|||
return false;
|
||||
}
|
||||
|
||||
public void enableAndroidNotifications(boolean enable) {
|
||||
if (notificationTypes == null) notificationTypes = new ArrayList<String>();
|
||||
if (enable && (!notificationTypes.contains(NOTIFY_TYPE_ANDROID))) {
|
||||
notificationTypes.add(NOTIFY_TYPE_ANDROID);
|
||||
}
|
||||
if (!enable) {
|
||||
notificationTypes.remove(NOTIFY_TYPE_ANDROID);
|
||||
notificationFilter = null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isNotifyUnread() {
|
||||
if (!isNotifyAndroid()) return false;
|
||||
return NOTIFY_FILTER_UNREAD.equals(notificationFilter);
|
||||
}
|
||||
|
||||
public boolean isNotifyFocus() {
|
||||
if (!isNotifyAndroid()) return false;
|
||||
return NOTIFY_FILTER_FOCUS.equals(notificationFilter);
|
||||
}
|
||||
|
||||
public void setNotifyUnread() {
|
||||
this.notificationFilter = NOTIFY_FILTER_UNREAD;
|
||||
}
|
||||
|
||||
public void setNotifyFocus() {
|
||||
this.notificationFilter = NOTIFY_FILTER_FOCUS;
|
||||
}
|
||||
|
||||
private static final String NOTIFY_TYPE_ANDROID = "android";
|
||||
public static final String NOTIFY_FILTER_UNREAD = "unread";
|
||||
public static final String NOTIFY_FILTER_FOCUS = "focus";
|
||||
|
|
|
@ -17,8 +17,6 @@ public class FeedResult {
|
|||
|
||||
public String label;
|
||||
|
||||
public String id;
|
||||
|
||||
public String favicon;
|
||||
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ public class Folder {
|
|||
|
||||
@Override
|
||||
public boolean equals(Object otherFolder) {
|
||||
if (! (otherFolder instanceof Folder)) return false;
|
||||
return TextUtils.equals(((Folder) otherFolder).name, name);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,12 +4,15 @@ import java.util.Date;
|
|||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.newsblur.database.DatabaseConstants;
|
||||
|
||||
public class Reply {
|
||||
|
||||
// new replies cannot possibly have the server-generated ID, so are inserted with partial info until reconciled
|
||||
public static final String PLACEHOLDER_COMMENT_ID = "__PLACEHOLDER_ID__";
|
||||
|
||||
@SerializedName("reply_id")
|
||||
public String id;
|
||||
|
||||
|
@ -25,9 +28,12 @@ public class Reply {
|
|||
@SerializedName("date")
|
||||
public Date date;
|
||||
|
||||
// NB: this is the commentId that we generate, not the API one
|
||||
// not vended by API directly, but all replies come in the context of an enclosing comment
|
||||
public String commentId;
|
||||
|
||||
// not vended by API, indicates this is a client-side placeholder for until we can get an ID from the server
|
||||
public boolean isPlaceholder = false;
|
||||
|
||||
public ContentValues getValues() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(DatabaseConstants.REPLY_DATE, date.getTime());
|
||||
|
@ -36,6 +42,7 @@ public class Reply {
|
|||
values.put(DatabaseConstants.REPLY_COMMENTID, commentId);
|
||||
values.put(DatabaseConstants.REPLY_ID, id);
|
||||
values.put(DatabaseConstants.REPLY_USERID, userId);
|
||||
values.put(DatabaseConstants.REPLY_ISPLACEHOLDER, isPlaceholder ? "true" : "false");
|
||||
return values;
|
||||
}
|
||||
|
||||
|
@ -47,11 +54,8 @@ public class Reply {
|
|||
reply.commentId = cursor.getString(cursor.getColumnIndex(DatabaseConstants.REPLY_COMMENTID));
|
||||
reply.id = cursor.getString(cursor.getColumnIndex(DatabaseConstants.REPLY_ID));
|
||||
reply.userId = cursor.getString(cursor.getColumnIndex(DatabaseConstants.REPLY_USERID));
|
||||
reply.isPlaceholder = Boolean.parseBoolean(cursor.getString(cursor.getColumnIndex(DatabaseConstants.REPLY_ISPLACEHOLDER)));
|
||||
return reply;
|
||||
}
|
||||
|
||||
// construct a string we can internally use as a PK
|
||||
public String constructId() {
|
||||
return TextUtils.concat(commentId, "_", Long.toString(date.getTime())).toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,13 +62,14 @@ public class SocialFeed implements Serializable {
|
|||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (! (o instanceof SocialFeed)) return false;
|
||||
SocialFeed otherFeed = (SocialFeed) o;
|
||||
boolean equals = (TextUtils.equals(userId, otherFeed.userId) &&
|
||||
positiveCount == otherFeed.positiveCount &&
|
||||
neutralCount == otherFeed.neutralCount &&
|
||||
negativeCount == otherFeed.negativeCount &&
|
||||
TextUtils.equals(photoUrl, otherFeed.photoUrl));
|
||||
return equals;
|
||||
return (TextUtils.equals(userId, otherFeed.userId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return userId.hashCode();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -40,9 +40,6 @@ public class Story implements Serializable {
|
|||
@SerializedName("story_tags")
|
||||
public String[] tags;
|
||||
|
||||
@SerializedName("user_tags")
|
||||
public String[] userTags = new String[]{};
|
||||
|
||||
@SerializedName("social_user_id")
|
||||
public String socialUserId;
|
||||
|
||||
|
@ -97,6 +94,9 @@ public class Story implements Serializable {
|
|||
// non-API, though it probably could/should be. populated on first story ingest if thumbnails are turned on
|
||||
public String thumbnailUrl;
|
||||
|
||||
// non-API, but tracked locally and fudged (see SyncService) to implement ordering of gobal shared stories
|
||||
public long sharedTimestamp = 0L;
|
||||
|
||||
public ContentValues getValues() {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(DatabaseConstants.STORY_ID, id);
|
||||
|
@ -116,7 +116,6 @@ public class Story implements Serializable {
|
|||
values.put(DatabaseConstants.STORY_INTELLIGENCE_TITLE, intelligence.intelligenceTitle);
|
||||
values.put(DatabaseConstants.STORY_INTELLIGENCE_TOTAL, intelligence.calcTotalIntel());
|
||||
values.put(DatabaseConstants.STORY_TAGS, TextUtils.join(",", tags));
|
||||
values.put(DatabaseConstants.STORY_USER_TAGS, TextUtils.join(",", userTags));
|
||||
values.put(DatabaseConstants.STORY_READ, read);
|
||||
values.put(DatabaseConstants.STORY_STARRED, starred);
|
||||
values.put(DatabaseConstants.STORY_STARRED_DATE, starredTimestamp);
|
||||
|
@ -124,6 +123,7 @@ public class Story implements Serializable {
|
|||
values.put(DatabaseConstants.STORY_HASH, storyHash);
|
||||
values.put(DatabaseConstants.STORY_IMAGE_URLS, TextUtils.join(",", imageUrls));
|
||||
values.put(DatabaseConstants.STORY_LAST_READ_DATE, lastReadTimestamp);
|
||||
values.put(DatabaseConstants.STORY_SHARED_DATE, sharedTimestamp);
|
||||
values.put(DatabaseConstants.STORY_SEARCH_HIT, searchHit);
|
||||
values.put(DatabaseConstants.STORY_THUMBNAIL_URL, thumbnailUrl);
|
||||
return values;
|
||||
|
@ -151,11 +151,11 @@ public class Story implements Serializable {
|
|||
story.starred = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_STARRED)) > 0;
|
||||
story.starredTimestamp = cursor.getLong(cursor.getColumnIndex(DatabaseConstants.STORY_STARRED_DATE));
|
||||
story.tags = TextUtils.split(cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_TAGS)), ",");
|
||||
story.userTags = TextUtils.split(cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_USER_TAGS)), ",");
|
||||
story.feedId = cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_FEED_ID));
|
||||
story.id = cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_ID));
|
||||
story.storyHash = cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_HASH));
|
||||
story.lastReadTimestamp = cursor.getLong(cursor.getColumnIndex(DatabaseConstants.STORY_LAST_READ_DATE));
|
||||
story.sharedTimestamp = cursor.getLong(cursor.getColumnIndex(DatabaseConstants.STORY_SHARED_DATE));
|
||||
story.thumbnailUrl = cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_THUMBNAIL_URL));
|
||||
return story;
|
||||
}
|
||||
|
|
|
@ -61,21 +61,7 @@ public class UserDetails {
|
|||
@SerializedName("following_you")
|
||||
public boolean followsYou;
|
||||
|
||||
@SerializedName("popular_publishers")
|
||||
public Publisher[] popularPublishers;
|
||||
|
||||
@SerializedName("photo_url")
|
||||
public String photoUrl;
|
||||
|
||||
public class Publisher {
|
||||
|
||||
@SerializedName("story_count")
|
||||
int storyCount;
|
||||
|
||||
@SerializedName("feed_title")
|
||||
String feedTitle;
|
||||
|
||||
int id;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,26 +4,28 @@ import android.app.Activity;
|
|||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.app.DialogFragment;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.activity.Main;
|
||||
import com.newsblur.network.APIManager;
|
||||
import com.newsblur.network.domain.AddFeedResponse;
|
||||
import com.newsblur.service.NBSyncService;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
||||
public class AddFeedFragment extends DialogFragment {
|
||||
|
||||
private static final String FEED_ID = "feed_url";
|
||||
private static final String FEED_URI = "feed_url";
|
||||
private static final String FEED_NAME = "feed_name";
|
||||
private APIManager apiManager;
|
||||
|
||||
|
||||
public static AddFeedFragment newInstance(final String feedId, final String feedName) {
|
||||
public static AddFeedFragment newInstance(String feedUri, String feedName) {
|
||||
AddFeedFragment frag = new AddFeedFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putString(FEED_ID, feedId);
|
||||
args.putString(FEED_URI, feedUri);
|
||||
args.putString(FEED_NAME, feedName);
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
|
@ -33,7 +35,9 @@ public class AddFeedFragment extends DialogFragment {
|
|||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final String addFeedString = getResources().getString(R.string.add_feed_message);
|
||||
final Activity activity = getActivity();
|
||||
apiManager = new APIManager(activity);
|
||||
final APIManager apiManager = new APIManager(activity);
|
||||
final Intent intent = new Intent(activity, Main.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setMessage(String.format(addFeedString, getArguments().getString(FEED_NAME)));
|
||||
|
@ -41,23 +45,25 @@ public class AddFeedFragment extends DialogFragment {
|
|||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
|
||||
new AsyncTask<Void, Void, Boolean>() {
|
||||
new AsyncTask<Void, Void, AddFeedResponse>() {
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... arg) {
|
||||
return apiManager.addFeed(getArguments().getString(FEED_ID), null);
|
||||
protected AddFeedResponse doInBackground(Void... arg) {
|
||||
((AddFeedProgressListener) activity).addFeedStarted();
|
||||
return apiManager.addFeed(getArguments().getString(FEED_URI));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
if (result) {
|
||||
activity.finish();
|
||||
protected void onPostExecute(AddFeedResponse result) {
|
||||
if (!result.isError()) {
|
||||
// trigger a sync when we return to Main so that the new feed will show up
|
||||
NBSyncService.forceFeedsFolders();
|
||||
AddFeedFragment.this.dismiss();
|
||||
intent.putExtra(Main.EXTRA_FORCE_SHOW_FEED_ID, result.feed.feedId);
|
||||
} else {
|
||||
AddFeedFragment.this.dismiss();
|
||||
Toast.makeText(activity, "Error adding feed", Toast.LENGTH_SHORT).show();
|
||||
UIUtils.safeToast(activity, R.string.add_feed_error, Toast.LENGTH_SHORT);
|
||||
}
|
||||
activity.startActivity(intent);
|
||||
activity.finish();
|
||||
AddFeedFragment.this.dismiss();
|
||||
};
|
||||
}.execute();
|
||||
}
|
||||
|
@ -66,8 +72,14 @@ public class AddFeedFragment extends DialogFragment {
|
|||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
AddFeedFragment.this.dismiss();
|
||||
activity.startActivity(intent);
|
||||
activity.finish();
|
||||
}
|
||||
});
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
public interface AddFeedProgressListener {
|
||||
public abstract void addFeedStarted();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,11 @@ package com.newsblur.fragment;
|
|||
import android.content.Loader;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.View;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.database.StoryItemsAdapter;
|
||||
|
||||
public class AllSharedStoriesItemListFragment extends ItemListFragment {
|
||||
|
@ -24,4 +28,11 @@ public class AllSharedStoriesItemListFragment extends ItemListFragment {
|
|||
return everythingFragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
menu.removeItem(R.id.menu_mark_newer_stories_as_read);
|
||||
menu.removeItem(R.id.menu_mark_older_stories_as_read);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
package com.newsblur.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.app.DialogFragment;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.widget.RadioButton;
|
||||
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.Bind;
|
||||
import butterknife.OnClick;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
import com.newsblur.util.DefaultFeedViewChangedListener;
|
||||
|
||||
/**
|
||||
* Created by mark on 09/01/2014.
|
||||
*/
|
||||
public class DefaultFeedViewDialogFragment extends DialogFragment {
|
||||
|
||||
private static String CURRENT_VIEW = "currentView";
|
||||
private DefaultFeedView currentValue;
|
||||
@Bind(R.id.radio_story) RadioButton storyButton;
|
||||
@Bind(R.id.radio_text) RadioButton textButton;
|
||||
|
||||
public static DefaultFeedViewDialogFragment newInstance(DefaultFeedView currentValue) {
|
||||
DefaultFeedViewDialogFragment dialog = new DefaultFeedViewDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putSerializable(CURRENT_VIEW, currentValue);
|
||||
dialog.setArguments(args);
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||
currentValue = (DefaultFeedView) getArguments().getSerializable(CURRENT_VIEW);
|
||||
View v = inflater.inflate(R.layout.defaultfeedview_dialog, null);
|
||||
ButterKnife.bind(this, v);
|
||||
|
||||
storyButton.setChecked(currentValue == DefaultFeedView.STORY);
|
||||
textButton.setChecked(currentValue == DefaultFeedView.TEXT);
|
||||
|
||||
getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
getDialog().getWindow().getAttributes().gravity = Gravity.BOTTOM;
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
@OnClick(R.id.radio_story) void selectStory() {
|
||||
if (currentValue != DefaultFeedView.STORY) {
|
||||
((DefaultFeedViewChangedListener) getActivity()).defaultFeedViewChanged(DefaultFeedView.STORY);
|
||||
}
|
||||
dismiss();
|
||||
}
|
||||
|
||||
@OnClick(R.id.radio_text) void selectText() {
|
||||
if (currentValue != DefaultFeedView.TEXT) {
|
||||
((DefaultFeedViewChangedListener) getActivity()).defaultFeedViewChanged(DefaultFeedView.TEXT);
|
||||
}
|
||||
dismiss();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package com.newsblur.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.app.DialogFragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
|
||||
public class EditReplyDialogFragment extends DialogFragment {
|
||||
|
||||
private static final String STORY = "story";
|
||||
private static final String COMMENT_USER_ID = "comment_user_id";
|
||||
private static final String REPLY_ID = "reply_id";
|
||||
private static final String REPLY_TEXT = "reply_text";
|
||||
|
||||
public static EditReplyDialogFragment newInstance(Story story, String commentUserId, String replyId, String replyText) {
|
||||
EditReplyDialogFragment frag = new EditReplyDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putSerializable(STORY, story);
|
||||
args.putString(COMMENT_USER_ID, commentUserId);
|
||||
args.putString(REPLY_ID, replyId);
|
||||
args.putString(REPLY_TEXT, replyText);
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity activity = getActivity();
|
||||
final Story story = (Story) getArguments().getSerializable(STORY);
|
||||
final String commentUserId = getArguments().getString(COMMENT_USER_ID);
|
||||
final String replyId = getArguments().getString(REPLY_ID);
|
||||
String replyText = getArguments().getString(REPLY_TEXT);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setTitle(R.string.edit_reply);
|
||||
|
||||
LayoutInflater layoutInflater = LayoutInflater.from(activity);
|
||||
View replyView = layoutInflater.inflate(R.layout.reply_dialog, null);
|
||||
builder.setView(replyView);
|
||||
final EditText reply = (EditText) replyView.findViewById(R.id.reply_field);
|
||||
reply.setText(replyText);
|
||||
|
||||
builder.setPositiveButton(R.string.edit_reply_update, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
String replyText = reply.getText().toString();
|
||||
FeedUtils.updateReply(activity, story, commentUserId, replyId, replyText);
|
||||
EditReplyDialogFragment.this.dismiss();
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.edit_reply_delete, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
FeedUtils.deleteReply(activity, story, commentUserId, replyId);
|
||||
EditReplyDialogFragment.this.dismiss();
|
||||
}
|
||||
});
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
}
|
|
@ -69,6 +69,10 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
|||
@Bind(R.id.folderfeed_list) ExpandableListView list;
|
||||
public boolean firstCursorSeenYet = false;
|
||||
|
||||
// the two-step context menu for feeds requires us to temp store the feed long-pressed so
|
||||
// it can be accessed during the sub-menu tap
|
||||
private Feed lastMenuFeed;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
@ -98,11 +102,11 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
|||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
switch (id) {
|
||||
case SOCIALFEEDS_LOADER:
|
||||
return FeedUtils.dbHelper.getSocialFeedsLoader(currentState);
|
||||
return FeedUtils.dbHelper.getSocialFeedsLoader();
|
||||
case FOLDERS_LOADER:
|
||||
return FeedUtils.dbHelper.getFoldersLoader();
|
||||
case FEEDS_LOADER:
|
||||
return FeedUtils.dbHelper.getFeedsLoader(currentState);
|
||||
return FeedUtils.dbHelper.getFeedsLoader();
|
||||
case SAVEDCOUNT_LOADER:
|
||||
return FeedUtils.dbHelper.getSavedStoryCountsLoader();
|
||||
default:
|
||||
|
@ -150,6 +154,7 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
|||
|
||||
public void hasUpdated() {
|
||||
if (isAdded()) {
|
||||
com.newsblur.util.Log.d(this, "loading feeds in mode: " + currentState);
|
||||
try {
|
||||
getLoaderManager().restartLoader(SOCIALFEEDS_LOADER, null, this);
|
||||
getLoaderManager().restartLoader(FOLDERS_LOADER, null, this);
|
||||
|
@ -236,6 +241,7 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
|||
switch(type) {
|
||||
case ExpandableListView.PACKED_POSITION_TYPE_GROUP:
|
||||
if (adapter.isRowSavedStories(groupPosition)) break;
|
||||
if (currentState == StateFilter.SAVED) break;
|
||||
if (adapter.isRowReadStories(groupPosition)) break;
|
||||
if (groupPosition == FolderListAdapter.GLOBAL_SHARED_STORIES_GROUP_POSITION) break;
|
||||
if (groupPosition == FolderListAdapter.ALL_SHARED_STORIES_GROUP_POSITION) break;
|
||||
|
@ -250,12 +256,15 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
|||
|
||||
case ExpandableListView.PACKED_POSITION_TYPE_CHILD:
|
||||
if (adapter.isRowSavedStories(groupPosition)) break;
|
||||
if (currentState == StateFilter.SAVED) break;
|
||||
inflater.inflate(R.menu.context_feed, menu);
|
||||
if (groupPosition == FolderListAdapter.ALL_SHARED_STORIES_GROUP_POSITION) {
|
||||
menu.removeItem(R.id.menu_delete_feed);
|
||||
menu.removeItem(R.id.menu_choose_folders);
|
||||
menu.removeItem(R.id.menu_unmute_feed);
|
||||
menu.removeItem(R.id.menu_mute_feed);
|
||||
menu.removeItem(R.id.menu_notifications);
|
||||
menu.removeItem(R.id.menu_instafetch_feed);
|
||||
} else {
|
||||
menu.removeItem(R.id.menu_unfollow);
|
||||
|
||||
|
@ -264,6 +273,23 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
|||
menu.removeItem(R.id.menu_unmute_feed);
|
||||
} else {
|
||||
menu.removeItem(R.id.menu_mute_feed);
|
||||
menu.removeItem(R.id.menu_mark_feed_as_read);
|
||||
menu.removeItem(R.id.menu_notifications);
|
||||
menu.removeItem(R.id.menu_instafetch_feed);
|
||||
break;
|
||||
}
|
||||
if (feed.isNotifyUnread()) {
|
||||
menu.findItem(R.id.menu_notifications_disable).setChecked(false);
|
||||
menu.findItem(R.id.menu_notifications_unread).setChecked(true);
|
||||
menu.findItem(R.id.menu_notifications_focus).setChecked(false);
|
||||
} else if (feed.isNotifyFocus()) {
|
||||
menu.findItem(R.id.menu_notifications_disable).setChecked(false);
|
||||
menu.findItem(R.id.menu_notifications_unread).setChecked(false);
|
||||
menu.findItem(R.id.menu_notifications_focus).setChecked(true);
|
||||
} else {
|
||||
menu.findItem(R.id.menu_notifications_disable).setChecked(true);
|
||||
menu.findItem(R.id.menu_notifications_unread).setChecked(false);
|
||||
menu.findItem(R.id.menu_notifications_focus).setChecked(false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -272,6 +298,27 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
|||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.menu_notifications) {
|
||||
// this means the notifications menu has been opened, but this is our one chance to see the list position
|
||||
// and get the ID of the feed for which the menu was opened. (no packed pos when the submenu is tapped)
|
||||
ExpandableListView.ExpandableListContextMenuInfo info = (ExpandableListView.ExpandableListContextMenuInfo) item.getMenuInfo();
|
||||
int childPosition = ExpandableListView.getPackedPositionChild(info.packedPosition);
|
||||
int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition);
|
||||
lastMenuFeed = adapter.getFeed(groupPosition, childPosition);
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.menu_notifications_disable) {
|
||||
FeedUtils.disableNotifications(getActivity(), lastMenuFeed);
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.menu_notifications_focus) {
|
||||
FeedUtils.enableFocusNotifications(getActivity(), lastMenuFeed);
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.menu_notifications_unread) {
|
||||
FeedUtils.enableUnreadNotifications(getActivity(), lastMenuFeed);
|
||||
return true;
|
||||
}
|
||||
ExpandableListView.ExpandableListContextMenuInfo info = (ExpandableListView.ExpandableListContextMenuInfo) item.getMenuInfo();
|
||||
int childPosition = ExpandableListView.getPackedPositionChild(info.packedPosition);
|
||||
int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition);
|
||||
|
@ -309,6 +356,8 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
|||
FeedUtils.muteFeeds(getActivity(), adapter.getAllFeedsForFolder(groupPosition));
|
||||
} else if (item.getItemId() == R.id.menu_unmute_folder) {
|
||||
FeedUtils.unmuteFeeds(getActivity(), adapter.getAllFeedsForFolder(groupPosition));
|
||||
} else if (item.getItemId() == R.id.menu_instafetch_feed) {
|
||||
FeedUtils.instaFetchFeed(getActivity(), adapter.getFeed(groupPosition, childPosition).feedId);
|
||||
}
|
||||
|
||||
return super.onContextItemSelected(item);
|
||||
|
@ -316,6 +365,8 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
|||
|
||||
private void markFeedsAsRead(FeedSet fs) {
|
||||
FeedUtils.markRead(getActivity(), fs, null, null, R.array.mark_all_read_options, false);
|
||||
adapter.lastFeedViewedId = fs.getSingleFeed();
|
||||
adapter.lastFolderViewed = fs.getFolderName();
|
||||
}
|
||||
|
||||
public void changeState(StateFilter state) {
|
||||
|
@ -325,6 +376,26 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
|||
hasUpdated();
|
||||
}
|
||||
|
||||
public void clearRecents() {
|
||||
adapter.lastFeedViewedId = null;
|
||||
adapter.lastFolderViewed = null;
|
||||
}
|
||||
|
||||
public void forceShowFeed(String feedId) {
|
||||
adapter.lastFeedViewedId = feedId;
|
||||
adapter.lastFolderViewed = null;
|
||||
}
|
||||
|
||||
public void setSearchQuery(String q) {
|
||||
adapter.activeSearchQuery = q;
|
||||
adapter.forceRecount();
|
||||
checkOpenFolderPreferences();
|
||||
}
|
||||
|
||||
public String getSearchQuery() {
|
||||
return adapter.activeSearchQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every time unread counts are updated in the adapter, ping the Main activity with
|
||||
* the new data. It is, unfortunately, quite expensive to compute given the current
|
||||
|
@ -333,6 +404,7 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
|||
public void pushUnreadCounts() {
|
||||
((Main) getActivity()).updateUnreadCounts((adapter.totalNeutCount+adapter.totalSocialNeutCount), (adapter.totalPosCount+adapter.totalSocialPosiCount));
|
||||
((Main) getActivity()).updateFeedCount(adapter.lastFeedCount);
|
||||
com.newsblur.util.Log.d(this, "showing " + adapter.lastFeedCount + " feeds");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -358,6 +430,8 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
|||
i = new Intent(getActivity(), FolderItemsList.class);
|
||||
String canonicalFolderName = adapter.getGroupFolderName(groupPosition);
|
||||
i.putExtra(FolderItemsList.EXTRA_FOLDER_NAME, canonicalFolderName);
|
||||
adapter.lastFeedViewedId = null;
|
||||
adapter.lastFolderViewed = canonicalFolderName;
|
||||
}
|
||||
FeedSet fs = adapter.getGroup(groupPosition);
|
||||
i.putExtra(ItemsList.EXTRA_FEED_SET, fs);
|
||||
|
@ -428,6 +502,8 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
|||
intent.putExtra(FeedItemsList.EXTRA_FEED, feed);
|
||||
intent.putExtra(FeedItemsList.EXTRA_FOLDER_NAME, folderName);
|
||||
getActivity().startActivity(intent);
|
||||
adapter.lastFeedViewedId = feed.feedId;
|
||||
adapter.lastFolderViewed = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -52,7 +52,6 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
|
|||
protected ItemsList activity;
|
||||
@Bind(R.id.itemlistfragment_list) ListView itemList;
|
||||
protected StoryItemsAdapter adapter;
|
||||
protected DefaultFeedView defaultFeedView;
|
||||
private boolean cursorSeenYet = false;
|
||||
private boolean stopLoading = false;
|
||||
|
||||
|
@ -72,13 +71,22 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
|
|||
// flag indicating a gesture just occurred so we can ignore spurious story taps right after
|
||||
private boolean gestureDebounce = false;
|
||||
|
||||
// we have to de-dupe auto-mark-read-on-scroll actions
|
||||
private String lastAutoMarkHash = null;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
defaultFeedView = (DefaultFeedView)getArguments().getSerializable("defaultFeedView");
|
||||
activity = (ItemsList) getActivity();
|
||||
// tell the sync service to discard the reading session at the start of the next sync, just in case
|
||||
NBSyncService.resetReadingSession();
|
||||
|
||||
if (getFeedSet() == null) {
|
||||
com.newsblur.util.Log.w(this.getClass().getName(), "item list started without FeedSet.");
|
||||
activity.finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// warm up the sync service as soon as possible since it will init the story session DB
|
||||
triggerRefresh(1, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -145,6 +153,15 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
|
|||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
if ((adapter != null) && adapter.isStale()) {
|
||||
Log.e(this.getClass().getName(), "stale fragment loaded, falling back.");
|
||||
getActivity().finish();
|
||||
}
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
private void triggerRefresh(int desiredStoryCount, Integer totalSeen) {
|
||||
// ask the sync service for as many stories as we want
|
||||
boolean gotSome = NBSyncService.requestMoreForFeed(getFeedSet(), desiredStoryCount, totalSeen);
|
||||
|
@ -163,11 +180,7 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
|
|||
* Indicate that the DB was cleared.
|
||||
*/
|
||||
public void resetEmptyState() {
|
||||
// this is going to cause us to lose access to any previous cursor and the next one might be
|
||||
// stale, so wipe the listview. the adapter will be recreated in onLoadFinished as usual
|
||||
if (adapter != null) adapter.notifyDataSetInvalidated();
|
||||
if (itemList != null) itemList.setAdapter(null);
|
||||
adapter = null;
|
||||
cursorSeenYet = false;
|
||||
FeedUtils.dbHelper.clearStorySession();
|
||||
}
|
||||
|
@ -252,6 +265,17 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
|
|||
// load an extra page or two worth of stories past the viewport
|
||||
int desiredStoryCount = firstVisible + (visibleCount*2) + 1;
|
||||
triggerRefresh(desiredStoryCount, storiesSeen);
|
||||
|
||||
if ((storiesSeen > 0) &&
|
||||
(firstVisible > 0) &&
|
||||
PrefsUtils.isMarkReadOnScroll(getActivity())) {
|
||||
int topVisible = firstVisible - 1;
|
||||
Story story = adapter.getStory(topVisible);
|
||||
if (!story.storyHash.equals(lastAutoMarkHash)) {
|
||||
lastAutoMarkHash = story.storyHash;
|
||||
FeedUtils.markStoryAsRead(story, getActivity());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -291,16 +315,19 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
|
|||
public synchronized void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
|
||||
if (stopLoading) return;
|
||||
if (cursor != null) {
|
||||
if (NBSyncService.ResetSession) {
|
||||
if (! NBSyncService.isFeedSetReady(getFeedSet())) {
|
||||
// the DB hasn't caught up yet from the last story list; don't display stale stories.
|
||||
com.newsblur.util.Log.i(this.getClass().getName(), "discarding stale load");
|
||||
com.newsblur.util.Log.i(this.getClass().getName(), "stale load");
|
||||
adapter.setShowNone(true);
|
||||
setLoading(true);
|
||||
triggerRefresh(1, null);
|
||||
return;
|
||||
}
|
||||
cursorSeenYet = true;
|
||||
com.newsblur.util.Log.d(this.getClass().getName(), "loaded cursor with count: " + cursor.getCount());
|
||||
if (cursor.getCount() < 1) {
|
||||
triggerRefresh(1, 0);
|
||||
} else {
|
||||
cursorSeenYet = true;
|
||||
com.newsblur.util.Log.d(this.getClass().getName(), "loaded cursor with count: " + cursor.getCount());
|
||||
if (cursor.getCount() < 1) {
|
||||
triggerRefresh(1, 0);
|
||||
}
|
||||
adapter.setShowNone(false);
|
||||
}
|
||||
adapter.swapCursor(cursor);
|
||||
}
|
||||
|
@ -312,10 +339,6 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
|
|||
if (adapter != null) adapter.notifyDataSetInvalidated();
|
||||
}
|
||||
|
||||
public void setDefaultFeedView(DefaultFeedView value) {
|
||||
this.defaultFeedView = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||
// context menu like to get accidentally triggered by the ListView event handler right after
|
||||
|
@ -405,6 +428,7 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
|
|||
|
||||
int truePosition = position - 1;
|
||||
Story story = adapter.getStory(truePosition);
|
||||
if (story == null) return; // can happen on shrinking lists
|
||||
if (getActivity().isFinishing()) return;
|
||||
UIUtils.startReadingActivity(getFeedSet(), story.storyHash, getActivity());
|
||||
}
|
||||
|
@ -458,6 +482,7 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
|
|||
}
|
||||
if (index <= -1) return;
|
||||
Story story = adapter.getStory(index-1);
|
||||
if (story == null) return;
|
||||
switch (action) {
|
||||
case GEST_ACTION_MARKREAD:
|
||||
FeedUtils.markStoryAsRead(story, getActivity());;
|
||||
|
|