diff --git a/.gitignore b/.gitignore
index 54ca6b802..daefd998f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@ media/iphone/build
build/
.DS_Store
**/*.perspectivev*
+.vscode/*
data/
config/certificates
**/*.xcuserstate
@@ -71,3 +72,5 @@ clients/android/NewsBlur/build.gradle
clients/android/NewsBlur/gradle*
clients/android/NewsBlur/settings.gradle
/docker/volumes/*
+
+**/node_modules
diff --git a/__init__.py b/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/apps/analyzer/tasks.py b/apps/analyzer/tasks.py
index 489efc670..ff8c9516e 100644
--- a/apps/analyzer/tasks.py
+++ b/apps/analyzer/tasks.py
@@ -1,13 +1,12 @@
-from celery import Task
+from newsblur.celeryapp import app
from utils import log as logging
-class EmailPopularityQuery(Task):
+@app.task()
+def EmailPopularityQuery(pk):
+ from apps.analyzer.models import MPopularityQuery
+
+ query = MPopularityQuery.objects.get(pk=pk)
+ logging.debug(" -> ~BB~FCRunning popularity query: ~SB%s" % query)
+
+ query.send_email()
- def run(self, pk):
- from apps.analyzer.models import MPopularityQuery
-
- query = MPopularityQuery.objects.get(pk=pk)
- logging.debug(" -> ~BB~FCRunning popularity query: ~SB%s" % query)
-
- query.send_email()
-
diff --git a/apps/feed_import/tasks.py b/apps/feed_import/tasks.py
index 59033fed9..b7a641bdf 100644
--- a/apps/feed_import/tasks.py
+++ b/apps/feed_import/tasks.py
@@ -1,21 +1,20 @@
-from celery import Task
+from newsblur.celeryapp import app
from django.contrib.auth.models import User
from apps.feed_import.models import UploadedOPML, OPMLImporter
from apps.reader.models import UserSubscription
from utils import log as logging
-class ProcessOPML(Task):
+@app.task()
+def ProcessOPML(user_id):
+ user = User.objects.get(pk=user_id)
+ logging.user(user, "~FR~SBOPML upload (task) starting...")
+
+ opml = UploadedOPML.objects.filter(user_id=user_id).first()
+ opml_importer = OPMLImporter(opml.opml_file, user)
+ opml_importer.process()
- def run(self, user_id):
- user = User.objects.get(pk=user_id)
- logging.user(user, "~FR~SBOPML upload (task) starting...")
-
- opml = UploadedOPML.objects.filter(user_id=user_id).first()
- opml_importer = OPMLImporter(opml.opml_file, user)
- opml_importer.process()
-
- feed_count = UserSubscription.objects.filter(user=user).count()
- user.profile.send_upload_opml_finished_email(feed_count)
- logging.user(user, "~FR~SBOPML upload (task): ~SK%s~SN~SB~FR feeds" % (feed_count))
+ feed_count = UserSubscription.objects.filter(user=user).count()
+ user.profile.send_upload_opml_finished_email(feed_count)
+ logging.user(user, "~FR~SBOPML upload (task): ~SK%s~SN~SB~FR feeds" % (feed_count))
diff --git a/apps/newsletters/models.py b/apps/newsletters/models.py
index 1b8200c4a..b0c9280db 100644
--- a/apps/newsletters/models.py
+++ b/apps/newsletters/models.py
@@ -31,9 +31,26 @@ class EmailNewsletter:
return
usf.add_folder('', 'Newsletters')
+ # First look for the email address
try:
feed = Feed.objects.get(feed_address=feed_address)
+ except Feed.MultipleObjectsReturned:
+ feeds = Feed.objects.filter(feed_address=feed_address)[:1]
+ if feeds.count():
+ feed = feeds[0]
except Feed.DoesNotExist:
+ feed = None
+
+ # If not found, check among titles user has subscribed to
+ if not feed:
+ newsletter_subs = UserSubscription.objects.filter(user=user, feed__feed_address__contains="newsletter:").only('feed')
+ newsletter_feed_ids = [us.feed.pk for us in newsletter_subs]
+ feeds = Feed.objects.filter(feed_title__iexact=sender_name, pk__in=newsletter_feed_ids)
+ if feeds.count():
+ feed = feeds[0]
+
+ # Create a new feed if it doesn't exist by sender name or email
+ if not feed:
feed = Feed.objects.create(feed_address=feed_address,
feed_link='http://' + sender_domain,
feed_title=sender_name,
@@ -148,8 +165,8 @@ class EmailNewsletter:
return profile.user
- def _feed_address(self, user, sender):
- return 'newsletter:%s:%s' % (user.pk, sender)
+ def _feed_address(self, user, sender_email):
+ return 'newsletter:%s:%s' % (user.pk, sender_email)
def _split_sender(self, sender):
tokens = re.search('(.*?) <(.*?)@(.*?)>', sender)
diff --git a/apps/notifications/tasks.py b/apps/notifications/tasks.py
index dbc3e4db2..a487d5e0a 100644
--- a/apps/notifications/tasks.py
+++ b/apps/notifications/tasks.py
@@ -1,10 +1,9 @@
-from celery import Task
+from newsblur.celeryapp import app
from django.contrib.auth.models import User
from apps.notifications.models import MUserFeedNotification
from utils import log as logging
-class QueueNotifications(Task):
-
- def run(self, feed_id, new_stories):
- MUserFeedNotification.push_feed_notifications(feed_id, new_stories)
\ No newline at end of file
+@app.task()
+def QueueNotifications(feed_id, new_stories):
+ MUserFeedNotification.push_feed_notifications(feed_id, new_stories)
diff --git a/apps/profile/forms.py b/apps/profile/forms.py
index 6f9e7f12b..1244eb22a 100644
--- a/apps/profile/forms.py
+++ b/apps/profile/forms.py
@@ -9,8 +9,6 @@ from apps.profile.models import change_password, blank_authenticate, MGiftCode,
from apps.social.models import MSocialProfile
PLANS = [
- ("newsblur-premium-12", mark_safe("$12 / year ($1/month)")),
- ("newsblur-premium-24", mark_safe("$24 / year ($2/month)")),
("newsblur-premium-36", mark_safe("$36 / year ($3/month)")),
]
@@ -35,7 +33,7 @@ class StripePlusPaymentForm(StripePaymentForm):
email = forms.EmailField(widget=forms.TextInput(attrs=dict(maxlength=75)),
label='Email address',
required=False)
- plan = forms.ChoiceField(required=False, widget=HorizRadioRenderer,
+ plan = forms.ChoiceField(required=False, widget=forms.RadioSelect,
choices=PLANS, label='Plan')
diff --git a/apps/profile/models.py b/apps/profile/models.py
index c910a63ee..f5efeee5c 100644
--- a/apps/profile/models.py
+++ b/apps/profile/models.py
@@ -492,7 +492,7 @@ class Profile(models.Model):
return ipn[0].payer_email
- def activate_ios_premium(self, product_identifier, transaction_identifier, amount=36):
+ def activate_ios_premium(self, transaction_identifier=None, amount=36):
payments = PaymentHistory.objects.filter(user=self.user,
payment_identifier=transaction_identifier,
payment_date__gte=datetime.datetime.now()-datetime.timedelta(days=3))
@@ -512,7 +512,30 @@ class Profile(models.Model):
if not self.is_premium:
self.activate_premium()
- logging.user(self.user, "~FG~BBNew iOS premium subscription: $%s~FW" % product_identifier)
+ logging.user(self.user, "~FG~BBNew iOS premium subscription: $%s~FW" % amount)
+ return True
+
+ def activate_android_premium(self, order_id=None, amount=36):
+ payments = PaymentHistory.objects.filter(user=self.user,
+ payment_identifier=order_id,
+ payment_date__gte=datetime.datetime.now()-datetime.timedelta(days=3))
+ if len(payments):
+ # Already paid
+ logging.user(self.user, "~FG~BBAlready paid Android premium subscription: $%s~FW" % amount)
+ return False
+
+ PaymentHistory.objects.create(user=self.user,
+ payment_date=datetime.datetime.now(),
+ payment_amount=amount,
+ payment_provider='android-subscription',
+ payment_identifier=order_id)
+
+ self.setup_premium_history()
+
+ if not self.is_premium:
+ self.activate_premium()
+
+ logging.user(self.user, "~FG~BBNew Android premium subscription: $%s~FW" % amount)
return True
@classmethod
diff --git a/apps/profile/tasks.py b/apps/profile/tasks.py
index 035ddc103..5d20526a7 100644
--- a/apps/profile/tasks.py
+++ b/apps/profile/tasks.py
@@ -1,90 +1,76 @@
import datetime
-from celery import Task
+from newsblur.celeryapp import app
from apps.profile.models import Profile, RNewUserQueue
from utils import log as logging
from apps.reader.models import UserSubscription, UserSubscriptionFolders
from apps.social.models import MSocialServices, MActivity, MInteraction
-class EmailNewUser(Task):
-
- def run(self, user_id):
- user_profile = Profile.objects.get(user__pk=user_id)
- user_profile.send_new_user_email()
+@app.task(name="email-new-user")
+def EmailNewUser(user_id):
+ user_profile = Profile.objects.get(user__pk=user_id)
+ user_profile.send_new_user_email()
-class EmailNewPremium(Task):
-
- def run(self, user_id):
- user_profile = Profile.objects.get(user__pk=user_id)
- user_profile.send_new_premium_email()
+@app.task(name="email-new-premium")
+def EmailNewPremium(user_id):
+ user_profile = Profile.objects.get(user__pk=user_id)
+ user_profile.send_new_premium_email()
-class PremiumExpire(Task):
- name = 'premium-expire'
-
- def run(self, **kwargs):
- # Get expired but grace period users
- two_days_ago = datetime.datetime.now() - datetime.timedelta(days=2)
- thirty_days_ago = datetime.datetime.now() - datetime.timedelta(days=30)
- expired_profiles = Profile.objects.filter(is_premium=True,
- premium_expire__lte=two_days_ago,
- premium_expire__gt=thirty_days_ago)
- logging.debug(" ---> %s users have expired premiums, emailing grace..." % expired_profiles.count())
- for profile in expired_profiles:
- if profile.grace_period_email_sent():
- continue
- profile.setup_premium_history()
- if profile.premium_expire < two_days_ago:
- profile.send_premium_expire_grace_period_email()
-
- # Get fully expired users
- expired_profiles = Profile.objects.filter(is_premium=True,
- premium_expire__lte=thirty_days_ago)
- logging.debug(" ---> %s users have expired premiums, deactivating and emailing..." % expired_profiles.count())
- for profile in expired_profiles:
- profile.setup_premium_history()
- if profile.premium_expire < thirty_days_ago:
- profile.send_premium_expire_email()
- profile.deactivate_premium()
-
-
-class ActivateNextNewUser(Task):
- name = 'activate-next-new-user'
-
- def run(self):
- RNewUserQueue.activate_next()
-
-
-class CleanupUser(Task):
- name = 'cleanup-user'
-
- def run(self, user_id):
- UserSubscription.trim_user_read_stories(user_id)
- UserSubscription.verify_feeds_scheduled(user_id)
- Profile.count_all_feed_subscribers_for_user(user_id)
- MInteraction.trim(user_id)
- MActivity.trim(user_id)
- UserSubscriptionFolders.add_missing_feeds_for_user(user_id)
- UserSubscriptionFolders.compact_for_user(user_id)
- # UserSubscription.refresh_stale_feeds(user_id)
+@app.task(name="premium-expire")
+def PremiumExpire(**kwargs):
+ # Get expired but grace period users
+ two_days_ago = datetime.datetime.now() - datetime.timedelta(days=2)
+ thirty_days_ago = datetime.datetime.now() - datetime.timedelta(days=30)
+ expired_profiles = Profile.objects.filter(is_premium=True,
+ premium_expire__lte=two_days_ago,
+ premium_expire__gt=thirty_days_ago)
+ logging.debug(" ---> %s users have expired premiums, emailing grace..." % expired_profiles.count())
+ for profile in expired_profiles:
+ if profile.grace_period_email_sent():
+ continue
+ profile.setup_premium_history()
+ if profile.premium_expire < two_days_ago:
+ profile.send_premium_expire_grace_period_email()
- try:
- ss = MSocialServices.objects.get(user_id=user_id)
- except MSocialServices.DoesNotExist:
- logging.debug(" ---> ~FRCleaning up user, can't find social_services for user_id: ~SB%s" % user_id)
- return
- ss.sync_twitter_photo()
+ # Get fully expired users
+ expired_profiles = Profile.objects.filter(is_premium=True,
+ premium_expire__lte=thirty_days_ago)
+ logging.debug(" ---> %s users have expired premiums, deactivating and emailing..." % expired_profiles.count())
+ for profile in expired_profiles:
+ profile.setup_premium_history()
+ if profile.premium_expire < thirty_days_ago:
+ profile.send_premium_expire_email()
+ profile.deactivate_premium()
-class CleanSpam(Task):
- name = 'clean-spam'
+@app.task(name="activate-next-new-user")
+def ActivateNextNewUser():
+ RNewUserQueue.activate_next()
- def run(self, **kwargs):
- logging.debug(" ---> Finding spammers...")
- Profile.clear_dead_spammers(confirm=True)
+@app.task(name="cleanup-user")
+def CleanupUser(user_id):
+ UserSubscription.trim_user_read_stories(user_id)
+ UserSubscription.verify_feeds_scheduled(user_id)
+ Profile.count_all_feed_subscribers_for_user(user_id)
+ MInteraction.trim(user_id)
+ MActivity.trim(user_id)
+ UserSubscriptionFolders.add_missing_feeds_for_user(user_id)
+ UserSubscriptionFolders.compact_for_user(user_id)
+ # UserSubscription.refresh_stale_feeds(user_id)
+
+ try:
+ ss = MSocialServices.objects.get(user_id=user_id)
+ except MSocialServices.DoesNotExist:
+ logging.debug(" ---> ~FRCleaning up user, can't find social_services for user_id: ~SB%s" % user_id)
+ return
+ ss.sync_twitter_photo()
-class ReimportStripeHistory(Task):
- name = 'reimport-stripe-history'
+@app.task(name="clean-spam")
+def CleanSpam():
+ logging.debug(" ---> Finding spammers...")
+ Profile.clear_dead_spammers(confirm=True)
- def run(self, **kwargs):
- logging.debug(" ---> Reimporting Stripe history...")
- Profile.reimport_stripe_history(limit=10, days=1)
+@app.task(name="reimport-stripe-history")
+def ReimportStripeHistory():
+ logging.debug(" ---> Reimporting Stripe history...")
+ Profile.reimport_stripe_history(limit=10, days=1)
diff --git a/apps/profile/urls.py b/apps/profile/urls.py
index f6c530dc9..95910827f 100644
--- a/apps/profile/urls.py
+++ b/apps/profile/urls.py
@@ -22,6 +22,7 @@ urlpatterns = [
url(r'^never_expire_premium/?', views.never_expire_premium, name='profile-never-expire-premium'),
url(r'^upgrade_premium/?', views.upgrade_premium, name='profile-upgrade-premium'),
url(r'^save_ios_receipt/?', views.save_ios_receipt, name='save-ios-receipt'),
+ url(r'^save_android_receipt/?', views.save_android_receipt, name='save-android-receipt'),
url(r'^update_payment_history/?', views.update_payment_history, name='profile-update-payment-history'),
url(r'^delete_account/?', views.delete_account, name='profile-delete-account'),
url(r'^forgot_password_return/?', views.forgot_password_return, name='profile-forgot-password-return'),
@@ -30,4 +31,5 @@ urlpatterns = [
url(r'^delete_all_sites/?', views.delete_all_sites, name='profile-delete-all-sites'),
url(r'^email_optout/?', views.email_optout, name='profile-email-optout'),
url(r'^ios_subscription_status/?', views.ios_subscription_status, name='profile-ios-subscription-status'),
+ url(r'debug/?', views.trigger_error, name='trigger-error'),
]
diff --git a/apps/profile/views.py b/apps/profile/views.py
index 7d8cf1666..26c03eb54 100644
--- a/apps/profile/views.py
+++ b/apps/profile/views.py
@@ -98,7 +98,8 @@ def login(request):
return render(request, 'accounts/login.html', {
'form': form,
- 'next': request.POST.get('next', "")})
+ 'next': request.POST.get('next', "") or request.GET.get('next', "")
+ })
@csrf_exempt
def signup(request):
@@ -106,16 +107,17 @@ def signup(request):
recaptcha = request.POST.get('g-recaptcha-response', None)
recaptcha_error = None
- if not recaptcha:
- recaptcha_error = "Please hit the \"I'm not a robot\" button."
- else:
- response = requests.post('https://www.google.com/recaptcha/api/siteverify', {
- 'secret': settings.RECAPTCHA_SECRET_KEY,
- 'response': recaptcha,
- })
- result = response.json()
- if not result['success']:
- recaptcha_error = "Really, please hit the \"I'm not a robot\" button."
+ if settings.ENFORCE_SIGNUP_CAPTCHA:
+ if not recaptcha:
+ recaptcha_error = "Please hit the \"I'm not a robot\" button."
+ else:
+ response = requests.post('https://www.google.com/recaptcha/api/siteverify', {
+ 'secret': settings.RECAPTCHA_SECRET_KEY,
+ 'response': recaptcha,
+ })
+ result = response.json()
+ if not result['success']:
+ recaptcha_error = "Really, please hit the \"I'm not a robot\" button."
if request.method == "POST":
form = SignupForm(data=request.POST, prefix="signup")
@@ -331,7 +333,7 @@ def save_ios_receipt(request):
logging.user(request, "~BM~FBSaving iOS Receipt: %s %s" % (product_identifier, transaction_identifier))
- paid = request.user.profile.activate_ios_premium(product_identifier, transaction_identifier)
+ paid = request.user.profile.activate_ios_premium(transaction_identifier)
if paid:
logging.user(request, "~BM~FBSending iOS Receipt email: %s %s" % (product_identifier, transaction_identifier))
subject = "iOS Premium: %s (%s)" % (request.user.profile, product_identifier)
@@ -343,13 +345,32 @@ def save_ios_receipt(request):
return request.user.profile
+@ajax_login_required
+@json.json_view
+def save_android_receipt(request):
+ order_id = request.POST.get('order_id')
+ product_id = request.POST.get('product_id')
+
+ logging.user(request, "~BM~FBSaving Android Receipt: %s %s" % (product_id, order_id))
+
+ paid = request.user.profile.activate_android_premium(order_id)
+ if paid:
+ logging.user(request, "~BM~FBSending Android Receipt email: %s %s" % (product_id, order_id))
+ subject = "Android Premium: %s (%s)" % (request.user.profile, product_id)
+ message = """User: %s (%s) -- Email: %s, product: %s, order: %s""" % (request.user.username, request.user.pk, request.user.email, product_id, order_id)
+ mail_admins(subject, message, fail_silently=True)
+ else:
+ logging.user(request, "~BM~FBNot sending Android Receipt email, already paid: %s %s" % (product_id, order_id))
+
+
+ return request.user.profile
+
@login_required
def stripe_form(request):
user = request.user
success_updating = False
stripe.api_key = settings.STRIPE_SECRET
- plan = int(request.GET.get('plan', 2))
- plan = PLANS[plan-1][0]
+ plan = PLANS[0][0]
renew = is_true(request.GET.get('renew', False))
error = None
@@ -692,4 +713,9 @@ def ios_subscription_status(request):
return {
"code": 1
- }
\ No newline at end of file
+ }
+
+def trigger_error(request):
+ logging.user(request.user, "~BR~FW~SBTriggering divison by zero")
+ division_by_zero = 1 / 0
+ return HttpResponseRedirect(reverse('index'))
diff --git a/apps/push/views.py b/apps/push/views.py
index 4a0d407d5..b8f6e8418 100644
--- a/apps/push/views.py
+++ b/apps/push/views.py
@@ -52,11 +52,11 @@ def push_callback(request, push_id):
# XXX TODO: Optimize this by removing feedparser. It just needs to find out
# the hub_url or topic has changed. ElementTree could do it.
if random.random() < 0.1:
- parsed = feedparser.parse(request.raw_post_data)
+ parsed = feedparser.parse(request.body)
subscription.check_urls_against_pushed_data(parsed)
# Don't give fat ping, just fetch.
- # subscription.feed.queue_pushed_feed_xml(request.raw_post_data)
+ # subscription.feed.queue_pushed_feed_xml(request.body)
if subscription.feed.active_premium_subscribers >= 1:
subscription.feed.queue_pushed_feed_xml("Fetch me", latest_push_date_delta=latest_push_date_delta)
MFetchHistory.add(feed_id=subscription.feed_id,
diff --git a/apps/reader/forms.py b/apps/reader/forms.py
index deafa9810..5a0d74ddc 100644
--- a/apps/reader/forms.py
+++ b/apps/reader/forms.py
@@ -154,7 +154,8 @@ class SignupForm(forms.Form):
new_user = User(username=username)
new_user.set_password(password)
- new_user.is_active = False
+ if not getattr(settings, 'AUTO_ENABLE_NEW_USERS', True):
+ new_user.is_active = False
new_user.email = email
new_user.last_login = datetime.datetime.now()
new_user.save()
@@ -184,4 +185,4 @@ class FeatureForm(forms.Form):
feature = Feature(description=self.cleaned_data['description'],
date=datetime.datetime.utcnow() + datetime.timedelta(minutes=1))
feature.save()
- return feature
\ No newline at end of file
+ return feature
diff --git a/apps/reader/models.py b/apps/reader/models.py
index 4a76952cc..ba5d374cf 100644
--- a/apps/reader/models.py
+++ b/apps/reader/models.py
@@ -771,6 +771,9 @@ class UserSubscription(models.Model):
except pymongo.errors.OperationFailure as e:
stories_db = MStory.objects(story_hash__in=unread_story_hashes)[:100]
stories = Feed.format_stories(stories_db, self.feed_id)
+ except pymongo.errors.OperationFailure as e:
+ stories_db = MStory.objects(story_hash__in=unread_story_hashes)[:25]
+ stories = Feed.format_stories(stories_db, self.feed_id)
unread_stories = []
for story in stories:
@@ -1192,7 +1195,7 @@ class RUserStory:
redis_commands(read_story_key)
read_stories_list_key = 'lRS:%s' % user_id
- r.lrem(read_stories_list_key, story_hash)
+ r.lrem(read_stories_list_key, 1, story_hash)
if ps and username:
ps.publish(username, 'story:unread:%s' % story_hash)
@@ -1428,15 +1431,16 @@ class UserSubscriptionFolders(models.Model):
self.save()
if not multiples_found and deleted and commit_delete:
+ user_sub = None
try:
user_sub = UserSubscription.objects.get(user=self.user, feed=feed_id)
- except Feed.DoesNotExist:
+ except (Feed.DoesNotExist, UserSubscription.DoesNotExist):
duplicate_feed = DuplicateFeed.objects.filter(duplicate_feed_id=feed_id)
if duplicate_feed:
try:
user_sub = UserSubscription.objects.get(user=self.user,
feed=duplicate_feed[0].feed)
- except Feed.DoesNotExist:
+ except (Feed.DoesNotExist, UserSubscription.DoesNotExist):
return
if user_sub:
user_sub.delete()
diff --git a/apps/reader/tasks.py b/apps/reader/tasks.py
index a01a009a1..522c26211 100644
--- a/apps/reader/tasks.py
+++ b/apps/reader/tasks.py
@@ -1,46 +1,40 @@
import datetime
-from celery import Task
+from newsblur.celeryapp import app
from utils import log as logging
from django.contrib.auth.models import User
from django.conf import settings
from apps.reader.models import UserSubscription
from apps.social.models import MSocialSubscription
-
-class FreshenHomepage(Task):
- name = 'freshen-homepage'
-
- def run(self, **kwargs):
- day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1)
- user = User.objects.get(username=settings.HOMEPAGE_USERNAME)
- user.profile.last_seen_on = datetime.datetime.utcnow()
- user.profile.save()
+@app.task(name='freshen-homepage')
+def FreshenHomepage():
+ day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1)
+ user = User.objects.get(username=settings.HOMEPAGE_USERNAME)
+ user.profile.last_seen_on = datetime.datetime.utcnow()
+ user.profile.save()
+
+ usersubs = UserSubscription.objects.filter(user=user)
+ logging.debug(" ---> %s has %s feeds, freshening..." % (user.username, usersubs.count()))
+ for sub in usersubs:
+ sub.mark_read_date = day_ago
+ sub.needs_unread_recalc = True
+ sub.save()
+ sub.calculate_feed_scores(silent=True)
- usersubs = UserSubscription.objects.filter(user=user)
- logging.debug(" ---> %s has %s feeds, freshening..." % (user.username, usersubs.count()))
- for sub in usersubs:
- sub.mark_read_date = day_ago
- sub.needs_unread_recalc = True
- sub.save()
- sub.calculate_feed_scores(silent=True)
-
- socialsubs = MSocialSubscription.objects.filter(user_id=user.pk)
- logging.debug(" ---> %s has %s socialsubs, freshening..." % (user.username, socialsubs.count()))
- for sub in socialsubs:
- sub.mark_read_date = day_ago
- sub.needs_unread_recalc = True
- sub.save()
- sub.calculate_feed_scores(silent=True)
+ socialsubs = MSocialSubscription.objects.filter(user_id=user.pk)
+ logging.debug(" ---> %s has %s socialsubs, freshening..." % (user.username, socialsubs.count()))
+ for sub in socialsubs:
+ sub.mark_read_date = day_ago
+ sub.needs_unread_recalc = True
+ sub.save()
+ sub.calculate_feed_scores(silent=True)
-class CleanAnalytics(Task):
- name = 'clean-analytics'
- hard = 720*10
-
- def run(self, **kwargs):
- logging.debug(" ---> Cleaning analytics... %s feed fetches" % (
- settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.count(),
- ))
- day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1)
- settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.delete_many({
- "date": {"$lt": day_ago},
- })
+@app.task(name='clean_analytics', time_limit=720*10)
+def CleanAnalytics():
+ logging.debug(" ---> Cleaning analytics... %s feed fetches" % (
+ settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.count(),
+ ))
+ day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1)
+ settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.delete_many({
+ "date": {"$lt": day_ago},
+ })
diff --git a/apps/reader/views.py b/apps/reader/views.py
index fc275ce3a..4c2a0551b 100644
--- a/apps/reader/views.py
+++ b/apps/reader/views.py
@@ -559,9 +559,11 @@ def interactions_count(request):
@ajax_login_required
@json.json_view
def feed_unread_count(request):
+ get_post = getattr(request, request.method)
start = time.time()
user = request.user
- feed_ids = request.GET.getlist('feed_id') or request.GET.getlist('feed_id[]')
+ feed_ids = get_post.getlist('feed_id') or get_post.getlist('feed_id[]')
+
force = request.GET.get('force', False)
social_feed_ids = [feed_id for feed_id in feed_ids if 'social:' in feed_id]
feed_ids = list(set(feed_ids) - set(social_feed_ids))
@@ -1024,10 +1026,15 @@ def starred_story_hashes(request):
mstories = MStarredStory.objects(
user_id=user.pk
- ).only('story_hash', 'starred_date').order_by('-starred_date')
+ ).only('story_hash', 'starred_date', 'starred_updated').order_by('-starred_date')
if include_timestamps:
- story_hashes = [(s.story_hash, s.starred_date.strftime("%s")) for s in mstories]
+ story_hashes = []
+ for s in mstories:
+ date = s.starred_date
+ if s.starred_updated:
+ date = s.starred_updated
+ story_hashes.append((s.story_hash, date.strftime("%s")))
else:
story_hashes = [s.story_hash for s in mstories]
@@ -1315,28 +1322,32 @@ def load_read_stories(request):
@json.json_view
def load_river_stories__redis(request):
- limit = int(request.GET.get('limit', 12))
+ # get_post is request.REQUEST, since this endpoint needs to handle either
+ # GET or POST requests, since the parameters for this endpoint can be
+ # very long, at which point the max size of a GET url request is exceeded.
+ get_post = getattr(request, request.method)
+ limit = int(get_post.get('limit', 12))
start = time.time()
user = get_user(request)
message = None
- feed_ids = request.GET.getlist('feeds') or request.GET.getlist('feeds[]')
+ feed_ids = get_post.getlist('feeds') or get_post.getlist('feeds[]')
feed_ids = [int(feed_id) for feed_id in feed_ids if feed_id]
if not feed_ids:
- feed_ids = request.GET.getlist('f') or request.GET.getlist('f[]')
- feed_ids = [int(feed_id) for feed_id in request.GET.getlist('f') if feed_id]
- story_hashes = request.GET.getlist('h') or request.GET.getlist('h[]')
+ feed_ids = get_post.getlist('f') or get_post.getlist('f[]')
+ feed_ids = [int(feed_id) for feed_id in get_post.getlist('f') if feed_id]
+ story_hashes = get_post.getlist('h') or get_post.getlist('h[]')
story_hashes = story_hashes[:100]
original_feed_ids = list(feed_ids)
- page = int(request.GET.get('page', 1))
- order = request.GET.get('order', 'newest')
- read_filter = request.GET.get('read_filter', 'unread')
- query = request.GET.get('query', '').strip()
- include_hidden = is_true(request.GET.get('include_hidden', False))
- include_feeds = is_true(request.GET.get('include_feeds', False))
- initial_dashboard = is_true(request.GET.get('initial_dashboard', False))
- infrequent = is_true(request.GET.get('infrequent', False))
+ page = int(get_post.get('page', 1))
+ order = get_post.get('order', 'newest')
+ read_filter = get_post.get('read_filter', 'unread')
+ query = get_post.get('query', '').strip()
+ include_hidden = is_true(get_post.get('include_hidden', False))
+ include_feeds = is_true(get_post.get('include_feeds', False))
+ initial_dashboard = is_true(get_post.get('initial_dashboard', False))
+ infrequent = is_true(get_post.get('infrequent', False))
if infrequent:
- infrequent = request.GET.get('infrequent')
+ infrequent = get_post.get('infrequent')
now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone)
usersubs = []
code = 1
@@ -1567,9 +1578,9 @@ def complete_river(request):
@json.json_view
def unread_story_hashes__old(request):
user = get_user(request)
- feed_ids = request.POST.getlist('feed_id') or request.POST.getlist('feed_id[]')
+ feed_ids = request.GET.getlist('feed_id') or request.GET.getlist('feed_id[]')
feed_ids = [int(feed_id) for feed_id in feed_ids if feed_id]
- include_timestamps = is_true(request.POST.get('include_timestamps', False))
+ include_timestamps = is_true(request.GET.get('include_timestamps', False))
usersubs = {}
if not feed_ids:
@@ -2661,8 +2672,8 @@ def send_story_email(request):
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',
+ logging.user(request, '~BMSharing story by email to %s recipient%s (%s): ~FY~SB%s~SN~BM~FY/~SB%s' %
+ (len(to_addresses), '' if len(to_addresses) == 1 else 's', to_addresses,
story['story_title'][:50], feed and feed.feed_title[:50]))
return {'code': code, 'message': message}
diff --git a/apps/recommendations/views.py b/apps/recommendations/views.py
index b7bd277c0..ae9ac6065 100644
--- a/apps/recommendations/views.py
+++ b/apps/recommendations/views.py
@@ -110,4 +110,4 @@ def decline_feed(request):
recommended_feed.declined_date = datetime.datetime.now()
recommended_feed.save()
- return load_recommended_feed(request)
\ No newline at end of file
+ return load_recommended_feed(request)
diff --git a/apps/rss_feeds/icon_importer.py b/apps/rss_feeds/icon_importer.py
index d424a0567..e064152e3 100644
--- a/apps/rss_feeds/icon_importer.py
+++ b/apps/rss_feeds/icon_importer.py
@@ -215,7 +215,7 @@ class IconImporter(object):
url = self._url_from_html(content)
if not url:
try:
- content = requests.get(self.cleaned_feed_link).content
+ content = requests.get(self.cleaned_feed_link, timeout=10).content
url = self._url_from_html(content)
except (AttributeError, SocketError, requests.ConnectionError,
requests.models.MissingSchema, requests.sessions.InvalidSchema,
@@ -224,6 +224,7 @@ class IconImporter(object):
requests.models.ChunkedEncodingError,
requests.models.ContentDecodingError,
http.client.IncompleteRead,
+ requests.adapters.ReadTimeout,
LocationParseError, OpenSSLError, PyAsn1Error,
ValueError) as e:
logging.debug(" ---> ~SN~FRFailed~FY to fetch ~FGfeed icon~FY: %s" % e)
@@ -276,14 +277,12 @@ class IconImporter(object):
@timelimit(30)
def _1(url):
headers = {
- 'User-Agent': 'NewsBlur Favicon Fetcher - %s subscriber%s - %s '
- '(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) '
- 'AppleWebKit/534.48.3 (KHTML, like Gecko) Version/5.1 '
- 'Safari/534.48.3)' %
+ 'User-Agent': 'NewsBlur Favicon Fetcher - %s subscriber%s - %s %s' %
(
self.feed.num_subscribers,
's' if self.feed.num_subscribers != 1 else '',
- self.feed.permalink
+ self.feed.permalink,
+ self.feed.fake_user_agent,
),
'Connection': 'close',
'Accept': 'image/png,image/x-icon,image/*;q=0.9,*/*;q=0.8'
diff --git a/apps/rss_feeds/models.py b/apps/rss_feeds/models.py
index b0d5dc87d..8e5fd0848 100644
--- a/apps/rss_feeds/models.py
+++ b/apps/rss_feeds/models.py
@@ -409,6 +409,10 @@ class Feed(models.Model):
def favicon_fetching(self):
return bool(not (self.favicon_not_found or self.favicon_color))
+ @classmethod
+ def get_feed_by_url(self, *args, **kwargs):
+ return self.get_feed_from_url(*args, **kwargs)
+
@classmethod
def get_feed_from_url(cls, url, create=True, aggressive=False, fetch=True, offset=0, user=None, interactive=False):
feed = None
@@ -416,7 +420,10 @@ class Feed(models.Model):
original_url = url
if url and url.startswith('newsletter:'):
- return cls.objects.get(feed_address=url)
+ try:
+ return cls.objects.get(feed_address=url)
+ except cls.MultipleObjectsReturned:
+ return cls.objects.filter(feed_address=url)[0]
if url and re.match('(https?://)?twitter.com/\w+/?', url):
without_rss = True
if url and re.match(r'(https?://)?(www\.)?facebook.com/\w+/?$', url):
@@ -1116,20 +1123,20 @@ class Feed(models.Model):
# A known workaround is using facebook's user agent.
return 'facebookexternalhit/1.0 (+http://www.facebook.com/externalhit_uatext.php)'
- ua = ('NewsBlur Feed Fetcher - %s subscriber%s - %s '
- '(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) '
- 'AppleWebKit/537.36 (KHTML, like Gecko) '
- 'Chrome/56.0.2924.87 Safari/537.36)' % (
+ ua = ('NewsBlur Feed Fetcher - %s subscriber%s - %s %s' % (
self.num_subscribers,
's' if self.num_subscribers != 1 else '',
self.permalink,
+ self.fake_user_agent,
))
return ua
@property
def fake_user_agent(self):
- ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:49.0) Gecko/20100101 Firefox/49.0"
+ ua = ('("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '
+ 'AppleWebKit/605.1.15 (KHTML, like Gecko) '
+ 'Version/14.0.1 Safari/605.1.15")')
return ua
@@ -2796,7 +2803,17 @@ class MStory(mongo.Document):
if len(image_urls):
self.image_urls = [u for u in image_urls if u]
+ else:
+ return
+ max_length = MStory.image_urls.field.max_length
+ while len(''.join(self.image_urls)) > max_length:
+ if len(self.image_urls) <= 1:
+ self.image_urls[0] = self.image_urls[0][:max_length-1]
+ break
+ else:
+ self.image_urls.pop()
+
return self.image_urls
def fetch_original_text(self, force=False, request=None, debug=False):
@@ -2833,6 +2850,7 @@ class MStarredStory(mongo.DynamicDocument):
mongoengine's inheritance model on every single row."""
user_id = mongo.IntField(unique_with=('story_guid',))
starred_date = mongo.DateTimeField()
+ starred_updated = mongo.DateTimeField()
story_feed_id = mongo.IntField()
story_date = mongo.DateTimeField()
story_title = mongo.StringField(max_length=1024)
@@ -2879,7 +2897,8 @@ class MStarredStory(mongo.DynamicDocument):
self.story_original_content_z = zlib.compress(self.story_original_content)
self.story_original_content = None
self.story_hash = self.feed_guid_hash
-
+ self.starred_updated = datetime.datetime.now()
+
return super(MStarredStory, self).save(*args, **kwargs)
@classmethod
@@ -3010,6 +3029,11 @@ class MStarredStoryCounts(mongo.Document):
secret_token = user.profile.secret_token
slug = self.slug if self.slug else ""
+ if not self.slug and self.tag:
+ slug = slugify(self.tag)
+ self.slug = slug
+ self.save()
+
return "%s/reader/starred_rss/%s/%s/%s" % (settings.NEWSBLUR_URL, self.user_id,
secret_token, slug)
diff --git a/apps/rss_feeds/page_importer.py b/apps/rss_feeds/page_importer.py
index f5f0f5e3a..17a6d5683 100644
--- a/apps/rss_feeds/page_importer.py
+++ b/apps/rss_feeds/page_importer.py
@@ -51,13 +51,11 @@ class PageImporter(object):
@property
def headers(self):
return {
- 'User-Agent': 'NewsBlur Page Fetcher - %s subscriber%s - %s '
- '(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) '
- 'AppleWebKit/534.48.3 (KHTML, like Gecko) Version/5.1 '
- 'Safari/534.48.3)' % (
+ 'User-Agent': 'NewsBlur Page Fetcher - %s subscriber%s - %s %s' % (
self.feed.num_subscribers,
's' if self.feed.num_subscribers != 1 else '',
self.feed.permalink,
+ self.feed.fake_user_agent,
),
}
@@ -92,11 +90,12 @@ class PageImporter(object):
data = response.read()
else:
try:
- response = requests.get(feed_link, headers=self.headers)
+ response = requests.get(feed_link, headers=self.headers, timeout=10)
response.connection.close()
except requests.exceptions.TooManyRedirects:
- response = requests.get(feed_link)
- except (AttributeError, SocketError, OpenSSLError, PyAsn1Error, TypeError) as e:
+ response = requests.get(feed_link, timeout=10)
+ except (AttributeError, SocketError, OpenSSLError, PyAsn1Error, TypeError,
+ requests.adapters.ReadTimeout) as e:
logging.debug(' ***> [%-30s] Page fetch failed using requests: %s' % (self.feed.log_title[:30], e))
self.save_no_page()
return
@@ -186,12 +185,18 @@ class PageImporter(object):
return
try:
- response = requests.get(story_permalink, headers=self.headers)
+ response = requests.get(story_permalink, headers=self.headers, timeout=10)
response.connection.close()
- except (AttributeError, SocketError, OpenSSLError, PyAsn1Error, requests.exceptions.ConnectionError, requests.exceptions.TooManyRedirects) as e:
+ except (AttributeError, SocketError, OpenSSLError, PyAsn1Error,
+ requests.exceptions.ConnectionError,
+ requests.exceptions.TooManyRedirects,
+ requests.adapters.ReadTimeout) as e:
try:
- response = requests.get(story_permalink)
- except (AttributeError, SocketError, OpenSSLError, PyAsn1Error, requests.exceptions.ConnectionError, requests.exceptions.TooManyRedirects) as e:
+ response = requests.get(story_permalink, timeout=10)
+ except (AttributeError, SocketError, OpenSSLError, PyAsn1Error,
+ requests.exceptions.ConnectionError,
+ requests.exceptions.TooManyRedirects,
+ requests.adapters.ReadTimeout) as e:
logging.debug(' ***> [%-30s] Original story fetch failed using requests: %s' % (self.feed.log_title[:30], e))
return
try:
@@ -293,7 +298,8 @@ class PageImporter(object):
feed_page.page_data = zlib.compress(html)
feed_page.save()
except MFeedPage.DoesNotExist:
- feed_page = MFeedPage.objects.create(feed_id=self.feed.pk, page_data=html)
+ feed_page = MFeedPage.objects.create(feed_id=self.feed.pk,
+ page_data=zlib.compress(html))
return feed_page
def save_page_node(self, html):
diff --git a/apps/rss_feeds/tasks.py b/apps/rss_feeds/tasks.py
index 3d0ae6497..0d1243dad 100644
--- a/apps/rss_feeds/tasks.py
+++ b/apps/rss_feeds/tasks.py
@@ -3,7 +3,7 @@ import os
import shutil
import time
import redis
-from celery import Task
+from newsblur.celeryapp import app
from celery.exceptions import SoftTimeLimitExceeded
from utils import log as logging
from utils import s3_utils as s3
@@ -13,259 +13,230 @@ from utils.mongo_raw_log_middleware import MongoDumpMiddleware
from utils.redis_raw_log_middleware import RedisDumpMiddleware
FEED_TASKING_MAX = 10000
-class TaskFeeds(Task):
- name = 'task-feeds'
-
- def run(self, **kwargs):
- from apps.rss_feeds.models import Feed
- settings.LOG_TO_STREAM = True
- now = datetime.datetime.utcnow()
- start = time.time()
- r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
- tasked_feeds_size = r.zcard('tasked_feeds')
-
- hour_ago = now - datetime.timedelta(hours=1)
- r.zremrangebyscore('fetched_feeds_last_hour', 0, int(hour_ago.strftime('%s')))
-
- now_timestamp = int(now.strftime("%s"))
- queued_feeds = r.zrangebyscore('scheduled_updates', 0, now_timestamp)
- r.zremrangebyscore('scheduled_updates', 0, now_timestamp)
- if not queued_feeds:
- logging.debug(" ---> ~SN~FB~BMNo feeds to queue! Exiting...")
- return
-
- r.sadd('queued_feeds', *queued_feeds)
- logging.debug(" ---> ~SN~FBQueuing ~SB%s~SN stale feeds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
- len(queued_feeds),
- r.zcard('tasked_feeds'),
- r.scard('queued_feeds'),
- r.zcard('scheduled_updates')))
-
- # Regular feeds
- if tasked_feeds_size < FEED_TASKING_MAX:
- feeds = r.srandmember('queued_feeds', FEED_TASKING_MAX)
- Feed.task_feeds(feeds, verbose=True)
- active_count = len(feeds)
- else:
- logging.debug(" ---> ~SN~FBToo many tasked feeds. ~SB%s~SN tasked." % tasked_feeds_size)
- active_count = 0
-
- logging.debug(" ---> ~SN~FBTasking %s feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
- active_count,
- int((time.time() - start)),
- r.zcard('tasked_feeds'),
- r.scard('queued_feeds'),
- r.zcard('scheduled_updates')))
-
-class TaskBrokenFeeds(Task):
- name = 'task-broken-feeds'
- max_retries = 0
- ignore_result = True
+@app.task(name='task-feeds')
+def TaskFeeds():
+ from apps.rss_feeds.models import Feed
+ settings.LOG_TO_STREAM = True
+ now = datetime.datetime.utcnow()
+ start = time.time()
+ r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
+ tasked_feeds_size = r.zcard('tasked_feeds')
- def run(self, **kwargs):
- from apps.rss_feeds.models import Feed
- settings.LOG_TO_STREAM = True
- now = datetime.datetime.utcnow()
- start = time.time()
- r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
-
- logging.debug(" ---> ~SN~FBQueuing broken feeds...")
-
- # Force refresh feeds
- refresh_feeds = Feed.objects.filter(
- active=True,
- fetched_once=False,
- active_subscribers__gte=1
- ).order_by('?')[:100]
- refresh_count = refresh_feeds.count()
- cp1 = time.time()
-
- logging.debug(" ---> ~SN~FBFound %s active, unfetched broken feeds" % refresh_count)
-
- # Mistakenly inactive feeds
- hours_ago = (now - datetime.timedelta(minutes=10)).strftime('%s')
- old_tasked_feeds = r.zrangebyscore('tasked_feeds', 0, hours_ago)
- inactive_count = len(old_tasked_feeds)
- if inactive_count:
- r.zremrangebyscore('tasked_feeds', 0, hours_ago)
- # r.sadd('queued_feeds', *old_tasked_feeds)
- for feed_id in old_tasked_feeds:
- r.zincrby('error_feeds', 1, feed_id)
- feed = Feed.get_by_id(feed_id)
- feed.set_next_scheduled_update()
- logging.debug(" ---> ~SN~FBRe-queuing ~SB%s~SN dropped/broken feeds (~SB%s/%s~SN queued/tasked)" % (
- inactive_count,
- r.scard('queued_feeds'),
- r.zcard('tasked_feeds')))
- cp2 = time.time()
-
- old = now - datetime.timedelta(days=1)
- old_feeds = Feed.objects.filter(
- next_scheduled_update__lte=old,
- active_subscribers__gte=1
- ).order_by('?')[:500]
- old_count = old_feeds.count()
- cp3 = time.time()
-
- logging.debug(" ---> ~SN~FBTasking ~SBrefresh:~FC%s~FB inactive:~FC%s~FB old:~FC%s~SN~FB broken feeds... (%.4s/%.4s/%.4s)" % (
- refresh_count,
- inactive_count,
- old_count,
- cp1 - start,
- cp2 - cp1,
- cp3 - cp2,
- ))
-
- Feed.task_feeds(refresh_feeds, verbose=False)
- Feed.task_feeds(old_feeds, verbose=False)
-
- logging.debug(" ---> ~SN~FBTasking broken feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
- int((time.time() - start)),
- r.zcard('tasked_feeds'),
- r.scard('queued_feeds'),
- r.zcard('scheduled_updates')))
-
-class UpdateFeeds(Task):
- name = 'update-feeds'
- max_retries = 0
- ignore_result = True
- time_limit = 10*60
- soft_time_limit = 9*60
-
- def run(self, feed_pks, **kwargs):
- from apps.rss_feeds.models import Feed
- from apps.statistics.models import MStatistics
- r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
-
- mongodb_replication_lag = int(MStatistics.get('mongodb_replication_lag', 0))
- compute_scores = bool(mongodb_replication_lag < 10)
-
- profiler = DBProfilerMiddleware()
- profiler_activated = profiler.process_celery()
- if profiler_activated:
- mongo_middleware = MongoDumpMiddleware()
- mongo_middleware.process_celery(profiler)
- redis_middleware = RedisDumpMiddleware()
- redis_middleware.process_celery(profiler)
-
- options = {
- 'quick': float(MStatistics.get('quick_fetch', 0)),
- 'updates_off': MStatistics.get('updates_off', False),
- 'compute_scores': compute_scores,
- 'mongodb_replication_lag': mongodb_replication_lag,
- }
-
- if not isinstance(feed_pks, list):
- feed_pks = [feed_pks]
-
- for feed_pk in feed_pks:
- feed = Feed.get_by_id(feed_pk)
- if not feed or feed.pk != int(feed_pk):
- logging.info(" ---> ~FRRemoving feed_id %s from tasked_feeds queue, points to %s..." % (feed_pk, feed and feed.pk))
- r.zrem('tasked_feeds', feed_pk)
- if not feed:
- continue
- try:
- feed.update(**options)
- except SoftTimeLimitExceeded as e:
- feed.save_feed_history(505, 'Timeout', e)
- logging.info(" ---> [%-30s] ~BR~FWTime limit hit!~SB~FR Moving on to next feed..." % feed)
- if profiler_activated: profiler.process_celery_finished()
-
-class NewFeeds(Task):
- name = 'new-feeds'
- max_retries = 0
- ignore_result = True
- time_limit = 10*60
- soft_time_limit = 9*60
-
- def run(self, feed_pks, **kwargs):
- from apps.rss_feeds.models import Feed
- if not isinstance(feed_pks, list):
- feed_pks = [feed_pks]
-
- options = {}
- for feed_pk in feed_pks:
- feed = Feed.get_by_id(feed_pk)
- if not feed: continue
- feed.update(options=options)
-
-class PushFeeds(Task):
- name = 'push-feeds'
- max_retries = 0
- ignore_result = True
-
- def run(self, feed_id, xml, **kwargs):
- from apps.rss_feeds.models import Feed
- from apps.statistics.models import MStatistics
-
- mongodb_replication_lag = int(MStatistics.get('mongodb_replication_lag', 0))
- compute_scores = bool(mongodb_replication_lag < 60)
-
- options = {
- 'feed_xml': xml,
- 'compute_scores': compute_scores,
- 'mongodb_replication_lag': mongodb_replication_lag,
- }
- feed = Feed.get_by_id(feed_id)
- if feed:
- feed.update(options=options)
-
-class BackupMongo(Task):
- name = 'backup-mongo'
- max_retries = 0
- ignore_result = True
+ hour_ago = now - datetime.timedelta(hours=1)
+ r.zremrangebyscore('fetched_feeds_last_hour', 0, int(hour_ago.strftime('%s')))
- def run(self, **kwargs):
- COLLECTIONS = "classifier_tag classifier_author classifier_feed classifier_title userstories starred_stories shared_stories category category_site sent_emails social_profile social_subscription social_services statistics feedback"
+ now_timestamp = int(now.strftime("%s"))
+ queued_feeds = r.zrangebyscore('scheduled_updates', 0, now_timestamp)
+ r.zremrangebyscore('scheduled_updates', 0, now_timestamp)
+ if not queued_feeds:
+ logging.debug(" ---> ~SN~FB~BMNo feeds to queue! Exiting...")
+ return
+
+ r.sadd('queued_feeds', *queued_feeds)
+ logging.debug(" ---> ~SN~FBQueuing ~SB%s~SN stale feeds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
+ len(queued_feeds),
+ r.zcard('tasked_feeds'),
+ r.scard('queued_feeds'),
+ r.zcard('scheduled_updates')))
+
+ # Regular feeds
+ if tasked_feeds_size < FEED_TASKING_MAX:
+ feeds = r.srandmember('queued_feeds', FEED_TASKING_MAX)
+ Feed.task_feeds(feeds, verbose=True)
+ active_count = len(feeds)
+ else:
+ logging.debug(" ---> ~SN~FBToo many tasked feeds. ~SB%s~SN tasked." % tasked_feeds_size)
+ active_count = 0
+
+ logging.debug(" ---> ~SN~FBTasking %s feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
+ active_count,
+ int((time.time() - start)),
+ r.zcard('tasked_feeds'),
+ r.scard('queued_feeds'),
+ r.zcard('scheduled_updates')))
- date = time.strftime('%Y-%m-%d-%H-%M')
- collections = COLLECTIONS.split(' ')
- db_name = 'newsblur'
- dir_name = 'backup_mongo_%s' % date
- filename = '%s.tgz' % dir_name
+@app.task(name='task-broken-feeds')
+def TaskBrokenFeeds():
+ from apps.rss_feeds.models import Feed
+ settings.LOG_TO_STREAM = True
+ now = datetime.datetime.utcnow()
+ start = time.time()
+ r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
+
+ logging.debug(" ---> ~SN~FBQueuing broken feeds...")
+
+ # Force refresh feeds
+ refresh_feeds = Feed.objects.filter(
+ active=True,
+ fetched_once=False,
+ active_subscribers__gte=1
+ ).order_by('?')[:100]
+ refresh_count = refresh_feeds.count()
+ cp1 = time.time()
+
+ logging.debug(" ---> ~SN~FBFound %s active, unfetched broken feeds" % refresh_count)
- os.mkdir(dir_name)
+ # Mistakenly inactive feeds
+ hours_ago = (now - datetime.timedelta(minutes=10)).strftime('%s')
+ old_tasked_feeds = r.zrangebyscore('tasked_feeds', 0, hours_ago)
+ inactive_count = len(old_tasked_feeds)
+ if inactive_count:
+ r.zremrangebyscore('tasked_feeds', 0, hours_ago)
+ # r.sadd('queued_feeds', *old_tasked_feeds)
+ for feed_id in old_tasked_feeds:
+ r.zincrby('error_feeds', 1, feed_id)
+ feed = Feed.get_by_id(feed_id)
+ feed.set_next_scheduled_update()
+ logging.debug(" ---> ~SN~FBRe-queuing ~SB%s~SN dropped/broken feeds (~SB%s/%s~SN queued/tasked)" % (
+ inactive_count,
+ r.scard('queued_feeds'),
+ r.zcard('tasked_feeds')))
+ cp2 = time.time()
+
+ old = now - datetime.timedelta(days=1)
+ old_feeds = Feed.objects.filter(
+ next_scheduled_update__lte=old,
+ active_subscribers__gte=1
+ ).order_by('?')[:500]
+ old_count = old_feeds.count()
+ cp3 = time.time()
+
+ logging.debug(" ---> ~SN~FBTasking ~SBrefresh:~FC%s~FB inactive:~FC%s~FB old:~FC%s~SN~FB broken feeds... (%.4s/%.4s/%.4s)" % (
+ refresh_count,
+ inactive_count,
+ old_count,
+ cp1 - start,
+ cp2 - cp1,
+ cp3 - cp2,
+ ))
+
+ Feed.task_feeds(refresh_feeds, verbose=False)
+ Feed.task_feeds(old_feeds, verbose=False)
+
+ logging.debug(" ---> ~SN~FBTasking broken feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
+ int((time.time() - start)),
+ r.zcard('tasked_feeds'),
+ r.scard('queued_feeds'),
+ r.zcard('scheduled_updates')))
+
+@app.task(name='update-feeds', time_limit=10*60, soft_time_limit=9*60, ignore_result=True)
+def UpdateFeeds(feed_pks):
+ from apps.rss_feeds.models import Feed
+ from apps.statistics.models import MStatistics
+ r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
- for collection in collections:
- cmd = 'mongodump --db %s --collection %s -o %s' % (db_name, collection, dir_name)
- logging.debug(' ---> ~FMDumping ~SB%s~SN: %s' % (collection, cmd))
- os.system(cmd)
+ mongodb_replication_lag = int(MStatistics.get('mongodb_replication_lag', 0))
+ compute_scores = bool(mongodb_replication_lag < 10)
+
+ profiler = DBProfilerMiddleware()
+ profiler_activated = profiler.process_celery()
+ if profiler_activated:
+ mongo_middleware = MongoDumpMiddleware()
+ mongo_middleware.process_celery(profiler)
+ redis_middleware = RedisDumpMiddleware()
+ redis_middleware.process_celery(profiler)
+
+ options = {
+ 'quick': float(MStatistics.get('quick_fetch', 0)),
+ 'updates_off': MStatistics.get('updates_off', False),
+ 'compute_scores': compute_scores,
+ 'mongodb_replication_lag': mongodb_replication_lag,
+ }
+
+ if not isinstance(feed_pks, list):
+ feed_pks = [feed_pks]
+
+ for feed_pk in feed_pks:
+ feed = Feed.get_by_id(feed_pk)
+ if not feed or feed.pk != int(feed_pk):
+ logging.info(" ---> ~FRRemoving feed_id %s from tasked_feeds queue, points to %s..." % (feed_pk, feed and feed.pk))
+ r.zrem('tasked_feeds', feed_pk)
+ if not feed:
+ continue
+ try:
+ feed.update(**options)
+ except SoftTimeLimitExceeded as e:
+ feed.save_feed_history(505, 'Timeout', e)
+ logging.info(" ---> [%-30s] ~BR~FWTime limit hit!~SB~FR Moving on to next feed..." % feed)
+ if profiler_activated: profiler.process_celery_finished()
- cmd = 'tar -jcf %s %s' % (filename, dir_name)
+@app.task(name='new-feeds', time_limit=10*60, soft_time_limit=9*60, ignore_result=True)
+def NewFeeds(feed_pks):
+ from apps.rss_feeds.models import Feed
+ if not isinstance(feed_pks, list):
+ feed_pks = [feed_pks]
+
+ options = {}
+ for feed_pk in feed_pks:
+ feed = Feed.get_by_id(feed_pk)
+ if not feed: continue
+ feed.update(options=options)
+
+@app.task(name='push-feeds', ignore_result=True)
+def PushFeeds(feed_id, xml):
+ from apps.rss_feeds.models import Feed
+ from apps.statistics.models import MStatistics
+
+ mongodb_replication_lag = int(MStatistics.get('mongodb_replication_lag', 0))
+ compute_scores = bool(mongodb_replication_lag < 60)
+
+ options = {
+ 'feed_xml': xml,
+ 'compute_scores': compute_scores,
+ 'mongodb_replication_lag': mongodb_replication_lag,
+ }
+ feed = Feed.get_by_id(feed_id)
+ if feed:
+ feed.update(options=options)
+
+@app.task(name='backup-mongo', ignore_result=True)
+def BackupMongo():
+ COLLECTIONS = "classifier_tag classifier_author classifier_feed classifier_title userstories starred_stories shared_stories category category_site sent_emails social_profile social_subscription social_services statistics feedback"
+
+ date = time.strftime('%Y-%m-%d-%H-%M')
+ collections = COLLECTIONS.split(' ')
+ db_name = 'newsblur'
+ dir_name = 'backup_mongo_%s' % date
+ filename = '%s.tgz' % dir_name
+
+ os.mkdir(dir_name)
+
+ for collection in collections:
+ cmd = 'mongodump --db %s --collection %s -o %s' % (db_name, collection, dir_name)
+ logging.debug(' ---> ~FMDumping ~SB%s~SN: %s' % (collection, cmd))
os.system(cmd)
- logging.debug(' ---> ~FRUploading ~SB~FM%s~SN~FR to S3...' % filename)
- s3.save_file_in_s3(filename)
- shutil.rmtree(dir_name)
- os.remove(filename)
- logging.debug(' ---> ~FRFinished uploading ~SB~FM%s~SN~FR to S3.' % filename)
+ cmd = 'tar -jcf %s %s' % (filename, dir_name)
+ os.system(cmd)
+
+ logging.debug(' ---> ~FRUploading ~SB~FM%s~SN~FR to S3...' % filename)
+ s3.save_file_in_s3(filename)
+ shutil.rmtree(dir_name)
+ os.remove(filename)
+ logging.debug(' ---> ~FRFinished uploading ~SB~FM%s~SN~FR to S3.' % filename)
-class ScheduleImmediateFetches(Task):
+@app.task()
+def ScheduleImmediateFetches(feed_ids, user_id=None):
+ from apps.rss_feeds.models import Feed
- def run(self, feed_ids, user_id=None, **kwargs):
- from apps.rss_feeds.models import Feed
-
- if not isinstance(feed_ids, list):
- feed_ids = [feed_ids]
-
- Feed.schedule_feed_fetches_immediately(feed_ids, user_id=user_id)
+ if not isinstance(feed_ids, list):
+ feed_ids = [feed_ids]
+
+ Feed.schedule_feed_fetches_immediately(feed_ids, user_id=user_id)
-class SchedulePremiumSetup(Task):
+@app.task()
+def SchedulePremiumSetup(feed_ids):
+ from apps.rss_feeds.models import Feed
- def run(self, feed_ids, **kwargs):
- from apps.rss_feeds.models import Feed
-
- if not isinstance(feed_ids, list):
- feed_ids = [feed_ids]
-
- Feed.setup_feeds_for_premium_subscribers(feed_ids)
-
-class ScheduleCountTagsForUser(Task):
+ if not isinstance(feed_ids, list):
+ feed_ids = [feed_ids]
- def run(self, user_id):
- from apps.rss_feeds.models import MStarredStoryCounts
-
- MStarredStoryCounts.count_for_user(user_id)
+ Feed.setup_feeds_for_premium_subscribers(feed_ids)
+
+@app.task()
+def ScheduleCountTagsForUser(user_id):
+ from apps.rss_feeds.models import MStarredStoryCounts
+
+ MStarredStoryCounts.count_for_user(user_id)
diff --git a/apps/rss_feeds/text_importer.py b/apps/rss_feeds/text_importer.py
index 9d8399dee..414717da4 100644
--- a/apps/rss_feeds/text_importer.py
+++ b/apps/rss_feeds/text_importer.py
@@ -37,13 +37,11 @@ class TextImporter:
def headers(self):
num_subscribers = getattr(self.feed, 'num_subscribers', 0)
return {
- 'User-Agent': 'NewsBlur Content Fetcher - %s subscriber%s - %s '
- '(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) '
- 'AppleWebKit/534.48.3 (KHTML, like Gecko) Version/5.1 '
- 'Safari/534.48.3)' % (
+ 'User-Agent': 'NewsBlur Content Fetcher - %s subscriber%s - %s %s' % (
num_subscribers,
's' if num_subscribers != 1 else '',
- getattr(self.feed, 'permalink', '')
+ getattr(self.feed, 'permalink', ''),
+ getattr(self.feed, 'fake_user_agent', ''),
),
}
@@ -203,7 +201,7 @@ class TextImporter:
url = "https://www.newsblur.com/rss_feeds/original_text_fetcher?url=%s" % url
try:
- r = requests.get(url, headers=headers)
+ r = requests.get(url, headers=headers, verify=False, timeout=15)
r.connection.close()
except (AttributeError, SocketError, requests.ConnectionError,
requests.models.MissingSchema, requests.sessions.InvalidSchema,
@@ -211,6 +209,7 @@ class TextImporter:
requests.models.InvalidURL,
requests.models.ChunkedEncodingError,
requests.models.ContentDecodingError,
+ requests.adapters.ReadTimeout,
urllib3.exceptions.LocationValueError,
LocationParseError, OpenSSLError, PyAsn1Error) as e:
logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: %s" % e)
diff --git a/apps/rss_feeds/views.py b/apps/rss_feeds/views.py
index ead4f0ee2..5dec70f29 100644
--- a/apps/rss_feeds/views.py
+++ b/apps/rss_feeds/views.py
@@ -185,7 +185,7 @@ def load_feed_statistics_embedded(request, feed_id):
)
def assemble_statistics(user, feed_id):
- timezone = user.profile.timezone
+ user_timezone = user.profile.timezone
stats = dict()
feed = get_object_or_404(Feed, pk=feed_id)
feed.update_all_statistics()
@@ -201,7 +201,7 @@ def assemble_statistics(user, feed_id):
if feed.is_push:
try:
stats['push_expires'] = localtime_for_timezone(feed.push.lease_expires,
- timezone).strftime("%Y-%m-%d %H:%M:%S")
+ user_timezone).strftime("%Y-%m-%d %H:%M:%S")
except PushSubscription.DoesNotExist:
stats['push_expires'] = 'Missing push'
feed.is_push = False
@@ -233,7 +233,7 @@ def assemble_statistics(user, feed_id):
stats['story_count_history'] = story_count_history
# Rotate hours to match user's timezone offset
- localoffset = timezone.utcoffset(datetime.datetime.utcnow())
+ localoffset = user_timezone.utcoffset(datetime.datetime.utcnow())
hours_offset = int(localoffset.total_seconds() / 3600)
rotated_hours = {}
for hour, value in list(stats['story_hours_history'].items()):
@@ -253,7 +253,7 @@ def assemble_statistics(user, feed_id):
stats['classifier_counts'] = json.decode(feed.data.feed_classifier_counts)
# Fetch histories
- fetch_history = MFetchHistory.feed(feed_id, timezone=timezone)
+ fetch_history = MFetchHistory.feed(feed_id, timezone=user_timezone)
stats['feed_fetch_history'] = fetch_history['feed_fetch_history']
stats['page_fetch_history'] = fetch_history['page_fetch_history']
stats['feed_push_history'] = fetch_history['push_history']
@@ -515,11 +515,13 @@ def status(request):
@json.json_view
def original_text(request):
- story_id = request.GET.get('story_id')
- feed_id = request.GET.get('feed_id')
- story_hash = request.GET.get('story_hash', None)
- force = request.GET.get('force', False)
- debug = request.GET.get('debug', False)
+ # iOS sends a POST, web sends a GET
+ GET_POST = getattr(request, request.method)
+ story_id = GET_POST.get('story_id')
+ feed_id = GET_POST.get('feed_id')
+ story_hash = GET_POST.get('story_hash', None)
+ force = GET_POST.get('force', False)
+ debug = GET_POST.get('debug', False)
if story_hash:
story, _ = MStory.find_story(story_hash=story_hash)
@@ -542,7 +544,7 @@ def original_text(request):
'failed': not original_text or len(original_text) < 100,
}
-@required_params('story_hash')
+@required_params('story_hash', method="GET")
def original_story(request):
story_hash = request.GET.get('story_hash')
force = request.GET.get('force', False)
@@ -559,7 +561,7 @@ def original_story(request):
return HttpResponse(original_page or "")
-@required_params('story_hash')
+@required_params('story_hash', method="GET")
@json.json_view
def story_changes(request):
story_hash = request.GET.get('story_hash', None)
diff --git a/apps/search/models.py b/apps/search/models.py
index 6baa9c449..7ff3921cf 100644
--- a/apps/search/models.py
+++ b/apps/search/models.py
@@ -78,7 +78,7 @@ class MUserSearch(mongo.Document):
logging.user(user, "~FCIndexing ~SB%s feeds~SN in %s chunks..." %
(total, len(feed_id_chunks)))
- tasks = [IndexSubscriptionsChunkForSearch().s(feed_ids=feed_id_chunk,
+ tasks = [IndexSubscriptionsChunkForSearch.s(feed_ids=feed_id_chunk,
user_id=self.user_id
).set(queue='search_indexer')
for feed_id_chunk in feed_id_chunks]
diff --git a/apps/search/tasks.py b/apps/search/tasks.py
index b0c3b6305..2fb7df6c0 100644
--- a/apps/search/tasks.py
+++ b/apps/search/tasks.py
@@ -1,26 +1,23 @@
-from celery import Task
+from newsblur.celeryapp import app
+from utils import log as logging
-class IndexSubscriptionsForSearch(Task):
+@app.task()
+def IndexSubscriptionsForSearch(user_id):
+ from apps.search.models import MUserSearch
- def run(self, user_id):
- from apps.search.models import MUserSearch
-
- user_search = MUserSearch.get_user(user_id)
- user_search.index_subscriptions_for_search()
+ user_search = MUserSearch.get_user(user_id)
+ user_search.index_subscriptions_for_search()
-class IndexSubscriptionsChunkForSearch(Task):
+@app.task()
+def IndexSubscriptionsChunkForSearch(feed_ids, user_id):
+ logging.debug(" ---> Indexing: %s for %s" % (feed_ids, user_id))
+ from apps.search.models import MUserSearch
- ignore_result = False
-
- def run(self, feed_ids, user_id):
- from apps.search.models import MUserSearch
-
- user_search = MUserSearch.get_user(user_id)
- user_search.index_subscriptions_chunk_for_search(feed_ids)
+ user_search = MUserSearch.get_user(user_id)
+ user_search.index_subscriptions_chunk_for_search(feed_ids)
-class IndexFeedsForSearch(Task):
+@app.task()
+def IndexFeedsForSearch(feed_ids, user_id):
+ from apps.search.models import MUserSearch
- def run(self, feed_ids, user_id):
- from apps.search.models import MUserSearch
-
- MUserSearch.index_feeds_for_search(feed_ids, user_id)
\ No newline at end of file
+ MUserSearch.index_feeds_for_search(feed_ids, user_id)
diff --git a/apps/social/models.py b/apps/social/models.py
index 06bdd8abe..59b3edc55 100644
--- a/apps/social/models.py
+++ b/apps/social/models.py
@@ -2304,6 +2304,13 @@ class MSharedStory(mongo.DynamicDocument):
image_sources = [img.get('src') for img in soup.findAll('img') if img and img.get('src')]
if len(image_sources) > 0:
self.image_urls = image_sources
+ max_length = MSharedStory.image_urls.field.max_length
+ while len(''.join(self.image_urls)) > max_length:
+ if len(self.image_urls) <= 1:
+ self.image_urls[0] = self.image_urls[0][:max_length-1]
+ break
+ else:
+ self.image_urls.pop()
self.save()
def calculate_image_sizes(self, force=False):
@@ -2314,10 +2321,7 @@ class MSharedStory(mongo.DynamicDocument):
return self.image_sizes
headers = {
- 'User-Agent': 'NewsBlur Image Fetcher - %s '
- '(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) '
- 'AppleWebKit/534.48.3 (KHTML, like Gecko) Version/5.1 '
- 'Safari/534.48.3)' % (
+ 'User-Agent': 'NewsBlur Image Fetcher - %s' % (
settings.NEWSBLUR_URL
),
}
@@ -2328,7 +2332,7 @@ class MSharedStory(mongo.DynamicDocument):
for image_source in self.image_urls[:10]:
if any(ignore in image_source for ignore in IGNORE_IMAGE_SOURCES):
continue
- req = requests.get(image_source, headers=headers, stream=True)
+ req = requests.get(image_source, headers=headers, stream=True, timeout=10)
try:
datastream = BytesIO(req.content)
width, height = ImageOps.image_size(datastream)
@@ -2713,7 +2717,7 @@ class MSocialServices(mongo.Document):
os.remove(filename)
else:
api.update_status(status=message)
- except tweepy.TweepError as e:
+ except (tweepy.TweepError, requests.adapters.ReadError) as e:
user = User.objects.get(pk=self.user_id)
logging.user(user, "~FRTwitter error: ~SB%s" % e)
return
@@ -2728,7 +2732,7 @@ class MSocialServices(mongo.Document):
url = shared_story.image_urls[0]
image_filename = os.path.basename(urllib.parse.urlparse(url).path)
- req = requests.get(url, stream=True)
+ req = requests.get(url, stream=True, timeout=10)
filename = "/tmp/%s-%s" % (shared_story.story_hash, image_filename)
if req.status_code == 200:
diff --git a/apps/social/tasks.py b/apps/social/tasks.py
index 5afd6105a..fc619a3f9 100644
--- a/apps/social/tasks.py
+++ b/apps/social/tasks.py
@@ -1,92 +1,79 @@
from bson.objectid import ObjectId
-from celery import Task
+from newsblur.celeryapp import app
from apps.social.models import MSharedStory, MSocialProfile, MSocialServices, MSocialSubscription
from django.contrib.auth.models import User
from utils import log as logging
-class PostToService(Task):
-
- def run(self, shared_story_id, service):
- try:
- shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
- shared_story.post_to_service(service)
- except MSharedStory.DoesNotExist:
- logging.debug(" ---> Shared story not found (%s). Can't post to: %s" % (shared_story_id, service))
+@app.task()
+def PostToService(shared_story_id, service):
+ try:
+ shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
+ shared_story.post_to_service(service)
+ except MSharedStory.DoesNotExist:
+ logging.debug(" ---> Shared story not found (%s). Can't post to: %s" % (shared_story_id, service))
-class EmailNewFollower(Task):
-
- def run(self, follower_user_id, followee_user_id):
- user_profile = MSocialProfile.get_user(followee_user_id)
- user_profile.send_email_for_new_follower(follower_user_id)
+@app.task()
+def EmailNewFollower(follower_user_id, followee_user_id):
+ user_profile = MSocialProfile.get_user(followee_user_id)
+ user_profile.send_email_for_new_follower(follower_user_id)
-class EmailFollowRequest(Task):
-
- def run(self, follower_user_id, followee_user_id):
- user_profile = MSocialProfile.get_user(followee_user_id)
- user_profile.send_email_for_follow_request(follower_user_id)
+@app.task()
+def EmailFollowRequest(follower_user_id, followee_user_id):
+ user_profile = MSocialProfile.get_user(followee_user_id)
+ user_profile.send_email_for_follow_request(follower_user_id)
-class EmailFirstShare(Task):
-
- def run(self, user_id):
- user = User.objects.get(pk=user_id)
- user.profile.send_first_share_to_blurblog_email()
+@app.task()
+def EmailFirstShare(user_id):
+ user = User.objects.get(pk=user_id)
+ user.profile.send_first_share_to_blurblog_email()
-class EmailCommentReplies(Task):
-
- def run(self, shared_story_id, reply_id):
- shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
- shared_story.send_emails_for_new_reply(ObjectId(reply_id))
+@app.task()
+def EmailCommentReplies(shared_story_id, reply_id):
+ shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
+ shared_story.send_emails_for_new_reply(ObjectId(reply_id))
-class EmailStoryReshares(Task):
-
- def run(self, shared_story_id):
- shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
- shared_story.send_email_for_reshare()
+@app.task()
+def EmailStoryReshares(shared_story_id):
+ shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
+ shared_story.send_email_for_reshare()
-class SyncTwitterFriends(Task):
-
- def run(self, user_id):
- social_services = MSocialServices.objects.get(user_id=user_id)
- social_services.sync_twitter_friends()
+@app.task()
+def SyncTwitterFriends(user_id):
+ social_services = MSocialServices.objects.get(user_id=user_id)
+ social_services.sync_twitter_friends()
-class SyncFacebookFriends(Task):
-
- def run(self, user_id):
- social_services = MSocialServices.objects.get(user_id=user_id)
- social_services.sync_facebook_friends()
+@app.task()
+def SyncFacebookFriends(user_id):
+ social_services = MSocialServices.objects.get(user_id=user_id)
+ social_services.sync_facebook_friends()
-class SharePopularStories(Task):
- name = 'share-popular-stories'
-
- def run(self, **kwargs):
- logging.debug(" ---> Sharing popular stories...")
- MSharedStory.share_popular_stories(interactive=False)
+@app.task(name="share-popular-stories")
+def SharePopularStories():
+ logging.debug(" ---> Sharing popular stories...")
+ MSharedStory.share_popular_stories(interactive=False)
-class CleanSocialSpam(Task):
- name = 'clean-social-spam'
-
- def run(self, **kwargs):
- logging.debug(" ---> Finding social spammers...")
- MSharedStory.count_potential_spammers(destroy=True)
+@app.task(name='clean-social-spam')
+def CleanSocialSpam():
+ logging.debug(" ---> Finding social spammers...")
+ MSharedStory.count_potential_spammers(destroy=True)
-class UpdateRecalcForSubscription(Task):
+@app.task()
+def UpdateRecalcForSubscription(subscription_user_id, shared_story_id):
+ user = User.objects.get(pk=subscription_user_id)
+ socialsubs = MSocialSubscription.objects.filter(subscription_user_id=subscription_user_id)
+ try:
+ shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
+ except MSharedStory.DoesNotExist:
+ return
- def run(self, subscription_user_id, shared_story_id):
- user = User.objects.get(pk=subscription_user_id)
- socialsubs = MSocialSubscription.objects.filter(subscription_user_id=subscription_user_id)
- try:
- shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
- except MSharedStory.DoesNotExist:
- return
-
- logging.debug(" ---> ~FM~SNFlipping unread recalc for ~SB%s~SN subscriptions to ~SB%s's blurblog~SN" % (
- socialsubs.count(),
- user.username
- ))
- for socialsub in socialsubs:
- socialsub.needs_unread_recalc = True
- socialsub.save()
-
- shared_story.publish_update_to_subscribers()
+ logging.debug(" ---> ~FM~SNFlipping unread recalc for ~SB%s~SN subscriptions to ~SB%s's blurblog~SN" % (
+ socialsubs.count(),
+ user.username
+ ))
+ for socialsub in socialsubs:
+ socialsub.needs_unread_recalc = True
+ socialsub.save()
+
+ shared_story.publish_update_to_subscribers()
diff --git a/apps/social/views.py b/apps/social/views.py
index 48a20f293..27acff1f6 100644
--- a/apps/social/views.py
+++ b/apps/social/views.py
@@ -507,7 +507,7 @@ def load_social_page(request, user_id, username=None, **kwargs):
return render(request, template, params)
-@required_params('story_id', feed_id=int)
+@required_params('story_id', feed_id=int, method="GET")
def story_public_comments(request):
format = request.GET.get('format', 'json')
relative_user_id = request.GET.get('user_id', None)
@@ -1175,7 +1175,7 @@ def ignore_follower(request):
return {'code': code}
-@required_params('query')
+@required_params('query', method="GET")
@json.json_view
def find_friends(request):
query = request.GET['query']
@@ -1372,7 +1372,7 @@ def shared_stories_rss_feed(request, user_id, username):
))
return HttpResponse(rss.writeString('utf-8'), content_type='application/rss+xml')
-@required_params('user_id')
+@required_params('user_id', method="GET")
@json.json_view
def social_feed_trainer(request):
social_user_id = request.GET['user_id']
diff --git a/apps/static/views.py b/apps/static/views.py
index 3f451e4b7..8c2d96fa5 100644
--- a/apps/static/views.py
+++ b/apps/static/views.py
@@ -15,7 +15,7 @@ def faq(request):
return render(request, 'static/faq.xhtml')
def api(request):
- filename = settings.TEMPLATE_DIRS[0] + '/static/api.yml'
+ filename = settings.TEMPLATES[0]['DIRS'][0] + '/static/api.yml'
api_yml_file = open(filename).read()
data = yaml.load(api_yml_file)
diff --git a/apps/statistics/tasks.py b/apps/statistics/tasks.py
index fb6bf8ac1..ea86d38d4 100644
--- a/apps/statistics/tasks.py
+++ b/apps/statistics/tasks.py
@@ -1,21 +1,17 @@
-from celery import Task
+from newsblur.celeryapp import app
from apps.statistics.models import MStatistics
from apps.statistics.models import MFeedback
-# from utils import log as logging
+from utils import log as logging
-class CollectStats(Task):
- name = 'collect-stats'
-
- def run(self, **kwargs):
- # logging.debug(" ---> ~FBCollecting stats...")
- MStatistics.collect_statistics()
+@app.task(name='collect-stats')
+def CollectStats():
+ logging.debug(" ---> ~FBCollecting stats...")
+ MStatistics.collect_statistics()
-class CollectFeedback(Task):
- name = 'collect-feedback'
-
- def run(self, **kwargs):
- # logging.debug(" ---> ~FBCollecting feedback...")
- MFeedback.collect_feedback()
\ No newline at end of file
+@app.task(name='collect-feedback')
+def CollectFeedback():
+ logging.debug(" ---> ~FBCollecting feedback...")
+ MFeedback.collect_feedback()
diff --git a/clients/android/NewsBlur/AndroidManifest.xml b/clients/android/NewsBlur/AndroidManifest.xml
index 515898b5a..fc63f0059 100644
--- a/clients/android/NewsBlur/AndroidManifest.xml
+++ b/clients/android/NewsBlur/AndroidManifest.xml
@@ -128,6 +128,13 @@
+
+
+
+
@@ -174,7 +181,7 @@
android:exported="false">
diff --git a/clients/android/NewsBlur/build.gradle b/clients/android/NewsBlur/build.gradle
index a18fbd2ce..b48d5402c 100644
--- a/clients/android/NewsBlur/build.gradle
+++ b/clients/android/NewsBlur/build.gradle
@@ -1,4 +1,5 @@
buildscript {
+ ext.kotlin_version = '1.4.10'
repositories {
mavenCentral()
maven {
@@ -8,7 +9,8 @@ buildscript {
google()
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.0.0'
+ classpath 'com.android.tools.build:gradle:4.1.0'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@@ -18,33 +20,40 @@ repositories {
url 'https://maven.google.com'
}
jcenter()
+ google()
}
apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
apply plugin: 'checkstyle'
dependencies {
- implementation 'com.android.support:support-core-utils:28.0.0'
- implementation 'com.android.support:support-fragment:28.0.0'
- implementation 'com.android.support:support-core-ui:28.0.0'
- implementation 'com.squareup.okhttp3:okhttp:3.12.12'
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'androidx.fragment:fragment-ktx:1.2.5'
+ implementation 'androidx.recyclerview:recyclerview:1.1.0'
+ implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
+ implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.google.code.gson:gson:2.8.6'
- implementation 'com.android.support:recyclerview-v7:28.0.0'
+ implementation 'com.android.billingclient:billing:3.0.1'
+ implementation 'nl.dionsegijn:konfetti:1.2.2'
+ implementation 'com.github.jinatonic.confetti:confetti:1.1.2'
+ implementation 'com.google.android.play:core:1.8.3'
}
android {
- compileSdkVersion 28
+ compileSdkVersion 29
defaultConfig {
applicationId "com.newsblur"
minSdkVersion 21
- targetSdkVersion 28
- versionCode 168
- versionName "10.1"
+ targetSdkVersion 29
+ versionCode 177
+ versionName "10.1.1"
}
compileOptions.with {
- sourceCompatibility = JavaVersion.VERSION_1_7
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
}
- viewBinding.enabled = true
+ android.buildFeatures.viewBinding = true
sourceSets {
main {
diff --git a/clients/android/NewsBlur/gradle.properties b/clients/android/NewsBlur/gradle.properties
new file mode 100644
index 000000000..5465fec0e
--- /dev/null
+++ b/clients/android/NewsBlur/gradle.properties
@@ -0,0 +1,2 @@
+android.enableJetifier=true
+android.useAndroidX=true
\ No newline at end of file
diff --git a/clients/android/NewsBlur/release/output.json b/clients/android/NewsBlur/release/output.json
new file mode 100644
index 000000000..64ced0efd
--- /dev/null
+++ b/clients/android/NewsBlur/release/output.json
@@ -0,0 +1,20 @@
+{
+ "version": 1,
+ "artifactType": {
+ "type": "APK",
+ "kind": "Directory"
+ },
+ "applicationId": "com.newsblur",
+ "variantName": "release",
+ "elements": [
+ {
+ "type": "SINGLE",
+ "filters": [],
+ "properties": [],
+ "versionCode": 171,
+ "versionName": "171",
+ "enabled": true,
+ "outputFile": "NewsBlur-release.apk"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/clients/android/NewsBlur/res/drawable-nodpi/dark_feed_background_highlight.xml b/clients/android/NewsBlur/res/drawable-nodpi/dark_feed_background_highlight.xml
index 27084309e..456289ccd 100644
--- a/clients/android/NewsBlur/res/drawable-nodpi/dark_feed_background_highlight.xml
+++ b/clients/android/NewsBlur/res/drawable-nodpi/dark_feed_background_highlight.xml
@@ -9,8 +9,7 @@
-
-
+
diff --git a/clients/android/NewsBlur/res/drawable-nodpi/feed_background_highlight.xml b/clients/android/NewsBlur/res/drawable-nodpi/feed_background_highlight.xml
index f52202068..a85904fca 100644
--- a/clients/android/NewsBlur/res/drawable-nodpi/feed_background_highlight.xml
+++ b/clients/android/NewsBlur/res/drawable-nodpi/feed_background_highlight.xml
@@ -9,8 +9,7 @@
-
-
+
diff --git a/clients/android/NewsBlur/res/drawable/ic_bookmark.xml b/clients/android/NewsBlur/res/drawable/ic_bookmark.xml
new file mode 100644
index 000000000..2e1dfadc7
--- /dev/null
+++ b/clients/android/NewsBlur/res/drawable/ic_bookmark.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/clients/android/NewsBlur/res/drawable/ic_dining.xml b/clients/android/NewsBlur/res/drawable/ic_dining.xml
new file mode 100644
index 000000000..6793ae0f9
--- /dev/null
+++ b/clients/android/NewsBlur/res/drawable/ic_dining.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/clients/android/NewsBlur/res/drawable/ic_folder.xml b/clients/android/NewsBlur/res/drawable/ic_folder.xml
new file mode 100644
index 000000000..721dec314
--- /dev/null
+++ b/clients/android/NewsBlur/res/drawable/ic_folder.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/clients/android/NewsBlur/res/drawable/ic_hand_pointing_right.xml b/clients/android/NewsBlur/res/drawable/ic_hand_pointing_right.xml
new file mode 100644
index 000000000..3463f37a2
--- /dev/null
+++ b/clients/android/NewsBlur/res/drawable/ic_hand_pointing_right.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/clients/android/NewsBlur/res/drawable/ic_lock.xml b/clients/android/NewsBlur/res/drawable/ic_lock.xml
new file mode 100644
index 000000000..6c3cc5598
--- /dev/null
+++ b/clients/android/NewsBlur/res/drawable/ic_lock.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/clients/android/NewsBlur/res/drawable/ic_rss_feed.xml b/clients/android/NewsBlur/res/drawable/ic_rss_feed.xml
new file mode 100644
index 000000000..88af5ee32
--- /dev/null
+++ b/clients/android/NewsBlur/res/drawable/ic_rss_feed.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/clients/android/NewsBlur/res/drawable/ic_search.xml b/clients/android/NewsBlur/res/drawable/ic_search.xml
new file mode 100644
index 000000000..4e02b363a
--- /dev/null
+++ b/clients/android/NewsBlur/res/drawable/ic_search.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/clients/android/NewsBlur/res/drawable/ic_sync.xml b/clients/android/NewsBlur/res/drawable/ic_sync.xml
new file mode 100644
index 000000000..e16b8024f
--- /dev/null
+++ b/clients/android/NewsBlur/res/drawable/ic_sync.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/clients/android/NewsBlur/res/drawable/ic_text.xml b/clients/android/NewsBlur/res/drawable/ic_text.xml
new file mode 100644
index 000000000..a8ae5a005
--- /dev/null
+++ b/clients/android/NewsBlur/res/drawable/ic_text.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/clients/android/NewsBlur/res/drawable/mute_feed_off.webp b/clients/android/NewsBlur/res/drawable/mute_feed_off.webp
new file mode 100644
index 000000000..c4180b628
Binary files /dev/null and b/clients/android/NewsBlur/res/drawable/mute_feed_off.webp differ
diff --git a/clients/android/NewsBlur/res/drawable/mute_feed_on.webp b/clients/android/NewsBlur/res/drawable/mute_feed_on.webp
new file mode 100644
index 000000000..c320f1f57
Binary files /dev/null and b/clients/android/NewsBlur/res/drawable/mute_feed_on.webp differ
diff --git a/clients/android/NewsBlur/res/layout/activity_configure_widget.xml b/clients/android/NewsBlur/res/layout/activity_configure_widget.xml
deleted file mode 100644
index 1c1afa844..000000000
--- a/clients/android/NewsBlur/res/layout/activity_configure_widget.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/clients/android/NewsBlur/res/layout/activity_itemslist.xml b/clients/android/NewsBlur/res/layout/activity_itemslist.xml
index 567805d29..251b7c3f7 100644
--- a/clients/android/NewsBlur/res/layout/activity_itemslist.xml
+++ b/clients/android/NewsBlur/res/layout/activity_itemslist.xml
@@ -27,6 +27,13 @@
android:layout_below="@id/itemlist_search_query"
/>
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/clients/android/NewsBlur/res/layout/activity_premium.xml b/clients/android/NewsBlur/res/layout/activity_premium.xml
new file mode 100644
index 000000000..02eca7922
--- /dev/null
+++ b/clients/android/NewsBlur/res/layout/activity_premium.xml
@@ -0,0 +1,293 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/clients/android/NewsBlur/res/layout/activity_profile.xml b/clients/android/NewsBlur/res/layout/activity_profile.xml
index 53e8894bc..7a0089efa 100644
--- a/clients/android/NewsBlur/res/layout/activity_profile.xml
+++ b/clients/android/NewsBlur/res/layout/activity_profile.xml
@@ -10,13 +10,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
-
-
-
+
diff --git a/clients/android/NewsBlur/res/layout/dialog_add_feed.xml b/clients/android/NewsBlur/res/layout/dialog_add_feed.xml
index 802a16759..c21b12d3d 100644
--- a/clients/android/NewsBlur/res/layout/dialog_add_feed.xml
+++ b/clients/android/NewsBlur/res/layout/dialog_add_feed.xml
@@ -78,11 +78,11 @@
android:layout_height="1dp"
android:background="?android:attr/listDivider" />
-
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
\ No newline at end of file
diff --git a/clients/android/NewsBlur/res/layout/dialog_choosefolders.xml b/clients/android/NewsBlur/res/layout/dialog_choosefolders.xml
index c563b219e..5bb1f4fbe 100644
--- a/clients/android/NewsBlur/res/layout/dialog_choosefolders.xml
+++ b/clients/android/NewsBlur/res/layout/dialog_choosefolders.xml
@@ -8,7 +8,7 @@
android:id="@+id/choose_folders_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- style="?divider"
+ style="?android:listDivider"
android:dividerHeight="2dp" />
diff --git a/clients/android/NewsBlur/res/layout/fragment_itemgrid.xml b/clients/android/NewsBlur/res/layout/fragment_itemgrid.xml
index 1138f8e44..7e59340b8 100644
--- a/clients/android/NewsBlur/res/layout/fragment_itemgrid.xml
+++ b/clients/android/NewsBlur/res/layout/fragment_itemgrid.xml
@@ -44,7 +44,7 @@
android:layout_height="6dp"
/>
-
-
+ android:gravity="center"
+ android:orientation="vertical">
+ android:src="@drawable/fleuron" />
+
+
+
+
+
+
+
+
diff --git a/clients/android/NewsBlur/res/layout/row_widget_config_feed.xml b/clients/android/NewsBlur/res/layout/row_widget_config_feed.xml
index 0556b79cd..db01d688e 100644
--- a/clients/android/NewsBlur/res/layout/row_widget_config_feed.xml
+++ b/clients/android/NewsBlur/res/layout/row_widget_config_feed.xml
@@ -12,6 +12,7 @@
android:id="@+id/check_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_marginEnd="4dp"
android:clickable="false"
android:focusable="false" />
@@ -20,7 +21,6 @@
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_gravity="center_vertical"
- android:layout_marginStart="4dp"
android:contentDescription="@string/description_row_feed_icon"
android:scaleType="centerCrop" />
@@ -39,4 +39,13 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
+
+
\ No newline at end of file
diff --git a/clients/android/NewsBlur/res/menu/itemslist.xml b/clients/android/NewsBlur/res/menu/itemslist.xml
index 5e023f541..02b39053b 100644
--- a/clients/android/NewsBlur/res/menu/itemslist.xml
+++ b/clients/android/NewsBlur/res/menu/itemslist.xml
@@ -63,6 +63,9 @@
+
diff --git a/clients/android/NewsBlur/res/menu/main.xml b/clients/android/NewsBlur/res/menu/main.xml
index 521f11387..0754c2644 100644
--- a/clients/android/NewsBlur/res/menu/main.xml
+++ b/clients/android/NewsBlur/res/menu/main.xml
@@ -21,6 +21,10 @@
android:title="@string/settings"
android:showAsAction="never" />
+
+
@@ -43,6 +47,10 @@
android:title="@string/menu_loginas"
android:showAsAction="never"
android:visible="false"/>
+
+
-
+
+
diff --git a/clients/android/NewsBlur/res/values/attrs.xml b/clients/android/NewsBlur/res/values/attrs.xml
index bc504ed65..635c10d98 100644
--- a/clients/android/NewsBlur/res/values/attrs.xml
+++ b/clients/android/NewsBlur/res/values/attrs.xml
@@ -23,7 +23,6 @@
-
diff --git a/clients/android/NewsBlur/res/values/dimens.xml b/clients/android/NewsBlur/res/values/dimens.xml
index 1186f1987..07a52bfc9 100644
--- a/clients/android/NewsBlur/res/values/dimens.xml
+++ b/clients/android/NewsBlur/res/values/dimens.xml
@@ -2,4 +2,5 @@
50dp
90dp
+ 4dp
\ No newline at end of file
diff --git a/clients/android/NewsBlur/res/values/strings.xml b/clients/android/NewsBlur/res/values/strings.xml
index dac91ee4d..e3eccc599 100644
--- a/clients/android/NewsBlur/res/values/strings.xml
+++ b/clients/android/NewsBlur/res/values/strings.xml
@@ -16,8 +16,7 @@
Next
Search for feeds
- Add \"%s\" to your feeds?
-
+
Loading…
Fetching story text…
@@ -29,6 +28,7 @@
The user\'s profile picture
folder icon
feed icon
+ feed mute toggle
An icon illustrating the user\'s activity
Follow or unfollow a user
Comment user image
@@ -135,6 +135,7 @@
Send story to…
Mark feed as read
Delete feed
+ Statistics
Delete saved search
Unfollow user
Choose folders
@@ -184,6 +185,8 @@
Folder View
Nested
Flat
+ Mute All
+ Mute None
Select All
Select None
Widget Background
@@ -213,6 +216,7 @@
Create a feedback post
Email a bug report
Theme…
+ Premium Account
Add new folder icon
@@ -255,10 +259,34 @@
%d stories/month
Preferences
+ Mute Sites
Widget
Tap to setup in NewsBlur
No active subscriptions detected
Loading...
+
+ Reading by folder is only available to
+ Search is only available to
+ premium subscribers
+ NewsBlur Premium
+ Thank you so much for going premium!
+ Thank you for going premium!
+ Your premium subscription is set to\nrenew on %s
+ Your premium subscription is set\nto expire on %s
+ Your premium subscription is set\nto never expire. Whoa!
+ Upgrading to a NewsBlur premium subscription gives you all of these features. Payments will be charged to your Play Store account at confirmation of purchase. Subscription renew unless auto-renew is turned off at least 24 hours before the end of the current period. Cancel at any time from Account Settings in Play Store.
+ privacy policy and terms of use for details.]]>
+ NewsBlur Premium Subscription
+ $35.99 per year ($3.00/month)
+ Error retrieving subscription details
+ Sites updated up to 10x more often
+ River of News (reading by folder)
+ Search sites and folders
+ Save stories with searchable tags
+ Privacy options for your blurblog
+ Custom RSS feeds for folders and saves stories
+ Text view conveniently extracts the story
+ You feed Shiloh, my poor, hungry dog, for a month
Offline
Download Stories
@@ -268,6 +296,12 @@
Keep Stories after Reading
Disable to reduce storage usage
+ You can follow up to 64 sites with a free standard account
+ Please mute %d sites or reset to most popular sites.
+ RESET TO POPULAR SITES
+ UPGRADE
+ %1$s/%2$s
+
Download Using
Restrict background data to chosen networks
Any Network
@@ -364,7 +398,6 @@
Reading
Immersive Mode Via Single Tap
Show Content Preview Text
- Show Image Preview Thumbnails
Image Preview Thumbnails
Notifications
Enable Notifications
@@ -414,6 +447,7 @@
Catching up reading actions...
On its way...
Cleaning up...
+ Sync saved stories actions…
Fetching fresh stories...
Storing%sunread stories...
Storing text for %s stories...
@@ -461,12 +495,14 @@
Mark Story Unread
Save Story
Unsave Story
+ Statistics
- @string/gest_action_none
- @string/gest_action_markread
- @string/gest_action_markunread
- @string/gest_action_save
- @string/gest_action_unsave
+ - @string/gest_action_statistics
- GEST_ACTION_NONE
@@ -474,6 +510,7 @@
- GEST_ACTION_MARKUNREAD
- GEST_ACTION_SAVE
- GEST_ACTION_UNSAVE
+ - GEST_ACTION_STATISTICS
GEST_ACTION_MARKREAD
@@ -484,6 +521,7 @@
- @string/gest_action_markunread
- @string/gest_action_save
- @string/gest_action_unsave
+ - @string/gest_action_statistics
- GEST_ACTION_NONE
@@ -491,6 +529,7 @@
- GEST_ACTION_MARKUNREAD
- GEST_ACTION_SAVE
- GEST_ACTION_UNSAVE
+ - GEST_ACTION_STATISTICS
GEST_ACTION_MARKUNREAD
@@ -556,7 +595,5 @@
story_notification_channel
New Stories
- Save Widget
- Select Feed
Go to feed
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/ActivityDetailsPagerAdapter.java b/clients/android/NewsBlur/src/com/newsblur/activity/ActivityDetailsPagerAdapter.java
index b70a6b1be..fe5641f31 100644
--- a/clients/android/NewsBlur/src/com/newsblur/activity/ActivityDetailsPagerAdapter.java
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/ActivityDetailsPagerAdapter.java
@@ -1,9 +1,9 @@
package com.newsblur.activity;
import android.content.res.Resources;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentPagerAdapter;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentPagerAdapter;
import com.newsblur.R;
import com.newsblur.domain.UserDetails;
@@ -21,7 +21,7 @@ public class ActivityDetailsPagerAdapter extends FragmentPagerAdapter {
private final Profile profile;
public ActivityDetailsPagerAdapter(FragmentManager fragmentManager, Profile profile) {
- super(fragmentManager);
+ super(fragmentManager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
this.profile = profile;
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/AddFeedExternal.java b/clients/android/NewsBlur/src/com/newsblur/activity/AddFeedExternal.java
index c31952319..bc044410c 100644
--- a/clients/android/NewsBlur/src/com/newsblur/activity/AddFeedExternal.java
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/AddFeedExternal.java
@@ -3,7 +3,7 @@ package com.newsblur.activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
+import androidx.fragment.app.DialogFragment;
import android.view.View;
import com.newsblur.R;
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/AddSocial.java b/clients/android/NewsBlur/src/com/newsblur/activity/AddSocial.java
index 201533813..47c8e176f 100644
--- a/clients/android/NewsBlur/src/com/newsblur/activity/AddSocial.java
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/AddSocial.java
@@ -2,8 +2,8 @@ package com.newsblur.activity;
import android.content.Intent;
import android.os.Bundle;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentTransaction;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
@@ -46,6 +46,7 @@ public class AddSocial extends NbActivity {
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ super.onActivityResult(requestCode, resultCode, intent);
switch (resultCode) {
case AddTwitter.TWITTER_AUTHED:
addSocialFragment.setTwitterAuthed();
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/FeedChooser.java b/clients/android/NewsBlur/src/com/newsblur/activity/FeedChooser.java
new file mode 100644
index 000000000..9698fc645
--- /dev/null
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/FeedChooser.java
@@ -0,0 +1,193 @@
+package com.newsblur.activity;
+
+import android.database.Cursor;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+import androidx.annotation.Nullable;
+import androidx.loader.content.Loader;
+
+import com.newsblur.R;
+import com.newsblur.domain.Feed;
+import com.newsblur.domain.Folder;
+import com.newsblur.util.FeedOrderFilter;
+import com.newsblur.util.FeedUtils;
+import com.newsblur.util.FolderViewFilter;
+import com.newsblur.util.ListOrderFilter;
+import com.newsblur.util.PrefsUtils;
+import com.newsblur.util.WidgetBackground;
+import com.newsblur.widget.WidgetUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+abstract public class FeedChooser extends NbActivity {
+
+ protected FeedChooserAdapter adapter;
+ protected ArrayList feeds;
+ protected ArrayList folders;
+ protected Map feedMap = new HashMap<>();
+ protected ArrayList folderNames = new ArrayList<>();
+ protected ArrayList> folderChildren = new ArrayList<>();
+
+ abstract void bindLayout();
+
+ abstract void setupList();
+
+ abstract void processFeeds(Cursor cursor);
+
+ abstract void processData();
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ bindLayout();
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ setupList();
+ loadFeeds();
+ loadFolders();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.menu_feed_chooser, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ ListOrderFilter listOrderFilter = PrefsUtils.getFeedChooserListOrder(this);
+ if (listOrderFilter == ListOrderFilter.ASCENDING) {
+ menu.findItem(R.id.menu_sort_order_ascending).setChecked(true);
+ } else if (listOrderFilter == ListOrderFilter.DESCENDING) {
+ menu.findItem(R.id.menu_sort_order_descending).setChecked(true);
+ }
+
+ FeedOrderFilter feedOrderFilter = PrefsUtils.getFeedChooserFeedOrder(this);
+ if (feedOrderFilter == FeedOrderFilter.NAME) {
+ menu.findItem(R.id.menu_sort_by_name).setChecked(true);
+ } else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS) {
+ menu.findItem(R.id.menu_sort_by_subs).setChecked(true);
+ } else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH) {
+ menu.findItem(R.id.menu_sort_by_stories_month).setChecked(true);
+ } else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY) {
+ menu.findItem(R.id.menu_sort_by_recent_story).setChecked(true);
+ } else if (feedOrderFilter == FeedOrderFilter.OPENS) {
+ menu.findItem(R.id.menu_sort_by_number_opens).setChecked(true);
+ }
+
+ FolderViewFilter folderViewFilter = PrefsUtils.getFeedChooserFolderView(this);
+ if (folderViewFilter == FolderViewFilter.NESTED) {
+ menu.findItem(R.id.menu_folder_view_nested).setChecked(true);
+ } else if (folderViewFilter == FolderViewFilter.FLAT) {
+ menu.findItem(R.id.menu_folder_view_flat).setChecked(true);
+ }
+
+ WidgetBackground widgetBackground = PrefsUtils.getWidgetBackground(this);
+ if (widgetBackground == WidgetBackground.DEFAULT) {
+ menu.findItem(R.id.menu_widget_background_default).setChecked(true);
+ } else if (widgetBackground == WidgetBackground.TRANSPARENT) {
+ menu.findItem(R.id.menu_widget_background_transparent).setChecked(true);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ case R.id.menu_sort_order_ascending:
+ replaceListOrderFilter(ListOrderFilter.ASCENDING);
+ return true;
+ case R.id.menu_sort_order_descending:
+ replaceListOrderFilter(ListOrderFilter.DESCENDING);
+ return true;
+ case R.id.menu_sort_by_name:
+ replaceFeedOrderFilter(FeedOrderFilter.NAME);
+ return true;
+ case R.id.menu_sort_by_subs:
+ replaceFeedOrderFilter(FeedOrderFilter.SUBSCRIBERS);
+ return true;
+ case R.id.menu_sort_by_recent_story:
+ replaceFeedOrderFilter(FeedOrderFilter.RECENT_STORY);
+ return true;
+ case R.id.menu_sort_by_stories_month:
+ replaceFeedOrderFilter(FeedOrderFilter.STORIES_MONTH);
+ return true;
+ case R.id.menu_sort_by_number_opens:
+ replaceFeedOrderFilter(FeedOrderFilter.OPENS);
+ return true;
+ case R.id.menu_folder_view_nested:
+ replaceFolderView(FolderViewFilter.NESTED);
+ return true;
+ case R.id.menu_folder_view_flat:
+ replaceFolderView(FolderViewFilter.FLAT);
+ return true;
+ case R.id.menu_widget_background_default:
+ setWidgetBackground(WidgetBackground.DEFAULT);
+ return true;
+ case R.id.menu_widget_background_transparent:
+ setWidgetBackground(WidgetBackground.TRANSPARENT);
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ protected void setAdapterData() {
+ adapter.setData(this.folderNames, this.folderChildren, this.feeds);
+ }
+
+ private void replaceFeedOrderFilter(FeedOrderFilter feedOrderFilter) {
+ PrefsUtils.setFeedChooserFeedOrder(this, feedOrderFilter);
+ adapter.replaceFeedOrder(feedOrderFilter);
+ }
+
+ private void replaceListOrderFilter(ListOrderFilter listOrderFilter) {
+ PrefsUtils.setFeedChooserListOrder(this, listOrderFilter);
+ adapter.replaceListOrder(listOrderFilter);
+ }
+
+ private void replaceFolderView(FolderViewFilter folderViewFilter) {
+ PrefsUtils.setFeedChooserFolderView(this, folderViewFilter);
+ adapter.replaceFolderView(folderViewFilter);
+ setAdapterData();
+ }
+
+ private void setWidgetBackground(WidgetBackground widgetBackground) {
+ PrefsUtils.setWidgetBackground(this, widgetBackground);
+ WidgetUtils.updateWidget(this);
+ }
+
+ private void loadFeeds() {
+ Loader loader = FeedUtils.dbHelper.getFeedsLoader();
+ loader.registerListener(loader.getId(), (loader1, cursor) -> processFeeds(cursor));
+ loader.startLoading();
+ }
+
+ private void loadFolders() {
+ Loader loader = FeedUtils.dbHelper.getFoldersLoader();
+ loader.registerListener(loader.getId(), (loader1, cursor) -> processFolders(cursor));
+ loader.startLoading();
+ }
+
+ private void processFolders(Cursor cursor) {
+ ArrayList folders = new ArrayList<>();
+ while (cursor != null && cursor.moveToNext()) {
+ Folder folder = Folder.fromCursor(cursor);
+ if (!folder.feedIds.isEmpty()) {
+ folders.add(folder);
+ }
+ }
+ this.folders = folders;
+ Collections.sort(this.folders, (o1, o2) -> Folder.compareFolderNames(o1.flatName(), o2.flatName()));
+ processData();
+ }
+}
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/FeedChooserAdapter.java b/clients/android/NewsBlur/src/com/newsblur/activity/FeedChooserAdapter.java
new file mode 100644
index 000000000..a358a3fad
--- /dev/null
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/FeedChooserAdapter.java
@@ -0,0 +1,252 @@
+package com.newsblur.activity;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.CheckBox;
+import android.widget.ExpandableListView;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.newsblur.R;
+import com.newsblur.domain.Feed;
+import com.newsblur.util.AppConstants;
+import com.newsblur.util.FeedOrderFilter;
+import com.newsblur.util.FeedUtils;
+import com.newsblur.util.FolderViewFilter;
+import com.newsblur.util.ListOrderFilter;
+import com.newsblur.util.PrefsUtils;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import java.util.TimeZone;
+
+public class FeedChooserAdapter extends BaseExpandableListAdapter {
+
+ protected final static int defaultTextSizeChild = 14;
+ protected final static int defaultTextSizeGroup = 13;
+
+ protected Set feedIds = new HashSet<>();
+ protected ArrayList folderNames = new ArrayList<>();
+ protected ArrayList> folderChildren = new ArrayList<>();
+
+ protected FolderViewFilter folderViewFilter;
+ protected ListOrderFilter listOrderFilter;
+ protected FeedOrderFilter feedOrderFilter;
+
+ protected float textSize;
+
+ FeedChooserAdapter(Context context) {
+ folderViewFilter = PrefsUtils.getFeedChooserFolderView(context);
+ listOrderFilter = PrefsUtils.getFeedChooserListOrder(context);
+ feedOrderFilter = PrefsUtils.getFeedChooserFeedOrder(context);
+ textSize = PrefsUtils.getListTextSize(context);
+ }
+
+ @Override
+ public int getGroupCount() {
+ return folderNames.size();
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ return folderChildren.get(groupPosition).size();
+ }
+
+ @Override
+ public String getGroup(int groupPosition) {
+ return folderNames.get(groupPosition);
+ }
+
+ @Override
+ public Feed getChild(int groupPosition, int childPosition) {
+ return folderChildren.get(groupPosition).get(childPosition);
+ }
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return folderNames.get(groupPosition).hashCode();
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return folderChildren.get(groupPosition).get(childPosition).hashCode();
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public View getGroupView(final int groupPosition, boolean isExpanded, View convertView, final ViewGroup parent) {
+ String folderName = folderNames.get(groupPosition);
+ if (folderName.equals(AppConstants.ROOT_FOLDER)) {
+ convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_root_folder, parent, false);
+ } else {
+ convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_folder, parent, false);
+ TextView textName = convertView.findViewById(R.id.text_folder_name);
+ textName.setTextSize(textSize * defaultTextSizeGroup);
+ textName.setText(folderName);
+ }
+
+ ((ExpandableListView) parent).expandGroup(groupPosition);
+ return convertView;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, final ViewGroup parent) {
+ if (convertView == null) {
+ convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_feed, parent, false);
+ }
+
+ final Feed feed = folderChildren.get(groupPosition).get(childPosition);
+ TextView textTitle = convertView.findViewById(R.id.text_title);
+ TextView textDetails = convertView.findViewById(R.id.text_details);
+ final CheckBox checkBox = convertView.findViewById(R.id.check_box);
+ ImageView img = convertView.findViewById(R.id.img);
+ textTitle.setTextSize(textSize * defaultTextSizeChild);
+ textDetails.setTextSize(textSize * defaultTextSizeChild);
+ textTitle.setText(feed.title);
+ checkBox.setChecked(feedIds.contains(feed.feedId));
+
+ if (feedOrderFilter == FeedOrderFilter.NAME || feedOrderFilter == FeedOrderFilter.OPENS) {
+ textDetails.setText(parent.getContext().getString(R.string.feed_opens, feed.feedOpens));
+ } else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS) {
+ textDetails.setText(parent.getContext().getString(R.string.feed_subscribers, feed.subscribers));
+ } else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH) {
+ textDetails.setText(parent.getContext().getString(R.string.feed_stories_per_month, feed.storiesPerMonth));
+ } else {
+ // FeedOrderFilter.RECENT_STORY
+ try {
+ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ Date dateTime = dateFormat.parse(feed.lastStoryDate);
+ CharSequence relativeTimeString = DateUtils.getRelativeTimeSpanString(dateTime.getTime(), System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
+ textDetails.setText(relativeTimeString);
+ } catch (Exception e) {
+ textDetails.setText(feed.lastStoryDate);
+ }
+ }
+
+ FeedUtils.iconLoader.displayImage(feed.faviconUrl, img, 0, false, img.getHeight(), true);
+ return convertView;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return true;
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return super.areAllItemsEnabled();
+ }
+
+ protected void setData(ArrayList activeFoldersNames, ArrayList> activeFolderChildren, ArrayList feeds) {
+ if (folderViewFilter == FolderViewFilter.NESTED) {
+ this.folderNames = activeFoldersNames;
+ this.folderChildren = activeFolderChildren;
+ } else {
+ this.folderNames = new ArrayList<>(1);
+ this.folderNames.add(AppConstants.ROOT_FOLDER);
+ this.folderChildren = new ArrayList<>();
+ this.folderChildren.add(feeds);
+ }
+ this.notifyDataChanged();
+ }
+
+ protected void replaceFeedOrder(FeedOrderFilter feedOrderFilter) {
+ this.feedOrderFilter = feedOrderFilter;
+ notifyDataChanged();
+ }
+
+ protected void replaceListOrder(ListOrderFilter listOrderFilter) {
+ this.listOrderFilter = listOrderFilter;
+ notifyDataChanged();
+ }
+
+ protected void replaceFolderView(FolderViewFilter folderViewFilter) {
+ this.folderViewFilter = folderViewFilter;
+ }
+
+ protected void notifyDataChanged() {
+ for (ArrayList feedList : this.folderChildren) {
+ Collections.sort(feedList, getListComparator());
+ }
+ this.notifyDataSetChanged();
+ }
+
+ protected void setFeedIds(Set feedIds) {
+ this.feedIds.clear();
+ this.feedIds.addAll(feedIds);
+ }
+
+ protected void replaceFeedIds(Set feedIds) {
+ setFeedIds(feedIds);
+ this.notifyDataSetChanged();
+ }
+
+ private Comparator getListComparator() {
+ return (o1, o2) -> {
+ if (feedOrderFilter == FeedOrderFilter.NAME && listOrderFilter == ListOrderFilter.ASCENDING) {
+ return o1.title.compareTo(o2.title);
+ } else if (feedOrderFilter == FeedOrderFilter.NAME && listOrderFilter == ListOrderFilter.DESCENDING) {
+ return o2.title.compareTo(o1.title);
+ } else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS && listOrderFilter == ListOrderFilter.ASCENDING) {
+ return Integer.valueOf(o1.subscribers).compareTo(Integer.valueOf(o2.subscribers));
+ } else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS && listOrderFilter == ListOrderFilter.DESCENDING) {
+ return Integer.valueOf(o2.subscribers).compareTo(Integer.valueOf(o1.subscribers));
+ } else if (feedOrderFilter == FeedOrderFilter.OPENS && listOrderFilter == ListOrderFilter.ASCENDING) {
+ return Integer.compare(o1.feedOpens, o2.feedOpens);
+ } else if (feedOrderFilter == FeedOrderFilter.OPENS && listOrderFilter == ListOrderFilter.DESCENDING) {
+ return Integer.compare(o2.feedOpens, o1.feedOpens);
+ } else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY && listOrderFilter == ListOrderFilter.ASCENDING) {
+ return compareLastStoryDateTimes(o1.lastStoryDate, o2.lastStoryDate, listOrderFilter);
+ } else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY && listOrderFilter == ListOrderFilter.DESCENDING) {
+ return compareLastStoryDateTimes(o1.lastStoryDate, o2.lastStoryDate, listOrderFilter);
+ } else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH && listOrderFilter == ListOrderFilter.ASCENDING) {
+ return Integer.compare(o1.storiesPerMonth, o2.storiesPerMonth);
+ } else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH && listOrderFilter == ListOrderFilter.DESCENDING) {
+ return Integer.compare(o2.storiesPerMonth, o1.storiesPerMonth);
+ }
+ return o1.title.compareTo(o2.title);
+ };
+ }
+
+ private int compareLastStoryDateTimes(String firstDateTime, String secondDateTime, ListOrderFilter listOrderFilter) {
+ try {
+ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
+ // found null last story date times on feeds
+ if (TextUtils.isEmpty(firstDateTime)) {
+ firstDateTime = "2000-01-01 00:00:00";
+ }
+ if (TextUtils.isEmpty(secondDateTime)) {
+ secondDateTime = "2000-01-01 00:00:00";
+ }
+
+ Date firstDate = dateFormat.parse(firstDateTime);
+ Date secondDate = dateFormat.parse(secondDateTime);
+ if (listOrderFilter == ListOrderFilter.ASCENDING) {
+ return firstDate.compareTo(secondDate);
+ } else {
+ return secondDate.compareTo(firstDate);
+ }
+ } catch (ParseException e) {
+ e.printStackTrace();
+ return 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/FeedItemsList.java b/clients/android/NewsBlur/src/com/newsblur/activity/FeedItemsList.java
index f3f66830e..59c4c4b41 100644
--- a/clients/android/NewsBlur/src/com/newsblur/activity/FeedItemsList.java
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/FeedItemsList.java
@@ -3,10 +3,14 @@ package com.newsblur.activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
+import androidx.fragment.app.DialogFragment;
import android.view.Menu;
import android.view.MenuItem;
+import com.google.android.play.core.review.ReviewInfo;
+import com.google.android.play.core.review.ReviewManager;
+import com.google.android.play.core.review.ReviewManagerFactory;
+import com.google.android.play.core.tasks.Task;
import com.newsblur.R;
import com.newsblur.domain.Feed;
import com.newsblur.fragment.DeleteFeedFragment;
@@ -14,6 +18,7 @@ import com.newsblur.fragment.FeedIntelTrainerFragment;
import com.newsblur.fragment.RenameDialogFragment;
import com.newsblur.util.FeedSet;
import com.newsblur.util.FeedUtils;
+import com.newsblur.util.PrefsUtils;
import com.newsblur.util.UIUtils;
public class FeedItemsList extends ItemsList {
@@ -22,6 +27,8 @@ public class FeedItemsList extends ItemsList {
public static final String EXTRA_FOLDER_NAME = "folderName";
private Feed feed;
private String folderName;
+ private ReviewManager reviewManager;
+ private ReviewInfo reviewInfo;
public static void startActivity(Context context, FeedSet feedSet,
Feed feed, String folderName) {
@@ -36,13 +43,28 @@ public class FeedItemsList extends ItemsList {
protected void onCreate(Bundle bundle) {
feed = (Feed) getIntent().getSerializableExtra(EXTRA_FEED);
folderName = getIntent().getStringExtra(EXTRA_FOLDER_NAME);
-
+
super.onCreate(bundle);
UIUtils.setCustomActionBar(this, feed.faviconUrl, feed.title);
- }
+ checkInAppReview();
+ }
- public void deleteFeed() {
+ @Override
+ public void onBackPressed() {
+ // see checkInAppReview()
+ if (reviewInfo != null) {
+ Task flow = reviewManager.launchReviewFlow(this, reviewInfo);
+ flow.addOnCompleteListener(task -> {
+ PrefsUtils.setInAppReviewed(this);
+ super.onBackPressed();
+ });
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ public void deleteFeed() {
DialogFragment deleteFeedFragment = DeleteFeedFragment.newInstance(feed, folderName);
deleteFeedFragment.show(getSupportFragmentManager(), "dialog");
}
@@ -85,6 +107,10 @@ public class FeedItemsList extends ItemsList {
// TODO: since this activity uses a feed object passed as an extra and doesn't query the DB,
// the name change won't be reflected until the activity finishes.
}
+ if (item.getItemId() == R.id.menu_statistics) {
+ FeedUtils.openStatistics(this, feed.feedId);
+ return true;
+ }
return false;
}
@@ -122,4 +148,16 @@ public class FeedItemsList extends ItemsList {
String getSaveSearchFeedId() {
return "feed:" + feed.feedId;
}
+
+ private void checkInAppReview() {
+ if (!PrefsUtils.hasInAppReviewed(this)) {
+ reviewManager = ReviewManagerFactory.create(this);
+ Task request = reviewManager.requestReviewFlow();
+ request.addOnCompleteListener(task -> {
+ if (task.isSuccessful()) {
+ reviewInfo = task.getResult();
+ }
+ });
+ }
+ }
}
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/InAppBrowser.java b/clients/android/NewsBlur/src/com/newsblur/activity/InAppBrowser.java
index 0f514e30f..a5b28eea0 100644
--- a/clients/android/NewsBlur/src/com/newsblur/activity/InAppBrowser.java
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/InAppBrowser.java
@@ -2,12 +2,13 @@ package com.newsblur.activity;
import android.graphics.Bitmap;
import android.os.Bundle;
-import android.support.v4.app.FragmentActivity;
import android.view.View;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
+import androidx.fragment.app.FragmentActivity;
+
import com.newsblur.databinding.ActivityInAppBrowserBinding;
import com.newsblur.util.PrefsUtils;
@@ -53,13 +54,4 @@ public class InAppBrowser extends FragmentActivity {
binding.webView.loadUrl(url);
}
-
- @Override
- public void onBackPressed() {
- if (binding.webView.canGoBack()) {
- binding.webView.goBack();
- } else {
- finish();
- }
- }
-}
+}
\ No newline at end of file
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/ItemsList.java b/clients/android/NewsBlur/src/com/newsblur/activity/ItemsList.java
index a27453eb0..17373d41f 100644
--- a/clients/android/NewsBlur/src/com/newsblur/activity/ItemsList.java
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/ItemsList.java
@@ -1,8 +1,8 @@
package com.newsblur.activity;
import android.os.Bundle;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentTransaction;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.Menu;
@@ -97,7 +97,7 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
if (activeSearchQuery != null) {
binding.itemlistSearchQuery.setText(activeSearchQuery);
binding.itemlistSearchQuery.setVisibility(View.VISIBLE);
- fs.setSearchQuery(activeSearchQuery);
+ checkSearchQuery();
}
binding.itemlistSearchQuery.setOnKeyListener(new OnKeyListener() {
@@ -191,6 +191,7 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
menu.findItem(R.id.menu_instafetch_feed).setVisible(false);
menu.findItem(R.id.menu_intel).setVisible(false);
menu.findItem(R.id.menu_rename_feed).setVisible(false);
+ menu.findItem(R.id.menu_statistics).setVisible(false);
}
if (!fs.isInfrequent()) {
@@ -349,11 +350,16 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
}
private void checkSearchQuery() {
- String oldQuery = fs.getSearchQuery();
String q = binding.itemlistSearchQuery.getText().toString().trim();
if (q.length() < 1) {
+ updateFleuron(false);
q = null;
+ } else if (!PrefsUtils.getIsPremium(this)) {
+ updateFleuron(true);
+ return;
}
+
+ String oldQuery = fs.getSearchQuery();
fs.setSearchQuery(q);
if (!TextUtils.equals(q, oldQuery)) {
FeedUtils.prepareReadingSession(fs, true);
@@ -364,6 +370,26 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
}
}
+ private void updateFleuron(boolean requiresPremium) {
+ FragmentTransaction transaction = getSupportFragmentManager()
+ .beginTransaction()
+ .setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out);
+
+ if (requiresPremium) {
+ transaction.hide(itemSetFragment);
+ binding.footerFleuron.textSubscription.setText(R.string.premium_subscribers_search);
+ binding.footerFleuron.containerSubscribe.setVisibility(View.VISIBLE);
+ binding.footerFleuron.getRoot().setVisibility(View.VISIBLE);
+ binding.footerFleuron.containerSubscribe.setOnClickListener(view -> UIUtils.startPremiumActivity(this));
+ } else {
+ transaction.show(itemSetFragment);
+ binding.footerFleuron.containerSubscribe.setVisibility(View.GONE);
+ binding.footerFleuron.getRoot().setVisibility(View.GONE);
+ binding.footerFleuron.containerSubscribe.setOnClickListener(null);
+ }
+ transaction.commit();
+ }
+
@Override
public void storyOrderChanged(StoryOrder newValue) {
updateStoryOrderPreference(newValue);
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/Login.java b/clients/android/NewsBlur/src/com/newsblur/activity/Login.java
index 3f37e5d22..a277f87cf 100644
--- a/clients/android/NewsBlur/src/com/newsblur/activity/Login.java
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/Login.java
@@ -1,9 +1,9 @@
package com.newsblur.activity;
import android.os.Bundle;
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentTransaction;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
import android.view.Window;
import com.newsblur.R;
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/LoginProgress.java b/clients/android/NewsBlur/src/com/newsblur/activity/LoginProgress.java
index 1a284eb68..c169c3c07 100644
--- a/clients/android/NewsBlur/src/com/newsblur/activity/LoginProgress.java
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/LoginProgress.java
@@ -1,9 +1,9 @@
package com.newsblur.activity;
import android.os.Bundle;
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentTransaction;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
import android.view.Window;
import com.newsblur.R;
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/Main.java b/clients/android/NewsBlur/src/com/newsblur/activity/Main.java
index 6d92112c9..a48d479fb 100644
--- a/clients/android/NewsBlur/src/com/newsblur/activity/Main.java
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/Main.java
@@ -5,9 +5,9 @@ import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
-import android.support.v4.app.DialogFragment;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.widget.SwipeRefreshLayout;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
@@ -373,6 +373,14 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
} else if (item.getItemId() == R.id.menu_theme_black) {
PrefsUtils.setSelectedTheme(this, ThemeValue.BLACK);
UIUtils.restartActivity(this);
+ } else if (item.getItemId() == R.id.menu_premium_account) {
+ Intent intent = new Intent(this, Premium.class);
+ startActivity(intent);
+ return true;
+ } else if (item.getItemId() == R.id.menu_mute_sites) {
+ Intent intent = new Intent(this, MuteConfig.class);
+ startActivity(intent);
+ return true;
}
return false;
}
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/MuteConfig.java b/clients/android/NewsBlur/src/com/newsblur/activity/MuteConfig.java
new file mode 100644
index 000000000..9c767a531
--- /dev/null
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/MuteConfig.java
@@ -0,0 +1,221 @@
+package com.newsblur.activity;
+
+import android.app.AlertDialog;
+import android.content.Intent;
+import android.database.Cursor;
+import android.text.TextUtils;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.core.content.ContextCompat;
+
+import com.newsblur.R;
+import com.newsblur.databinding.ActivityMuteConfigBinding;
+import com.newsblur.domain.Feed;
+import com.newsblur.domain.Folder;
+import com.newsblur.service.NBSyncService;
+import com.newsblur.util.AppConstants;
+import com.newsblur.util.FeedUtils;
+import com.newsblur.util.PrefsUtils;
+import com.newsblur.util.UIUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public class MuteConfig extends FeedChooser implements MuteConfigAdapter.FeedStateChangedListener {
+
+ private ActivityMuteConfigBinding binding;
+ private boolean checkedInitFeedsLimit = false;
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ menu.findItem(R.id.menu_select_all).setVisible(false);
+ menu.findItem(R.id.menu_select_none).setVisible(false);
+ menu.findItem(R.id.menu_widget_background).setVisible(false);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_mute_all:
+ setFeedsState(true);
+ return true;
+ case R.id.menu_mute_none:
+ setFeedsState(false);
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ void bindLayout() {
+ binding = ActivityMuteConfigBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+ }
+
+ @Override
+ void setupList() {
+ adapter = new MuteConfigAdapter(this, this);
+ binding.listView.setAdapter(adapter);
+ }
+
+ @Override
+ void processFeeds(Cursor cursor) {
+ ArrayList feeds = new ArrayList<>();
+ while (cursor != null && cursor.moveToNext()) {
+ Feed feed = Feed.fromCursor(cursor);
+ feeds.add(feed);
+ feedMap.put(feed.feedId, feed);
+ }
+ this.feeds = feeds;
+ processData();
+ }
+
+ @Override
+ void processData() {
+ if (folders != null && feeds != null) {
+ for (Folder folder : folders) {
+ ArrayList children = new ArrayList<>();
+ for (String feedId : folder.feedIds) {
+ Feed feed = feedMap.get(feedId);
+ if (!children.contains(feed)) {
+ children.add(feed);
+ }
+ }
+ folderNames.add(folder.flatName());
+ folderChildren.add(children);
+ }
+
+ setAdapterData();
+ syncActiveFeedCount();
+ checkedInitFeedsLimit = true;
+ }
+ }
+
+ @Override
+ public void setAdapterData() {
+ Set feedIds = new HashSet<>(this.feeds.size());
+ for (Feed feed : this.feeds) {
+ feedIds.add(feed.feedId);
+ }
+ adapter.setFeedIds(feedIds);
+
+ super.setAdapterData();
+ }
+
+ @Override
+ protected void handleUpdate(int updateType) {
+ super.handleUpdate(updateType);
+ if ((updateType & UPDATE_STATUS) != 0) {
+ String syncStatus = NBSyncService.getSyncStatusMessage(this, false);
+ if (syncStatus != null) {
+ binding.textSyncStatus.setText(syncStatus);
+ binding.textSyncStatus.setVisibility(View.VISIBLE);
+ } else {
+ binding.textSyncStatus.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ @Override
+ public void onFeedStateChanged() {
+ syncActiveFeedCount();
+ }
+
+ private void syncActiveFeedCount() {
+ // free standard accounts can follow up to 64 sites
+ boolean isPremium = PrefsUtils.getIsPremium(this);
+ if (!isPremium && feeds != null) {
+ int activeSites = 0;
+ for (Feed feed : feeds) {
+ if (feed.active) {
+ activeSites++;
+ }
+ }
+ int textColorRes = activeSites > AppConstants.FREE_ACCOUNT_SITE_LIMIT ? R.color.negative : R.color.positive;
+ binding.textSites.setTextColor(ContextCompat.getColor(this, textColorRes));
+ binding.textSites.setText(String.format(getString(R.string.mute_config_sites), activeSites, AppConstants.FREE_ACCOUNT_SITE_LIMIT));
+ showSitesCount();
+
+ if (activeSites > AppConstants.FREE_ACCOUNT_SITE_LIMIT && !checkedInitFeedsLimit) {
+ showAccountFeedsLimitDialog(activeSites - AppConstants.FREE_ACCOUNT_SITE_LIMIT);
+ }
+ } else {
+ hideSitesCount();
+ }
+ }
+
+ private void setFeedsState(boolean isMute) {
+ for (Feed feed : feeds) {
+ feed.active = !isMute;
+ }
+ adapter.notifyDataSetChanged();
+
+ if (isMute) FeedUtils.muteFeeds(this, adapter.feedIds);
+ else FeedUtils.unmuteFeeds(this, adapter.feedIds);
+ }
+
+ private void showAccountFeedsLimitDialog(int exceededLimitCount) {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.mute_config_title)
+ .setMessage(String.format(getString(R.string.mute_config_message), exceededLimitCount))
+ .setNeutralButton(android.R.string.ok, null)
+ .setPositiveButton(R.string.mute_config_upgrade, (dialogInterface, i) -> openUpgradeToPremium())
+ .show();
+ }
+
+ private void showSitesCount() {
+ ViewGroup.LayoutParams oldLayout = binding.listView.getLayoutParams();
+ FrameLayout.LayoutParams newLayout = new FrameLayout.LayoutParams(oldLayout);
+ newLayout.topMargin = UIUtils.dp2px(this, 56);
+ binding.listView.setLayoutParams(newLayout);
+ binding.containerSitesCount.setVisibility(View.VISIBLE);
+ binding.textResetSites.setOnClickListener(view -> resetToPopularFeeds());
+ }
+
+ private void hideSitesCount() {
+ ViewGroup.LayoutParams oldLayout = binding.listView.getLayoutParams();
+ FrameLayout.LayoutParams newLayout = new FrameLayout.LayoutParams(oldLayout);
+ newLayout.topMargin = UIUtils.dp2px(this, 0);
+ binding.listView.setLayoutParams(newLayout);
+ binding.containerSitesCount.setVisibility(View.GONE);
+ binding.textResetSites.setOnClickListener(null);
+ }
+
+ // reset to most popular sites based on subscribers
+ private void resetToPopularFeeds() {
+ // sort descending by subscribers
+ Collections.sort(feeds, (f1, f2) -> {
+ if (TextUtils.isEmpty(f1.subscribers)) f1.subscribers = "0";
+ if (TextUtils.isEmpty(f2.subscribers)) f2.subscribers = "0";
+ return Integer.valueOf(f2.subscribers).compareTo(Integer.valueOf(f1.subscribers));
+ });
+ Set activeFeedIds = new HashSet<>();
+ Set inactiveFeedIds = new HashSet<>();
+ for (int index = 0; index < feeds.size(); index++) {
+ Feed feed = feeds.get(index);
+ if (index < AppConstants.FREE_ACCOUNT_SITE_LIMIT) {
+ activeFeedIds.add(feed.feedId);
+ } else {
+ inactiveFeedIds.add(feed.feedId);
+ }
+ }
+ FeedUtils.unmuteFeeds(this, activeFeedIds);
+ FeedUtils.muteFeeds(this, inactiveFeedIds);
+ finish();
+ }
+
+ private void openUpgradeToPremium() {
+ Intent intent = new Intent(this, Premium.class);
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/MuteConfigAdapter.java b/clients/android/NewsBlur/src/com/newsblur/activity/MuteConfigAdapter.java
new file mode 100644
index 000000000..574571e9d
--- /dev/null
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/MuteConfigAdapter.java
@@ -0,0 +1,86 @@
+package com.newsblur.activity;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+
+import com.newsblur.R;
+import com.newsblur.domain.Feed;
+import com.newsblur.util.FeedUtils;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+public class MuteConfigAdapter extends FeedChooserAdapter {
+
+ private FeedStateChangedListener listener;
+
+ MuteConfigAdapter(Context context, FeedStateChangedListener listener) {
+ super(context);
+ this.listener = listener;
+ }
+
+ @Override
+ public View getGroupView(final int groupPosition, boolean isExpanded, View convertView, final ViewGroup parent) {
+ View groupView = super.getGroupView(groupPosition, isExpanded, convertView, parent);
+
+ groupView.setOnClickListener(v -> {
+ ArrayList folderChild = MuteConfigAdapter.this.folderChildren.get(groupPosition);
+ boolean allAreMute = true;
+ for (Feed feed : folderChild) {
+ if (feed.active) {
+ allAreMute = false;
+ break;
+ }
+ }
+
+ Set feedIds = new HashSet<>(folderChild.size());
+ for (Feed feed : folderChild) {
+ // flip active flag
+ feed.active = allAreMute;
+ feedIds.add(feed.feedId);
+ }
+
+ // if allAreMute initially, we need to unMute feeds
+ if (allAreMute) FeedUtils.unmuteFeeds(groupView.getContext(), feedIds);
+ else FeedUtils.muteFeeds(groupView.getContext(), feedIds);
+
+ listener.onFeedStateChanged();
+ notifyDataChanged();
+ });
+ return groupView;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, final ViewGroup parent) {
+ View childView = super.getChildView(groupPosition, childPosition, isLastChild, convertView, parent);
+ final Feed feed = folderChildren.get(groupPosition).get(childPosition);
+ final CheckBox checkBox = childView.findViewById(R.id.check_box);
+ final ImageView imgToggle = childView.findViewById(R.id.img_toggle);
+ checkBox.setVisibility(View.GONE);
+ imgToggle.setVisibility(View.VISIBLE);
+
+ if (feed.active) imgToggle.setBackgroundResource(R.drawable.mute_feed_on);
+ else imgToggle.setBackgroundResource(R.drawable.mute_feed_off);
+
+ childView.setOnClickListener(v -> {
+ feed.active = !feed.active;
+ Set feedIds = new HashSet<>(1);
+ feedIds.add(feed.feedId);
+ if (feed.active) FeedUtils.unmuteFeeds(childView.getContext(), feedIds);
+ else FeedUtils.muteFeeds(childView.getContext(), feedIds);
+
+ listener.onFeedStateChanged();
+ notifyDataChanged();
+ });
+ return childView;
+ }
+
+ interface FeedStateChangedListener {
+
+ void onFeedStateChanged();
+ }
+}
\ No newline at end of file
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/NbActivity.java b/clients/android/NewsBlur/src/com/newsblur/activity/NbActivity.java
index 5280a7e60..42da9b5ba 100644
--- a/clients/android/NewsBlur/src/com/newsblur/activity/NbActivity.java
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/NbActivity.java
@@ -1,11 +1,9 @@
package com.newsblur.activity;
-import android.appwidget.AppWidgetManager;
import android.os.Bundle;
-import android.support.v4.app.FragmentActivity;
+import androidx.fragment.app.FragmentActivity;
import android.widget.Toast;
-import com.newsblur.R;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.PrefConstants.ThemeValue;
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/Premium.java b/clients/android/NewsBlur/src/com/newsblur/activity/Premium.java
new file mode 100644
index 000000000..58f87ec58
--- /dev/null
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/Premium.java
@@ -0,0 +1,308 @@
+package com.newsblur.activity;
+
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.text.util.Linkify;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.res.ResourcesCompat;
+
+import com.android.billingclient.api.AcknowledgePurchaseParams;
+import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
+import com.android.billingclient.api.BillingClient;
+import com.android.billingclient.api.BillingClientStateListener;
+import com.android.billingclient.api.BillingFlowParams;
+import com.android.billingclient.api.BillingResult;
+import com.android.billingclient.api.Purchase;
+import com.android.billingclient.api.PurchasesUpdatedListener;
+import com.android.billingclient.api.SkuDetails;
+import com.android.billingclient.api.SkuDetailsParams;
+import com.newsblur.R;
+import com.newsblur.databinding.ActivityPremiumBinding;
+import com.newsblur.network.APIManager;
+import com.newsblur.network.domain.NewsBlurResponse;
+import com.newsblur.service.NBSyncService;
+import com.newsblur.util.AppConstants;
+import com.newsblur.util.BetterLinkMovementMethod;
+import com.newsblur.util.FeedUtils;
+import com.newsblur.util.Log;
+import com.newsblur.util.PrefsUtils;
+import com.newsblur.util.UIUtils;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import nl.dionsegijn.konfetti.emitters.StreamEmitter;
+import nl.dionsegijn.konfetti.models.Shape;
+import nl.dionsegijn.konfetti.models.Size;
+
+public class Premium extends NbActivity {
+
+ private ActivityPremiumBinding binding;
+ private BillingClient billingClient;
+ private SkuDetails subscriptionDetails;
+ private Purchase purchasedSubscription;
+
+ private AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = billingResult -> {
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
+ Log.d(Premium.this.getLocalClassName(), "acknowledgePurchaseResponseListener OK");
+ verifyUserSubscriptionStatus();
+ } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE) {
+ // Billing API version is not supported for the type requested.
+ Log.d(Premium.this.getLocalClassName(), "acknowledgePurchaseResponseListener BILLING_UNAVAILABLE");
+ } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE) {
+ // Network connection is down.
+ Log.d(Premium.this.getLocalClassName(), "acknowledgePurchaseResponseListener SERVICE_UNAVAILABLE");
+ } else {
+ // Handle any other error codes.
+ Log.d(Premium.this.getLocalClassName(), "acknowledgePurchaseResponseListener ERROR - message: " + billingResult.getDebugMessage());
+ }
+ };
+
+ private PurchasesUpdatedListener purchaseUpdateListener = (billingResult, purchases) -> {
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
+ Log.d(Premium.this.getLocalClassName(), "purchaseUpdateListener OK");
+ for (Purchase purchase : purchases) {
+ handlePurchase(purchase);
+ }
+ } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
+ // Handle an error caused by a user cancelling the purchase flow.
+ Log.d(Premium.this.getLocalClassName(), "purchaseUpdateListener USER_CANCELLED");
+ } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE) {
+ // Billing API version is not supported for the type requested.
+ Log.d(Premium.this.getLocalClassName(), "purchaseUpdateListener BILLING_UNAVAILABLE");
+ } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE) {
+ // Network connection is down.
+ Log.d(Premium.this.getLocalClassName(), "purchaseUpdateListener SERVICE_UNAVAILABLE");
+ } else {
+ // Handle any other error codes.
+ Log.d(Premium.this.getLocalClassName(), "purchaseUpdateListener ERROR - message: " + billingResult.getDebugMessage());
+ }
+ };
+
+ private BillingClientStateListener billingClientStateListener = new BillingClientStateListener() {
+ @Override
+ public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
+ // The BillingClient is ready. You can query purchases here.
+ Log.d(Premium.this.getLocalClassName(), "onBillingSetupFinished OK");
+ retrievePlayStoreSubscriptions();
+ verifyUserSubscriptionStatus();
+ } else {
+ showSubscriptionDetailsError();
+ }
+ }
+
+ @Override
+ public void onBillingServiceDisconnected() {
+ Log.d(Premium.this.getLocalClassName(), "onBillingServiceDisconnected");
+ // Try to restart the connection on the next request to
+ // Google Play by calling the startConnection() method.
+ showSubscriptionDetailsError();
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ binding = ActivityPremiumBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ setupUI();
+ setupBillingClient();
+ }
+
+ private void setupUI() {
+ UIUtils.setCustomActionBar(this, R.drawable.logo, getString(R.string.premium_toolbar_title));
+
+ // linkify before setting the string resource
+ BetterLinkMovementMethod.linkify(Linkify.WEB_URLS, binding.textPolicies)
+ .setOnLinkClickListener((textView, url) -> {
+ UIUtils.handleUri(Premium.this, Uri.parse(url));
+ return true;
+ });
+ binding.textPolicies.setText(UIUtils.fromHtml(getString(R.string.premium_policies)));
+ binding.textSubTitle.setPaintFlags(binding.textSubTitle.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
+ FeedUtils.iconLoader.displayImage(AppConstants.SHILOH_PHOTO_URL, binding.imgShiloh, 0, false);
+ }
+
+ private void setupBillingClient() {
+ billingClient = BillingClient.newBuilder(this)
+ .setListener(purchaseUpdateListener)
+ .enablePendingPurchases()
+ .build();
+
+ billingClient.startConnection(billingClientStateListener);
+ }
+
+ private void verifyUserSubscriptionStatus() {
+ boolean hasNewsBlurSubscription = PrefsUtils.getIsPremium(this);
+ Purchase playStoreSubscription = null;
+ Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.SUBS);
+ if (result.getPurchasesList() != null) {
+ for (Purchase purchase : result.getPurchasesList()) {
+ if (purchase.getSku().equals(AppConstants.PREMIUM_SKU)) {
+ playStoreSubscription = purchase;
+ }
+ }
+ }
+
+ if (hasNewsBlurSubscription || playStoreSubscription != null) {
+ binding.containerGoingPremium.setVisibility(View.GONE);
+ binding.containerGonePremium.setVisibility(View.VISIBLE);
+ long expirationTimeMs = PrefsUtils.getPremiumExpire(this);
+ String renewalString = null;
+
+ if (expirationTimeMs == 0) {
+ renewalString = getString(R.string.premium_subscription_no_expiration);
+ } else if (expirationTimeMs > 0) {
+ // date constructor expects ms
+ Date expirationDate = new Date(expirationTimeMs * 1000);
+ DateFormat dateFormat = new SimpleDateFormat("EEE, MMMM d, yyyy", Locale.getDefault());
+ dateFormat.setTimeZone(TimeZone.getDefault());
+ renewalString = getString(R.string.premium_subscription_renewal, dateFormat.format(expirationDate));
+
+ if (playStoreSubscription != null && !playStoreSubscription.isAutoRenewing()) {
+ renewalString = getString(R.string.premium_subscription_expiration, dateFormat.format(expirationDate));
+ }
+ }
+
+ if (!TextUtils.isEmpty(renewalString)) {
+ binding.textSubscriptionRenewal.setText(renewalString);
+ binding.textSubscriptionRenewal.setVisibility(View.VISIBLE);
+ }
+ showConfetti();
+ }
+
+ if (!hasNewsBlurSubscription && playStoreSubscription != null) {
+ purchasedSubscription = playStoreSubscription;
+ notifyNewsBlurOfSubscription();
+ }
+ }
+
+ private void retrievePlayStoreSubscriptions() {
+ List skuList = new ArrayList<>(1);
+ // add sub SKUs from Play Store
+ skuList.add(AppConstants.PREMIUM_SKU);
+ SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
+ params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS);
+ billingClient.querySkuDetailsAsync(params.build(), (billingResult, skuDetailsList) -> {
+ Log.d(Premium.this.getLocalClassName(), "SkuDetailsResponse");
+ processSkuDetailsList(skuDetailsList);
+ });
+ }
+
+ private void processSkuDetailsList(@Nullable List skuDetailsList) {
+ if (skuDetailsList != null) {
+ for (SkuDetails skuDetails : skuDetailsList) {
+ if (skuDetails.getSku().equals(AppConstants.PREMIUM_SKU)) {
+ Log.d(Premium.this.getLocalClassName(), "Sku detail: " + skuDetails.getTitle() + " | " + skuDetails.getDescription() + " | " + skuDetails.getPrice() + " | " + skuDetails.getSku());
+ subscriptionDetails = skuDetails;
+ }
+ }
+ }
+
+ if (subscriptionDetails != null) {
+ showSubscriptionDetails();
+ } else {
+ showSubscriptionDetailsError();
+ }
+ }
+
+ private void showSubscriptionDetailsError() {
+ binding.textLoading.setText(R.string.premium_subscription_details_error);
+ binding.textLoading.setVisibility(View.VISIBLE);
+ binding.containerSub.setVisibility(View.GONE);
+ }
+
+ private void showSubscriptionDetails() {
+ // handling dynamic currency and pricing for 1Y subscriptions
+ String currencySymbol = subscriptionDetails.getPrice().substring(0, 1);
+ String priceString = subscriptionDetails.getPrice().substring(1);
+ double price = Double.parseDouble(priceString);
+ StringBuilder pricingText = new StringBuilder();
+ pricingText.append(subscriptionDetails.getPrice());
+ pricingText.append(" per year (");
+ pricingText.append(currencySymbol);
+ pricingText.append(String.format(Locale.getDefault(), "%.2f", price / 12));
+ pricingText.append("/month)");
+
+ binding.textSubTitle.setText(subscriptionDetails.getTitle());
+ binding.textSubPrice.setText(pricingText);
+ binding.textLoading.setVisibility(View.GONE);
+ binding.containerSub.setVisibility(View.VISIBLE);
+ binding.containerSub.setOnClickListener(view -> launchBillingFlow(subscriptionDetails));
+ }
+
+ private void launchBillingFlow(@NonNull SkuDetails skuDetails) {
+ Log.d(Premium.this.getLocalClassName(), "launchBillingFlow for sku: " + skuDetails.getSku());
+ BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
+ .setSkuDetails(skuDetails)
+ .build();
+ billingClient.launchBillingFlow(this, billingFlowParams);
+ }
+
+ private void handlePurchase(Purchase purchase) {
+ Log.d(Premium.this.getLocalClassName(), "handlePurchase: " + purchase.getOrderId());
+ purchasedSubscription = purchase;
+ if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED && purchase.isAcknowledged()) {
+ verifyUserSubscriptionStatus();
+ } else if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED && !purchase.isAcknowledged()) {
+ // need to acknowledge first time sub otherwise it will void
+ Log.d(Premium.this.getLocalClassName(), "acknowledge purchase: " + purchase.getOrderId());
+ AcknowledgePurchaseParams acknowledgePurchaseParams =
+ AcknowledgePurchaseParams.newBuilder()
+ .setPurchaseToken(purchase.getPurchaseToken())
+ .build();
+ billingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
+ }
+ }
+
+ private void showConfetti() {
+ binding.konfetti.build()
+ .addColors(Color.YELLOW, Color.GREEN, Color.MAGENTA, Color.BLUE, Color.CYAN, Color.RED)
+ .setDirection(90)
+ .setFadeOutEnabled(true)
+ .setTimeToLive(1000L)
+ .addShapes(Shape.Square.INSTANCE, Shape.Circle.INSTANCE)
+ .addSizes(new Size(10, 5f))
+ .setPosition(0, binding.konfetti.getWidth() + 0f , -50f, -20f)
+ .streamFor(100, StreamEmitter.INDEFINITE);
+ }
+
+ private void notifyNewsBlurOfSubscription() {
+ if (purchasedSubscription != null) {
+ APIManager apiManager = new APIManager(this);
+ new AsyncTask() {
+ @Override
+ protected NewsBlurResponse doInBackground(Void... voids) {
+ return apiManager.saveReceipt(purchasedSubscription.getOrderId(), purchasedSubscription.getSku());
+ }
+
+ @Override
+ protected void onPostExecute(NewsBlurResponse result) {
+ super.onPostExecute(result);
+ if (!result.isError()) {
+ NBSyncService.forceFeedsFolders();
+ triggerSync();
+ }
+ finish();
+ }
+ }.execute();
+ }
+ }
+}
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/Profile.java b/clients/android/NewsBlur/src/com/newsblur/activity/Profile.java
index c376a4659..9684aa983 100644
--- a/clients/android/NewsBlur/src/com/newsblur/activity/Profile.java
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/Profile.java
@@ -2,9 +2,9 @@ package com.newsblur.activity;
import android.os.AsyncTask;
import android.os.Bundle;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentTransaction;
-import android.support.v4.view.ViewPager;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.viewpager.widget.ViewPager;
import android.text.TextUtils;
import android.view.MenuItem;
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/Reading.java b/clients/android/NewsBlur/src/com/newsblur/activity/Reading.java
index aa0869574..db4ceb9bb 100644
--- a/clients/android/NewsBlur/src/com/newsblur/activity/Reading.java
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/Reading.java
@@ -8,12 +8,12 @@ import android.content.res.Configuration;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentTransaction;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.content.Loader;
-import android.support.v4.view.ViewPager;
-import android.support.v4.view.ViewPager.OnPageChangeListener;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.Loader;
+import androidx.viewpager.widget.ViewPager;
+import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
@@ -178,7 +178,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
transaction.commit();
}
- getSupportLoaderManager().initLoader(0, null, this);
+ LoaderManager.getInstance(this).initLoader(0, null, this);
}
@Override
@@ -436,7 +436,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
private void updateCursor() {
synchronized (STORIES_MUTEX) {
try {
- getSupportLoaderManager().restartLoader(0, null, this);
+ LoaderManager.getInstance(this).restartLoader(0, null, this);
} catch (IllegalStateException ise) {
; // our heavy use of async can race loader calls, which it will gripe about, but this
// is only a refresh call, so dropping a refresh during creation is perfectly fine.
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/RegisterProgress.java b/clients/android/NewsBlur/src/com/newsblur/activity/RegisterProgress.java
index ef9e15956..d3277456e 100644
--- a/clients/android/NewsBlur/src/com/newsblur/activity/RegisterProgress.java
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/RegisterProgress.java
@@ -1,9 +1,9 @@
package com.newsblur.activity;
-import android.support.v4.app.FragmentActivity;
+import androidx.fragment.app.FragmentActivity;
import android.os.Bundle;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentTransaction;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
import com.newsblur.R;
import com.newsblur.fragment.RegisterProgressFragment;
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/SearchForFeeds.java b/clients/android/NewsBlur/src/com/newsblur/activity/SearchForFeeds.java
index ddfba1069..bd484072b 100644
--- a/clients/android/NewsBlur/src/com/newsblur/activity/SearchForFeeds.java
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/SearchForFeeds.java
@@ -8,9 +8,10 @@ import java.util.Set;
import android.app.SearchManager;
import android.content.Intent;
import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.Loader;
+import androidx.fragment.app.DialogFragment;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.app.LoaderManager.LoaderCallbacks;
+import androidx.loader.content.Loader;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -55,7 +56,7 @@ public class SearchForFeeds extends NbActivity implements LoaderCallbacks feeds;
- private ArrayList folders;
- private Map feedMap = new HashMap<>();
- private ArrayList folderNames = new ArrayList<>();
- private ArrayList> folderChildren = new ArrayList<>();
private ActivityWidgetConfigBinding binding;
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- binding = ActivityWidgetConfigBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
- getActionBar().setDisplayHomeAsUpEnabled(true);
- setupList();
- loadFeeds();
- loadFolders();
- }
-
@Override
protected void onPause() {
super.onPause();
@@ -61,127 +32,46 @@ public class WidgetConfig extends NbActivity {
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
- inflater.inflate(R.menu.menu_widget, menu);
+ inflater.inflate(R.menu.menu_feed_chooser, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
- ListOrderFilter listOrderFilter = PrefsUtils.getWidgetConfigListOrder(this);
- if (listOrderFilter == ListOrderFilter.ASCENDING) {
- menu.findItem(R.id.menu_sort_order_ascending).setChecked(true);
- } else if (listOrderFilter == ListOrderFilter.DESCENDING) {
- menu.findItem(R.id.menu_sort_order_descending).setChecked(true);
- }
-
- FeedOrderFilter feedOrderFilter = PrefsUtils.getWidgetConfigFeedOrder(this);
- if (feedOrderFilter == FeedOrderFilter.NAME) {
- menu.findItem(R.id.menu_sort_by_name).setChecked(true);
- } else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS) {
- menu.findItem(R.id.menu_sort_by_subs).setChecked(true);
- } else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH) {
- menu.findItem(R.id.menu_sort_by_stories_month).setChecked(true);
- } else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY) {
- menu.findItem(R.id.menu_sort_by_recent_story).setChecked(true);
- } else if (feedOrderFilter == FeedOrderFilter.OPENS) {
- menu.findItem(R.id.menu_sort_by_number_opens).setChecked(true);
- }
-
- FolderViewFilter folderViewFilter = PrefsUtils.getWidgetConfigFolderView(this);
- if (folderViewFilter == FolderViewFilter.NESTED) {
- menu.findItem(R.id.menu_folder_view_nested).setChecked(true);
- } else if (folderViewFilter == FolderViewFilter.FLAT) {
- menu.findItem(R.id.menu_folder_view_flat).setChecked(true);
- }
-
- WidgetBackground widgetBackground = PrefsUtils.getWidgetBackground(this);
- if (widgetBackground == WidgetBackground.DEFAULT) {
- menu.findItem(R.id.menu_widget_background_default).setChecked(true);
- } else if (widgetBackground == WidgetBackground.TRANSPARENT) {
- menu.findItem(R.id.menu_widget_background_transparent).setChecked(true);
- }
+ menu.findItem(R.id.menu_mute_all).setVisible(false);
+ menu.findItem(R.id.menu_mute_none).setVisible(false);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
- case android.R.id.home:
- finish();
- return true;
- case R.id.menu_sort_order_ascending:
- replaceListOrderFilter(ListOrderFilter.ASCENDING);
- return true;
- case R.id.menu_sort_order_descending:
- replaceListOrderFilter(ListOrderFilter.DESCENDING);
- return true;
- case R.id.menu_sort_by_name:
- replaceFeedOrderFilter(FeedOrderFilter.NAME);
- return true;
- case R.id.menu_sort_by_subs:
- replaceFeedOrderFilter(FeedOrderFilter.SUBSCRIBERS);
- return true;
- case R.id.menu_sort_by_recent_story:
- replaceFeedOrderFilter(FeedOrderFilter.RECENT_STORY);
- return true;
- case R.id.menu_sort_by_stories_month:
- replaceFeedOrderFilter(FeedOrderFilter.STORIES_MONTH);
- return true;
- case R.id.menu_sort_by_number_opens:
- replaceFeedOrderFilter(FeedOrderFilter.OPENS);
- return true;
- case R.id.menu_folder_view_nested:
- replaceFolderView(FolderViewFilter.NESTED);
- return true;
- case R.id.menu_folder_view_flat:
- replaceFolderView(FolderViewFilter.FLAT);
- return true;
case R.id.menu_select_all:
selectAllFeeds();
return true;
case R.id.menu_select_none:
- replaceWidgetFeedIds(Collections.emptySet());
- return true;
- case R.id.menu_widget_background_default:
- setWidgetBackground(WidgetBackground.DEFAULT);
- return true;
- case R.id.menu_widget_background_transparent:
- setWidgetBackground(WidgetBackground.TRANSPARENT);
+ replaceWidgetFeedIds(Collections.emptySet());
return true;
default:
return super.onOptionsItemSelected(item);
}
}
- private void setupList() {
+ @Override
+ void bindLayout() {
+ binding = ActivityWidgetConfigBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+ }
+
+ @Override
+ void setupList() {
adapter = new WidgetConfigAdapter(this);
binding.listView.setAdapter(adapter);
}
- private void loadFeeds() {
- Loader loader = FeedUtils.dbHelper.getFeedsLoader();
- loader.registerListener(loader.getId(), new Loader.OnLoadCompleteListener() {
- @Override
- public void onLoadComplete(@NonNull Loader loader, @Nullable Cursor cursor) {
- processFeeds(cursor);
- }
- });
- loader.startLoading();
- }
-
- private void loadFolders() {
- Loader loader = FeedUtils.dbHelper.getFoldersLoader();
- loader.registerListener(loader.getId(), new Loader.OnLoadCompleteListener() {
- @Override
- public void onLoadComplete(@NonNull Loader loader, @Nullable Cursor cursor) {
- processFolders(cursor);
- }
- });
- loader.startLoading();
- }
-
- private void processFeeds(Cursor cursor) {
+ @Override
+ void processFeeds(Cursor cursor) {
ArrayList feeds = new ArrayList<>();
while (cursor != null && cursor.moveToNext()) {
Feed feed = Feed.fromCursor(cursor);
@@ -194,25 +84,25 @@ public class WidgetConfig extends NbActivity {
processData();
}
- private void processFolders(Cursor cursor) {
- ArrayList folders = new ArrayList<>();
- while (cursor != null && cursor.moveToNext()) {
- Folder folder = Folder.fromCursor(cursor);
- if (!folder.feedIds.isEmpty()) {
- folders.add(folder);
+ @Override
+ public void setAdapterData() {
+ Set feedIds = PrefsUtils.getWidgetFeedIds(this);
+ // by default select all feeds
+ if (feedIds == null) {
+ feedIds = new HashSet<>(this.feeds.size());
+ for (Feed feed : this.feeds) {
+ feedIds.add(feed.feedId);
}
}
- this.folders = folders;
- Collections.sort(this.folders, new Comparator() {
- @Override
- public int compare(Folder o1, Folder o2) {
- return Folder.compareFolderNames(o1.flatName(), o2.flatName());
- }
- });
- processData();
+ adapter.setFeedIds(feedIds);
+
+ super.setAdapterData();
+ binding.listView.setVisibility(this.feeds.isEmpty() ? View.GONE : View.VISIBLE);
+ binding.textNoSubscriptions.setVisibility(this.feeds.isEmpty() ? View.VISIBLE : View.GONE);
}
- private void processData() {
+ @Override
+ void processData() {
if (folders != null && feeds != null) {
for (Folder folder : folders) {
ArrayList activeFeeds = new ArrayList<>();
@@ -226,7 +116,6 @@ public class WidgetConfig extends NbActivity {
folderChildren.add(activeFeeds);
}
- setSelectedFeeds();
setAdapterData();
}
}
@@ -243,44 +132,4 @@ public class WidgetConfig extends NbActivity {
PrefsUtils.setWidgetFeedIds(this, feedIds);
adapter.replaceFeedIds(feedIds);
}
-
- private void replaceFeedOrderFilter(FeedOrderFilter feedOrderFilter) {
- PrefsUtils.setWidgetConfigFeedOrder(this, feedOrderFilter);
- adapter.replaceFeedOrder(feedOrderFilter);
- }
-
- private void replaceListOrderFilter(ListOrderFilter listOrderFilter) {
- PrefsUtils.setWidgetConfigListOrder(this, listOrderFilter);
- adapter.replaceListOrder(listOrderFilter);
- }
-
- private void replaceFolderView(FolderViewFilter folderViewFilter) {
- PrefsUtils.setWidgetConfigFolderView(this, folderViewFilter);
- adapter.replaceFolderView(folderViewFilter);
- setAdapterData();
- }
-
- private void setWidgetBackground(WidgetBackground widgetBackground) {
- PrefsUtils.setWidgetBackground(this, widgetBackground);
- WidgetUtils.updateWidget(this);
- }
-
- private void setSelectedFeeds() {
- Set feedIds = PrefsUtils.getWidgetFeedIds(this);
- // by default select all feeds
- if (feedIds == null) {
- feedIds = new HashSet<>(this.feeds.size());
- for (Feed feed : this.feeds) {
- feedIds.add(feed.feedId);
- }
- }
- adapter.setFeedIds(feedIds);
- }
-
- private void setAdapterData() {
- adapter.setData(this.folderNames, this.folderChildren, this.feeds);
-
- binding.listView.setVisibility(this.feeds.isEmpty() ? View.GONE : View.VISIBLE);
- binding.textNoSubscriptions.setVisibility(this.feeds.isEmpty() ? View.VISIBLE : View.GONE);
- }
}
\ No newline at end of file
diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/WidgetConfigAdapter.java b/clients/android/NewsBlur/src/com/newsblur/activity/WidgetConfigAdapter.java
index ec3492fb6..5686ecbda 100644
--- a/clients/android/NewsBlur/src/com/newsblur/activity/WidgetConfigAdapter.java
+++ b/clients/android/NewsBlur/src/com/newsblur/activity/WidgetConfigAdapter.java
@@ -1,296 +1,72 @@
package com.newsblur.activity;
import android.content.Context;
-import android.text.TextUtils;
-import android.text.format.DateUtils;
-import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.BaseExpandableListAdapter;
import android.widget.CheckBox;
-import android.widget.ExpandableListView;
import android.widget.ImageView;
-import android.widget.TextView;
import com.newsblur.R;
import com.newsblur.domain.Feed;
-import com.newsblur.util.AppConstants;
-import com.newsblur.util.FeedOrderFilter;
-import com.newsblur.util.FeedUtils;
-import com.newsblur.util.FolderViewFilter;
-import com.newsblur.util.ListOrderFilter;
import com.newsblur.util.PrefsUtils;
-import java.text.DateFormat;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.Locale;
-import java.util.Set;
-import java.util.TimeZone;
-public class WidgetConfigAdapter extends BaseExpandableListAdapter {
-
- private final static int defaultTextSizeChild = 14;
- private final static int defaultTextSizeGroup = 13;
-
- private Set feedIds = new HashSet<>();
- private ArrayList folderNames = new ArrayList<>();
- private ArrayList> folderChildren = new ArrayList<>();
-
- private FolderViewFilter folderViewFilter;
- private ListOrderFilter listOrderFilter;
- private FeedOrderFilter feedOrderFilter;
-
- private float textSize;
+public class WidgetConfigAdapter extends FeedChooserAdapter {
WidgetConfigAdapter(Context context) {
- folderViewFilter = PrefsUtils.getWidgetConfigFolderView(context);
- listOrderFilter = PrefsUtils.getWidgetConfigListOrder(context);
- feedOrderFilter = PrefsUtils.getWidgetConfigFeedOrder(context);
- textSize = PrefsUtils.getListTextSize(context);
- }
-
- @Override
- public int getGroupCount() {
- return folderNames.size();
- }
-
- @Override
- public int getChildrenCount(int groupPosition) {
- return folderChildren.get(groupPosition).size();
- }
-
- @Override
- public String getGroup(int groupPosition) {
- return folderNames.get(groupPosition);
- }
-
- @Override
- public Feed getChild(int groupPosition, int childPosition) {
- return folderChildren.get(groupPosition).get(childPosition);
- }
-
- @Override
- public long getGroupId(int groupPosition) {
- return folderNames.get(groupPosition).hashCode();
- }
-
- @Override
- public long getChildId(int groupPosition, int childPosition) {
- return folderChildren.get(groupPosition).get(childPosition).hashCode();
- }
-
- @Override
- public boolean hasStableIds() {
- return true;
+ super(context);
}
@Override
public View getGroupView(final int groupPosition, boolean isExpanded, View convertView, final ViewGroup parent) {
- String folderName = folderNames.get(groupPosition);
- if (folderName.equals(AppConstants.ROOT_FOLDER)) {
- convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_root_folder, parent, false);
- } else {
- convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_folder, parent, false);
- TextView textName = convertView.findViewById(R.id.text_folder_name);
- textName.setTextSize(textSize * defaultTextSizeGroup);
- textName.setText(folderName);
- }
+ View groupView = super.getGroupView(groupPosition, isExpanded, convertView, parent);
- ((ExpandableListView) parent).expandGroup(groupPosition);
-
- convertView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- ArrayList folderChild = WidgetConfigAdapter.this.folderChildren.get(groupPosition);
- // check all is selected
- boolean allSelected = true;
- for (Feed feed : folderChild) {
- if (!feedIds.contains(feed.feedId)) {
- allSelected = false;
- break;
- }
+ groupView.setOnClickListener(v -> {
+ ArrayList folderChild = WidgetConfigAdapter.this.folderChildren.get(groupPosition);
+ // check all is selected
+ boolean allSelected = true;
+ for (Feed feed : folderChild) {
+ if (!feedIds.contains(feed.feedId)) {
+ allSelected = false;
+ break;
}
- for (Feed feed : folderChild) {
- if (allSelected) {
- feedIds.remove(feed.feedId);
- } else {
- feedIds.add(feed.feedId);
- }
- }
- setWidgetFeedIds(parent.getContext());
- notifyDataChanged();
}
+ for (Feed feed : folderChild) {
+ if (allSelected) {
+ feedIds.remove(feed.feedId);
+ } else {
+ feedIds.add(feed.feedId);
+ }
+ }
+ setWidgetFeedIds(parent.getContext());
+ notifyDataChanged();
});
- return convertView;
+ return groupView;
}
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, final ViewGroup parent) {
- if (convertView == null) {
- convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_feed, parent, false);
- }
-
+ View childView = super.getChildView(groupPosition, childPosition, isLastChild, convertView, parent);
final Feed feed = folderChildren.get(groupPosition).get(childPosition);
- TextView textTitle = convertView.findViewById(R.id.text_title);
- TextView textDetails = convertView.findViewById(R.id.text_details);
- final CheckBox checkBox = convertView.findViewById(R.id.check_box);
- ImageView img = convertView.findViewById(R.id.img);
- textTitle.setTextSize(textSize * defaultTextSizeChild);
- textDetails.setTextSize(textSize * defaultTextSizeChild);
- textTitle.setText(feed.title);
- checkBox.setChecked(feedIds.contains(feed.feedId));
+ final CheckBox checkBox = childView.findViewById(R.id.check_box);
+ final ImageView imgToggle = childView.findViewById(R.id.img_toggle);
+ checkBox.setVisibility(View.VISIBLE);
+ imgToggle.setVisibility(View.GONE);
- if (feedOrderFilter == FeedOrderFilter.NAME || feedOrderFilter == FeedOrderFilter.OPENS) {
- textDetails.setText(parent.getContext().getString(R.string.feed_opens, feed.feedOpens));
- } else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS) {
- textDetails.setText(parent.getContext().getString(R.string.feed_subscribers, feed.subscribers));
- } else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH) {
- textDetails.setText(parent.getContext().getString(R.string.feed_stories_per_month, feed.storiesPerMonth));
- } else {
- // FeedOrderFilter.RECENT_STORY
- try {
- DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
- Date dateTime = dateFormat.parse(feed.lastStoryDate);
- CharSequence relativeTimeString = DateUtils.getRelativeTimeSpanString(dateTime.getTime(), System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
- textDetails.setText(relativeTimeString);
- } catch (Exception e) {
- textDetails.setText(feed.lastStoryDate);
- }
- }
-
- FeedUtils.iconLoader.displayImage(feed.faviconUrl, img, 0, false, img.getHeight(), true);
-
- convertView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- checkBox.setChecked(!checkBox.isChecked());
- if (checkBox.isChecked()) {
- feedIds.add(feed.feedId);
- } else {
- feedIds.remove(feed.feedId);
- }
- setWidgetFeedIds(parent.getContext());
+ childView.setOnClickListener(v -> {
+ checkBox.setChecked(!checkBox.isChecked());
+ if (checkBox.isChecked()) {
+ feedIds.add(feed.feedId);
+ } else {
+ feedIds.remove(feed.feedId);
}
+ setWidgetFeedIds(parent.getContext());
});
- return convertView;
- }
-
- @Override
- public boolean isChildSelectable(int groupPosition, int childPosition) {
- return true;
- }
-
- @Override
- public boolean areAllItemsEnabled() {
- return super.areAllItemsEnabled();
- }
-
- void setData(ArrayList activeFoldersNames, ArrayList> activeFolderChildren, ArrayList feeds) {
- if (folderViewFilter == FolderViewFilter.NESTED) {
- this.folderNames = activeFoldersNames;
- this.folderChildren = activeFolderChildren;
- } else {
- this.folderNames = new ArrayList<>(1);
- this.folderNames.add(AppConstants.ROOT_FOLDER);
- this.folderChildren = new ArrayList<>();
- this.folderChildren.add(feeds);
- }
- this.notifyDataChanged();
- }
-
- void replaceFeedOrder(FeedOrderFilter feedOrderFilter) {
- this.feedOrderFilter = feedOrderFilter;
- notifyDataChanged();
- }
-
- void replaceListOrder(ListOrderFilter listOrderFilter) {
- this.listOrderFilter = listOrderFilter;
- notifyDataChanged();
- }
-
- void replaceFolderView(FolderViewFilter folderViewFilter) {
- this.folderViewFilter = folderViewFilter;
- }
-
- private void notifyDataChanged() {
- for (ArrayList feedList : this.folderChildren) {
- Collections.sort(feedList, getListComparator());
- }
- this.notifyDataSetChanged();
- }
-
- void setFeedIds(Set feedIds) {
- this.feedIds.clear();
- this.feedIds.addAll(feedIds);
- }
-
- void replaceFeedIds(Set feedIds) {
- setFeedIds(feedIds);
- this.notifyDataSetChanged();
+ return childView;
}
private void setWidgetFeedIds(Context context) {
PrefsUtils.setWidgetFeedIds(context, feedIds);
}
-
- private Comparator getListComparator() {
- return new Comparator() {
- @Override
- public int compare(Feed o1, Feed o2) {
- if (feedOrderFilter == FeedOrderFilter.NAME && listOrderFilter == ListOrderFilter.ASCENDING) {
- return o1.title.compareTo(o2.title);
- } else if (feedOrderFilter == FeedOrderFilter.NAME && listOrderFilter == ListOrderFilter.DESCENDING) {
- return o2.title.compareTo(o1.title);
- } else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS && listOrderFilter == ListOrderFilter.ASCENDING) {
- return Integer.valueOf(o1.subscribers).compareTo(Integer.valueOf(o2.subscribers));
- } else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS && listOrderFilter == ListOrderFilter.DESCENDING) {
- return Integer.valueOf(o2.subscribers).compareTo(Integer.valueOf(o1.subscribers));
- } else if (feedOrderFilter == FeedOrderFilter.OPENS && listOrderFilter == ListOrderFilter.ASCENDING) {
- return Integer.compare(o1.feedOpens, o2.feedOpens);
- } else if (feedOrderFilter == FeedOrderFilter.OPENS && listOrderFilter == ListOrderFilter.DESCENDING) {
- return Integer.compare(o2.feedOpens, o1.feedOpens);
- } else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY && listOrderFilter == ListOrderFilter.ASCENDING) {
- return compareLastStoryDateTimes(o1.lastStoryDate, o2.lastStoryDate, listOrderFilter);
- } else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY && listOrderFilter == ListOrderFilter.DESCENDING) {
- return compareLastStoryDateTimes(o1.lastStoryDate, o2.lastStoryDate, listOrderFilter);
- } else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH && listOrderFilter == ListOrderFilter.ASCENDING) {
- return Integer.compare(o1.storiesPerMonth, o2.storiesPerMonth);
- } else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH && listOrderFilter == ListOrderFilter.DESCENDING) {
- return Integer.compare(o2.storiesPerMonth, o1.storiesPerMonth);
- }
- return o1.title.compareTo(o2.title);
- }
- };
- }
-
- private int compareLastStoryDateTimes(String firstDateTime, String secondDateTime, ListOrderFilter listOrderFilter) {
- try {
- DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
- // found null last story date times on feeds
- if (TextUtils.isEmpty(firstDateTime)) {
- firstDateTime = "2000-01-01 00:00:00";
- }
- if (TextUtils.isEmpty(secondDateTime)) {
- secondDateTime = "2000-01-01 00:00:00";
- }
-
- Date firstDate = dateFormat.parse(firstDateTime);
- Date secondDate = dateFormat.parse(secondDateTime);
- if (listOrderFilter == ListOrderFilter.ASCENDING) {
- return firstDate.compareTo(secondDate);
- } else {
- return secondDate.compareTo(firstDate);
- }
- } catch (ParseException e) {
- e.printStackTrace();
- return 0;
- }
- }
}
diff --git a/clients/android/NewsBlur/src/com/newsblur/database/BlurDatabaseHelper.java b/clients/android/NewsBlur/src/com/newsblur/database/BlurDatabaseHelper.java
index 71ec7799e..487e42ba7 100644
--- a/clients/android/NewsBlur/src/com/newsblur/database/BlurDatabaseHelper.java
+++ b/clients/android/NewsBlur/src/com/newsblur/database/BlurDatabaseHelper.java
@@ -6,9 +6,9 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.os.CancellationSignal;
-import android.support.annotation.Nullable;
-import android.support.v4.content.AsyncTaskLoader;
-import android.support.v4.content.Loader;
+import androidx.annotation.Nullable;
+import androidx.loader.content.AsyncTaskLoader;
+import androidx.loader.content.Loader;
import android.text.TextUtils;
import android.util.Log;
@@ -284,6 +284,19 @@ public class BlurDatabaseHelper {
return hashes;
}
+ public Set getStarredStoryHashes() {
+ String q = "SELECT " + DatabaseConstants.STORY_HASH +
+ " FROM " + DatabaseConstants.STORY_TABLE +
+ " WHERE " + DatabaseConstants.STORY_STARRED + " = 1" ;
+ Cursor c = dbRO.rawQuery(q, null);
+ Set hashes = new HashSet<>(c.getCount());
+ while (c.moveToNext()) {
+ hashes.add(c.getString(c.getColumnIndexOrThrow(DatabaseConstants.STORY_HASH)));
+ }
+ c.close();
+ return hashes;
+ }
+
public Set getAllStoryImages() {
Cursor c = dbRO.query(DatabaseConstants.STORY_TABLE, new String[]{DatabaseConstants.STORY_IMAGE_URLS}, null, null, null, null, null);
Set urls = new HashSet(c.getCount());
@@ -584,6 +597,22 @@ public class BlurDatabaseHelper {
}
}
+ public void markStoryHashesStarred(Collection hashes, boolean isStarred) {
+ synchronized (RW_MUTEX) {
+ dbRW.beginTransaction();
+ try {
+ ContentValues values = new ContentValues();
+ values.put(DatabaseConstants.STORY_STARRED, isStarred);
+ for (String hash : hashes) {
+ dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash});
+ }
+ dbRW.setTransactionSuccessful();
+ } finally {
+ dbRW.endTransaction();
+ }
+ }
+ }
+
public void setFeedsActive(Set feedIds, boolean active) {
synchronized (RW_MUTEX) {
dbRW.beginTransaction();
diff --git a/clients/android/NewsBlur/src/com/newsblur/database/FolderListAdapter.java b/clients/android/NewsBlur/src/com/newsblur/database/FolderListAdapter.java
index 4e980e6c0..c8bda8ef9 100644
--- a/clients/android/NewsBlur/src/com/newsblur/database/FolderListAdapter.java
+++ b/clients/android/NewsBlur/src/com/newsblur/database/FolderListAdapter.java
@@ -75,6 +75,8 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
public int totalSocialNeutCount = 0;
/** Total positive unreads for all social feeds. */
public int totalSocialPosiCount = 0;
+ /** Total active feeds. */
+ public int totalActiveFeedCount = 0;
/** Feeds, indexed by feed ID. */
private Map feeds = Collections.emptyMap();
@@ -557,6 +559,7 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
feedPosCounts = new HashMap();
totalNeutCount = 0;
totalPosCount = 0;
+ totalActiveFeedCount = 0;
while (cursor.moveToNext()) {
Feed f = Feed.fromCursor(cursor);
feeds.put(f.feedId, f);
@@ -570,6 +573,9 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
feedNeutCounts.put(f.feedId, neut);
totalNeutCount += neut;
}
+ if (f.active) {
+ totalActiveFeedCount++;
+ }
}
recountFeeds();
notifyDataSetChanged();
diff --git a/clients/android/NewsBlur/src/com/newsblur/database/QueryCursorLoader.java b/clients/android/NewsBlur/src/com/newsblur/database/QueryCursorLoader.java
index 5cc4b3839..9377fce78 100644
--- a/clients/android/NewsBlur/src/com/newsblur/database/QueryCursorLoader.java
+++ b/clients/android/NewsBlur/src/com/newsblur/database/QueryCursorLoader.java
@@ -4,7 +4,7 @@ import android.content.Context;
import android.database.Cursor;
import android.os.CancellationSignal;
import android.os.OperationCanceledException;
-import android.support.v4.content.AsyncTaskLoader;
+import androidx.loader.content.AsyncTaskLoader;
import com.newsblur.util.AppConstants;
diff --git a/clients/android/NewsBlur/src/com/newsblur/database/ReadingAdapter.java b/clients/android/NewsBlur/src/com/newsblur/database/ReadingAdapter.java
index b3cb9b059..add8a3b5c 100644
--- a/clients/android/NewsBlur/src/com/newsblur/database/ReadingAdapter.java
+++ b/clients/android/NewsBlur/src/com/newsblur/database/ReadingAdapter.java
@@ -3,10 +3,10 @@ package com.newsblur.database;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Parcelable;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentTransaction;
-import android.support.v4.view.PagerAdapter;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.viewpager.widget.PagerAdapter;
import android.view.View;
import android.view.ViewGroup;
@@ -178,7 +178,6 @@ public class ReadingAdapter extends PagerAdapter {
}
}
fragment.setMenuVisibility(false);
- fragment.setUserVisibleHint(false);
if (curTransaction == null) {
curTransaction = fm.beginTransaction();
}
@@ -208,11 +207,9 @@ public class ReadingAdapter extends PagerAdapter {
if (fragment != lastActiveFragment) {
if (lastActiveFragment != null) {
lastActiveFragment.setMenuVisibility(false);
- lastActiveFragment.setUserVisibleHint(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
- fragment.setUserVisibleHint(true);
}
lastActiveFragment = fragment;
}
diff --git a/clients/android/NewsBlur/src/com/newsblur/database/StoryViewAdapter.java b/clients/android/NewsBlur/src/com/newsblur/database/StoryViewAdapter.java
index dec95911a..19b6dd1b9 100644
--- a/clients/android/NewsBlur/src/com/newsblur/database/StoryViewAdapter.java
+++ b/clients/android/NewsBlur/src/com/newsblur/database/StoryViewAdapter.java
@@ -4,8 +4,8 @@ import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Parcelable;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.util.DiffUtil;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.DiffUtil;
import android.text.TextUtils;
import android.view.ContextMenu;
import android.view.GestureDetector;
@@ -126,7 +126,11 @@ public class StoryViewAdapter extends RecyclerView.Adapter feedIds = new HashSet();
@@ -402,23 +406,23 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
FeedUtils.instaFetchFeed(getActivity(), adapter.getFeed(groupPosition, childPosition).feedId);
} else if (item.getItemId() == R.id.menu_intel) {
FeedIntelTrainerFragment intelFrag = FeedIntelTrainerFragment.newInstance(adapter.getFeed(groupPosition, childPosition), adapter.getChild(groupPosition, childPosition));
- intelFrag.show(getFragmentManager(), FeedIntelTrainerFragment.class.getName());
+ intelFrag.show(getParentFragmentManager(), FeedIntelTrainerFragment.class.getName());
} else if (item.getItemId() == R.id.menu_delete_saved_search) {
SavedSearch savedSearch = adapter.getSavedSearch(childPosition);
if (savedSearch != null) {
DialogFragment deleteFeedFragment = DeleteFeedFragment.newInstance(savedSearch);
- deleteFeedFragment.show(getFragmentManager(), "dialog");
+ deleteFeedFragment.show(getParentFragmentManager(), "dialog");
}
} else if (item.getItemId() == R.id.menu_delete_folder) {
Folder folder = adapter.getGroupFolder(groupPosition);
String folderParentName = folder.getFirstParentName();
DeleteFolderFragment deleteFolderFragment = DeleteFolderFragment.newInstance(folder.name, folderParentName);
- deleteFolderFragment.show(getFragmentManager(), deleteFolderFragment.getTag());
+ deleteFolderFragment.show(getParentFragmentManager(), deleteFolderFragment.getTag());
} else if (item.getItemId() == R.id.menu_rename_folder) {
Folder folder = adapter.getGroupFolder(groupPosition);
String folderParentName = folder.getFirstParentName();
RenameDialogFragment renameDialogFragment = RenameDialogFragment.newInstance(folder.name, folderParentName);
- renameDialogFragment.show(getFragmentManager(), renameDialogFragment.getTag());
+ renameDialogFragment.show(getParentFragmentManager(), renameDialogFragment.getTag());
}
return super.onContextItemSelected(item);
@@ -581,6 +585,15 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
return true;
}
+ private void checkAccountFeedsLimit() {
+ new Handler().postDelayed(() -> {
+ if (getActivity() != null && adapter.totalActiveFeedCount > AppConstants.FREE_ACCOUNT_SITE_LIMIT && !PrefsUtils.getIsPremium(getActivity())) {
+ Intent intent = new Intent(getActivity(), MuteConfig.class);
+ startActivity(intent);
+ }
+ }, 2000);
+ }
+
private void openSavedSearch(SavedSearch savedSearch) {
Intent intent = null;
FeedSet fs = null;
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/InfrequentCutoffDialogFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/InfrequentCutoffDialogFragment.java
index 4d3bfd250..448bcbfd6 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/InfrequentCutoffDialogFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/InfrequentCutoffDialogFragment.java
@@ -1,9 +1,9 @@
package com.newsblur.fragment;
import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v4.app.DialogFragment;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/ItemSetFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/ItemSetFragment.java
index 1bbf5571d..ca0fd4f7f 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/ItemSetFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/ItemSetFragment.java
@@ -5,23 +5,24 @@ import android.graphics.Typeface;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Parcelable;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.content.Loader;
-import android.support.v7.widget.GridLayoutManager;
-import android.support.v7.widget.RecyclerView;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.Loader;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.ViewGroup;
-import android.widget.LinearLayout;
+import android.widget.FrameLayout;
import com.newsblur.R;
import com.newsblur.activity.ItemsList;
import com.newsblur.activity.NbActivity;
import com.newsblur.database.StoryViewAdapter;
import com.newsblur.databinding.FragmentItemgridBinding;
+import com.newsblur.databinding.RowFleuronBinding;
import com.newsblur.domain.Story;
import com.newsblur.service.NBSyncService;
import com.newsblur.util.FeedSet;
@@ -57,7 +58,6 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
// loading indicator for when stories are present and fresh (at bottom of list)
protected ProgressThrobber bottomProgressView;
- private View fleuronFooter;
// the fleuron has padding that can't be calculated until after layout, but only changes
// rarely thereafter
private boolean fleuronResized = false;
@@ -69,6 +69,7 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
public boolean fullFlingComplete = false;
private FragmentItemgridBinding binding;
+ private RowFleuronBinding fleuronBinding;
public static ItemSetFragment newInstance() {
ItemSetFragment fragment = new ItemSetFragment();
@@ -80,7 +81,7 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
- getLoaderManager().initLoader(ITEMLIST_LOADER, null, this);
+ LoaderManager.getInstance(this).initLoader(ITEMLIST_LOADER, null, this);
}
@Override
@@ -118,6 +119,8 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_itemgrid, null);
binding = FragmentItemgridBinding.bind(v);
+ View fleuronView = inflater.inflate(R.layout.row_fleuron, null);
+ fleuronBinding = RowFleuronBinding.bind(fleuronView);
// disable the throbbers if animations are going to have a zero time scale
boolean isDisableAnimations = ViewUtils.isPowerSaveMode(getActivity());
@@ -136,8 +139,8 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
UIUtils.getColor(getActivity(), R.color.refresh_3),
UIUtils.getColor(getActivity(), R.color.refresh_4));
- fleuronFooter = inflater.inflate(R.layout.row_fleuron, null);
- fleuronFooter.setVisibility(View.INVISIBLE);
+ fleuronBinding.getRoot().setVisibility(View.INVISIBLE);
+ fleuronBinding.containerSubscribe.setOnClickListener(view -> UIUtils.startPremiumActivity(requireContext()));
binding.itemgridfragmentGrid.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
@@ -165,7 +168,7 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
adapter = new StoryViewAdapter(((NbActivity) getActivity()), this, getFeedSet(), listStyle);
adapter.addFooterView(footerView);
- adapter.addFooterView(fleuronFooter);
+ adapter.addFooterView(fleuronBinding.getRoot());
binding.itemgridfragmentGrid.setAdapter(adapter);
// the layout manager needs to know that the footer rows span all the way across
@@ -231,7 +234,7 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
public void hasUpdated() {
if (isAdded() && !getFeedSet().isMuted()) {
- getLoaderManager().restartLoader(ITEMLIST_LOADER , null, this);
+ LoaderManager.getInstance(this).restartLoader(ITEMLIST_LOADER , null, this);
}
}
@@ -293,9 +296,6 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
}
private void updateLoadingIndicators() {
- // sanity check that we even have views yet
- if (fleuronFooter == null) return;
-
calcFleuronPadding();
if (getFeedSet().isMuted()) {
@@ -307,6 +307,15 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
return;
}
+ if (cursorSeenYet && adapter.getRawStoryCount() > 0 && UIUtils.needsPremiumAccess(requireContext(), getFeedSet())) {
+ fleuronBinding.getRoot().setVisibility(View.VISIBLE);
+ fleuronBinding.containerSubscribe.setVisibility(View.VISIBLE);
+ binding.topLoadingThrob.setVisibility(View.INVISIBLE);
+ bottomProgressView.setVisibility(View.INVISIBLE);
+ fleuronResized = false;
+ return;
+ }
+
if ( (!cursorSeenYet) || NBSyncService.isFeedSetSyncing(getFeedSet(), getActivity()) ) {
binding.emptyViewText.setText(R.string.empty_list_view_loading);
binding.emptyViewText.setTypeface(null, Typeface.ITALIC);
@@ -319,7 +328,7 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
binding.topLoadingThrob.setVisibility(View.VISIBLE);
bottomProgressView.setVisibility(View.GONE);
}
- fleuronFooter.setVisibility(View.INVISIBLE);
+ fleuronBinding.getRoot().setVisibility(View.INVISIBLE);
} else {
ReadFilter readFilter = PrefsUtils.getReadFilter(getActivity(), getFeedSet());
if (readFilter == ReadFilter.UNREAD) {
@@ -333,7 +342,8 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
binding.topLoadingThrob.setVisibility(View.INVISIBLE);
bottomProgressView.setVisibility(View.INVISIBLE);
if (cursorSeenYet && NBSyncService.isFeedSetExhausted(getFeedSet()) && (adapter.getRawStoryCount() > 0)) {
- fleuronFooter.setVisibility(View.VISIBLE);
+ fleuronBinding.containerSubscribe.setVisibility(View.GONE);
+ fleuronBinding.getRoot().setVisibility(View.VISIBLE);
}
}
}
@@ -417,6 +427,9 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
// don't bother checking on scroll up
if (dy < 1) return;
+ // skip fetching more stories if premium access is required
+ if (UIUtils.needsPremiumAccess(requireContext(), getFeedSet()) && adapter.getItemCount() >= 3) return;
+
ensureSufficientStories();
// the list can be scrolled past the last item thanks to the offset footer, but don't fling
@@ -500,21 +513,21 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
* be scrolled until the bottom most story reaches to top, for those who mark-by-scrolling.
*/
private void calcFleuronPadding() {
- if (fleuronResized) return;
+ // sanity check that we even have views yet
+ if (fleuronResized || fleuronBinding.getRoot().getLayoutParams() == null) return;
int listHeight = binding.itemgridfragmentGrid.getMeasuredHeight();
- View innerView = fleuronFooter.findViewById(R.id.fleuron);
- ViewGroup.LayoutParams oldLayout = innerView.getLayoutParams();
- ViewGroup.MarginLayoutParams newLayout = new LinearLayout.LayoutParams(oldLayout);
- int marginPx_4dp = UIUtils.dp2px(getActivity(), 4);
- int defaultPx_100dp = UIUtils.dp2px(getActivity(), 100);
- int bufferPx_50dp = UIUtils.dp2px(getActivity(), 50);
+ ViewGroup.LayoutParams oldLayout = fleuronBinding.getRoot().getLayoutParams();
+ FrameLayout.LayoutParams newLayout = new FrameLayout.LayoutParams(oldLayout);
+ int marginPx_4dp = UIUtils.dp2px(requireContext(), 4);
+ int fleuronFooterHeightPx = fleuronBinding.getRoot().getMeasuredHeight();
if (listHeight > 1) {
- newLayout.setMargins(0, marginPx_4dp, 0, listHeight-bufferPx_50dp);
+ newLayout.setMargins(0, marginPx_4dp, 0, listHeight-fleuronFooterHeightPx);
fleuronResized = true;
} else {
+ int defaultPx_100dp = UIUtils.dp2px(requireContext(), 100);
newLayout.setMargins(0, marginPx_4dp, 0, defaultPx_100dp);
}
- innerView.setLayoutParams(newLayout);
+ fleuronBinding.getRoot().setLayoutParams(newLayout);
}
@Override
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/LoadingFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/LoadingFragment.java
index 3c095b63b..37fadd3cf 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/LoadingFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/LoadingFragment.java
@@ -1,7 +1,7 @@
package com.newsblur.fragment;
import android.os.Bundle;
-import android.support.v4.app.Fragment;
+import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/LoginAsDialogFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/LoginAsDialogFragment.java
index 25ce007d9..4c4a1a5cb 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/LoginAsDialogFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/LoginAsDialogFragment.java
@@ -7,7 +7,7 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
+import androidx.fragment.app.DialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/LoginProgressFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/LoginProgressFragment.java
index 998800ece..c134d866d 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/LoginProgressFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/LoginProgressFragment.java
@@ -5,7 +5,7 @@ import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.graphics.Bitmap;
-import android.support.v4.app.Fragment;
+import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/LoginRegisterFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/LoginRegisterFragment.java
index 6c2e7ed9a..c0cb953fc 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/LoginRegisterFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/LoginRegisterFragment.java
@@ -3,9 +3,9 @@ package com.newsblur.fragment;
import android.content.Intent;
import android.os.Bundle;
import android.net.Uri;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v4.app.Fragment;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -104,7 +104,7 @@ public class LoginRegisterFragment extends Fragment {
Intent i = new Intent(getActivity(), LoginProgress.class);
i.putExtra("username", binding.loginUsername.getText().toString());
- i.putExtra("password", binding.loginUsername.getText().toString());
+ i.putExtra("password", binding.loginPassword.getText().toString());
startActivity(i);
}
}
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/LogoutDialogFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/LogoutDialogFragment.java
index bd3a8ac29..ed2819ceb 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/LogoutDialogFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/LogoutDialogFragment.java
@@ -4,7 +4,7 @@ import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
+import androidx.fragment.app.DialogFragment;
import com.newsblur.R;
import com.newsblur.util.PrefsUtils;
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/NbFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/NbFragment.java
index 1f8e0d9a9..8c439fe07 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/NbFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/NbFragment.java
@@ -1,7 +1,7 @@
package com.newsblur.fragment;
import android.app.Activity;
-import android.support.v4.app.Fragment;
+import androidx.fragment.app.Fragment;
import com.newsblur.util.FeedUtils;
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/ProfileActivityDetailsFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/ProfileActivityDetailsFragment.java
index 765644341..032363d23 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/ProfileActivityDetailsFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/ProfileActivityDetailsFragment.java
@@ -4,7 +4,7 @@ import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
-import android.support.v4.app.Fragment;
+import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/ProfileDetailsFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/ProfileDetailsFragment.java
index def432b46..3f663af29 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/ProfileDetailsFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/ProfileDetailsFragment.java
@@ -4,8 +4,8 @@ import android.content.Context;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.os.Bundle;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
@@ -138,7 +138,7 @@ public class ProfileDetailsFragment extends Fragment implements OnClickListener
followButton.setVisibility(View.GONE);
unfollowButton.setVisibility(View.VISIBLE);
} else {
- FragmentManager fm = ProfileDetailsFragment.this.getFragmentManager();
+ FragmentManager fm = ProfileDetailsFragment.this.getParentFragmentManager();
AlertDialogFragment alertDialog = AlertDialogFragment.newAlertDialogFragment(getResources().getString(R.string.follow_error));
alertDialog.show(fm, "fragment_edit_name");
}
@@ -164,7 +164,7 @@ public class ProfileDetailsFragment extends Fragment implements OnClickListener
unfollowButton.setVisibility(View.GONE);
followButton.setVisibility(View.VISIBLE);
} else {
- FragmentManager fm = ProfileDetailsFragment.this.getFragmentManager();
+ FragmentManager fm = ProfileDetailsFragment.this.getParentFragmentManager();
AlertDialogFragment alertDialog = AlertDialogFragment.newAlertDialogFragment(getResources().getString(R.string.unfollow_error));
alertDialog.show(fm, "fragment_edit_name");
}
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/ReadFilterDialogFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/ReadFilterDialogFragment.java
index e65c8a9f0..b1387c804 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/ReadFilterDialogFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/ReadFilterDialogFragment.java
@@ -1,9 +1,9 @@
package com.newsblur.fragment;
import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v4.app.DialogFragment;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/ReadingActionConfirmationFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/ReadingActionConfirmationFragment.java
index 714f5076d..3632d826b 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/ReadingActionConfirmationFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/ReadingActionConfirmationFragment.java
@@ -7,7 +7,7 @@ import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
+import androidx.fragment.app.DialogFragment;
public class ReadingActionConfirmationFragment extends DialogFragment {
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/ReadingFontDialogFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/ReadingFontDialogFragment.java
index 443dadee1..fa88f9711 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/ReadingFontDialogFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/ReadingFontDialogFragment.java
@@ -1,9 +1,9 @@
package com.newsblur.fragment;
import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v4.app.DialogFragment;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/ReadingItemFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/ReadingItemFragment.java
index e5753444d..8edf8a7a2 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/ReadingItemFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/ReadingItemFragment.java
@@ -14,9 +14,9 @@ import android.graphics.drawable.GradientDrawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v4.app.DialogFragment;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextMenu;
@@ -415,7 +415,7 @@ public class ReadingItemFragment extends NbFragment implements PopupMenu.OnMenuI
private void clickShare() {
DialogFragment newFragment = ShareDialogFragment.newInstance(story, sourceUserId);
- newFragment.show(getFragmentManager(), "dialog");
+ newFragment.show(getParentFragmentManager(), "dialog");
}
private void updateShareButton() {
@@ -483,7 +483,7 @@ public class ReadingItemFragment extends NbFragment implements PopupMenu.OnMenuI
public void onClick(View v) {
if (story.feedId.equals("0")) return; // cannot train on feedless stories
StoryIntelTrainerFragment intelFrag = StoryIntelTrainerFragment.newInstance(story, fs);
- intelFrag.show(getFragmentManager(), StoryIntelTrainerFragment.class.getName());
+ intelFrag.show(getParentFragmentManager(), StoryIntelTrainerFragment.class.getName());
}
});
@@ -492,17 +492,15 @@ public class ReadingItemFragment extends NbFragment implements PopupMenu.OnMenuI
public void onClick(View v) {
if (story.feedId.equals("0")) return; // cannot train on feedless stories
StoryIntelTrainerFragment intelFrag = StoryIntelTrainerFragment.newInstance(story, fs);
- intelFrag.show(getFragmentManager(), StoryIntelTrainerFragment.class.getName());
+ intelFrag.show(getParentFragmentManager(), StoryIntelTrainerFragment.class.getName());
}
});
binding.readingItemTitle.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
- Intent i = new Intent(Intent.ACTION_VIEW);
try {
- i.setData(Uri.parse(story.permalink));
- startActivity(i);
+ UIUtils.handleUri(requireContext(), Uri.parse(story.permalink));
} catch (Throwable t) {
// we don't actually know if the user will successfully be able to open whatever string
// was in the permalink or if the Intent could throw errors
@@ -558,7 +556,7 @@ public class ReadingItemFragment extends NbFragment implements PopupMenu.OnMenuI
public void onClick(View view) {
if (story.feedId.equals("0")) return; // cannot train on feedless stories
StoryIntelTrainerFragment intelFrag = StoryIntelTrainerFragment.newInstance(story, fs);
- intelFrag.show(getFragmentManager(), StoryIntelTrainerFragment.class.getName());
+ intelFrag.show(getParentFragmentManager(), StoryIntelTrainerFragment.class.getName());
}
});
}
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/RegisterProgressFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/RegisterProgressFragment.java
index 9f3308d06..78bca9f32 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/RegisterProgressFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/RegisterProgressFragment.java
@@ -3,9 +3,9 @@ package com.newsblur.fragment;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v4.app.Fragment;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/RenameDialogFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/RenameDialogFragment.java
index 52a9f09a2..380010873 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/RenameDialogFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/RenameDialogFragment.java
@@ -5,8 +5,8 @@ import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.v4.app.DialogFragment;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/ReplyDialogFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/ReplyDialogFragment.java
index b132d67f3..df4093c31 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/ReplyDialogFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/ReplyDialogFragment.java
@@ -5,7 +5,7 @@ import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
+import androidx.fragment.app.DialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/SaveSearchFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/SaveSearchFragment.java
index 305e9bbc9..930bdbb7d 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/SaveSearchFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/SaveSearchFragment.java
@@ -4,8 +4,8 @@ import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.v4.app.DialogFragment;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
import com.newsblur.R;
import com.newsblur.network.APIManager;
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/SetupCommentSectionTask.java b/clients/android/NewsBlur/src/com/newsblur/fragment/SetupCommentSectionTask.java
index 18b202ba7..ffb91718c 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/SetupCommentSectionTask.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/SetupCommentSectionTask.java
@@ -10,8 +10,8 @@ import java.util.Set;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
-import android.support.v4.app.DialogFragment;
-import android.support.v4.app.FragmentManager;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
@@ -28,7 +28,6 @@ import com.newsblur.domain.Reply;
import com.newsblur.domain.Story;
import com.newsblur.domain.UserDetails;
import com.newsblur.domain.UserProfile;
-import com.newsblur.fragment.ReplyDialogFragment;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.UIUtils;
@@ -56,7 +55,7 @@ public class SetupCommentSectionTask extends AsyncTask {
public SetupCommentSectionTask(ReadingItemFragment fragment, View view, LayoutInflater inflater, Story story) {
this.fragment = fragment;
this.context = fragment.getActivity();
- this.manager = fragment.getFragmentManager();
+ this.manager = fragment.getParentFragmentManager();
this.inflater = inflater;
this.story = story;
viewHolder = new WeakReference(view);
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/ShareDialogFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/ShareDialogFragment.java
index 0554c42f9..d99e461ba 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/ShareDialogFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/ShareDialogFragment.java
@@ -5,7 +5,7 @@ import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
+import androidx.fragment.app.DialogFragment;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/StoryIntelTrainerFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/StoryIntelTrainerFragment.java
index 135dc4b8a..7675e9e0b 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/StoryIntelTrainerFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/StoryIntelTrainerFragment.java
@@ -7,7 +7,7 @@ import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
+import androidx.fragment.app.DialogFragment;
import android.text.InputType;
import android.text.TextUtils;
import android.view.Gravity;
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/StoryOrderDialogFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/StoryOrderDialogFragment.java
index 2ae1ef3b9..db343dac8 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/StoryOrderDialogFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/StoryOrderDialogFragment.java
@@ -1,7 +1,7 @@
package com.newsblur.fragment;
import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
+import androidx.fragment.app.DialogFragment;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/TextSizeDialogFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/TextSizeDialogFragment.java
index 5146df77d..922ae8605 100644
--- a/clients/android/NewsBlur/src/com/newsblur/fragment/TextSizeDialogFragment.java
+++ b/clients/android/NewsBlur/src/com/newsblur/fragment/TextSizeDialogFragment.java
@@ -2,7 +2,7 @@ package com.newsblur.fragment;
import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
+import androidx.fragment.app.DialogFragment;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
diff --git a/clients/android/NewsBlur/src/com/newsblur/network/APIConstants.java b/clients/android/NewsBlur/src/com/newsblur/network/APIConstants.java
index c87d92087..a958d10f5 100644
--- a/clients/android/NewsBlur/src/com/newsblur/network/APIConstants.java
+++ b/clients/android/NewsBlur/src/com/newsblur/network/APIConstants.java
@@ -52,6 +52,7 @@ public class APIConstants {
public static final String PATH_MARK_STORY_AS_UNREAD = "/reader/mark_story_as_unread/";
public static final String PATH_MARK_STORY_HASH_UNREAD = "/reader/mark_story_hash_as_unread/";
public static final String PATH_STARRED_STORIES = "/reader/starred_stories";
+ public static final String PATH_STARRED_STORY_HASHES = "/reader/starred_story_hashes";
public static final String PATH_FEED_AUTOCOMPLETE = "/rss_feeds/feed_autocomplete";
public static final String PATH_LIKE_COMMENT = "/social/like_comment";
public static final String PATH_UNLIKE_COMMENT = "/social/remove_like_comment";
@@ -76,6 +77,8 @@ public class APIConstants {
public static final String PATH_ADD_FOLDER = "/reader/add_folder";
public static final String PATH_DELETE_FOLDER = "/reader/delete_folder";
public static final String PATH_RENAME_FOLDER = "/reader/rename_folder";
+ public static final String PATH_SAVE_RECEIPT = "/profile/save_android_receipt";
+ public static final String PATH_FEED_STATISTICS = "/rss_feeds/statistics_embedded/";
public static String buildUrl(String path) {
return CurrentUrlBase + path;
@@ -129,6 +132,8 @@ public class APIConstants {
public static final String PARAMETER_FOLDER_TO_DELETE = "folder_to_delete";
public static final String PARAMETER_FOLDER_TO_RENAME = "folder_to_rename";
public static final String PARAMETER_NEW_FOLDER_NAME = "new_folder_name";
+ public static final String PARAMETER_ORDER_ID = "order_id";
+ public static final String PARAMETER_PRODUCT_ID = "product_id";
public static final String VALUE_PREFIX_SOCIAL = "social:";
public static final String VALUE_ALLSOCIAL = "river:blurblogs"; // the magic value passed to the mark-read API for all social feeds
diff --git a/clients/android/NewsBlur/src/com/newsblur/network/APIManager.java b/clients/android/NewsBlur/src/com/newsblur/network/APIManager.java
index ba2a59cb7..cb92b9f3e 100644
--- a/clients/android/NewsBlur/src/com/newsblur/network/APIManager.java
+++ b/clients/android/NewsBlur/src/com/newsblur/network/APIManager.java
@@ -15,7 +15,7 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
@@ -37,6 +37,7 @@ import com.newsblur.network.domain.LoginResponse;
import com.newsblur.network.domain.NewsBlurResponse;
import com.newsblur.network.domain.ProfileResponse;
import com.newsblur.network.domain.RegisterResponse;
+import com.newsblur.network.domain.StarredStoryHashesResponse;
import com.newsblur.network.domain.StoriesResponse;
import com.newsblur.network.domain.StoryTextResponse;
import com.newsblur.network.domain.UnreadCountResponse;
@@ -279,6 +280,11 @@ public class APIManager {
return (UnreadStoryHashesResponse) response.getResponse(gson, UnreadStoryHashesResponse.class);
}
+ public StarredStoryHashesResponse getStarredStoryHashes() {
+ APIResponse response = get(buildUrl(APIConstants.PATH_STARRED_STORY_HASHES));
+ return response.getResponse(gson, StarredStoryHashesResponse.class);
+ }
+
public StoriesResponse getStoriesByHash(List storyHashes) {
ValueMultimap values = new ValueMultimap();
for (String hash : storyHashes) {
@@ -661,6 +667,14 @@ public class APIManager {
return response.getResponse(gson, NewsBlurResponse.class);
}
+ public NewsBlurResponse saveReceipt(String orderId, String productId) {
+ ContentValues values = new ContentValues();
+ values.put(APIConstants.PARAMETER_ORDER_ID, orderId);
+ values.put(APIConstants.PARAMETER_PRODUCT_ID, productId);
+ APIResponse response = post(buildUrl(APIConstants.PATH_SAVE_RECEIPT), values);
+ return response.getResponse(gson, NewsBlurResponse.class);
+ }
+
/* HTTP METHODS */
private APIResponse get(final String urlString) {
diff --git a/clients/android/NewsBlur/src/com/newsblur/network/SearchAsyncTaskLoader.java b/clients/android/NewsBlur/src/com/newsblur/network/SearchAsyncTaskLoader.java
index 090d1b7b1..bb80c2860 100644
--- a/clients/android/NewsBlur/src/com/newsblur/network/SearchAsyncTaskLoader.java
+++ b/clients/android/NewsBlur/src/com/newsblur/network/SearchAsyncTaskLoader.java
@@ -3,7 +3,7 @@ package com.newsblur.network;
import java.util.ArrayList;
import android.content.Context;
-import android.support.v4.content.AsyncTaskLoader;
+import androidx.loader.content.AsyncTaskLoader;
import com.newsblur.domain.FeedResult;
diff --git a/clients/android/NewsBlur/src/com/newsblur/network/domain/FeedFolderResponse.java b/clients/android/NewsBlur/src/com/newsblur/network/domain/FeedFolderResponse.java
index 01b04f049..d9a5af1b8 100644
--- a/clients/android/NewsBlur/src/com/newsblur/network/domain/FeedFolderResponse.java
+++ b/clients/android/NewsBlur/src/com/newsblur/network/domain/FeedFolderResponse.java
@@ -35,14 +35,14 @@ public class FeedFolderResponse {
public boolean isAuthenticated;
public boolean isPremium;
+ public long premiumExpire;
public boolean isStaff;
public int starredCount;
public FeedFolderResponse(String json, Gson gson) {
long startTime = System.currentTimeMillis();
- JsonParser parser = new JsonParser();
- JsonObject asJsonObject = parser.parse(json).getAsJsonObject();
+ JsonObject asJsonObject = JsonParser.parseString(json).getAsJsonObject();
this.isAuthenticated = asJsonObject.get("authenticated").getAsBoolean();
if (asJsonObject.has("is_staff")) {
@@ -53,6 +53,7 @@ public class FeedFolderResponse {
if (userProfile != null) {
JsonObject profile = (JsonObject) userProfile;
this.isPremium = profile.get("is_premium").getAsBoolean();
+ this.premiumExpire = profile.get("premium_expire").getAsLong();
}
JsonElement starredCountElement = asJsonObject.get("starred_count");
@@ -127,7 +128,7 @@ public class FeedFolderResponse {
/**
* Parses a folder, which is a list of feeds and/or more folders.
*
- * @param parentName folder that surrounded this folder.
+ * @param parentNames folder that surrounded this folder.
* @param name the name of this folder or null if root.
* @param arrayValue the contents to be parsed.
*/
diff --git a/clients/android/NewsBlur/src/com/newsblur/network/domain/StarredStoryHashesResponse.kt b/clients/android/NewsBlur/src/com/newsblur/network/domain/StarredStoryHashesResponse.kt
new file mode 100644
index 000000000..215c63097
--- /dev/null
+++ b/clients/android/NewsBlur/src/com/newsblur/network/domain/StarredStoryHashesResponse.kt
@@ -0,0 +1,7 @@
+package com.newsblur.network.domain
+
+import com.google.gson.annotations.SerializedName
+
+data class StarredStoryHashesResponse(
+ @SerializedName("starred_story_hashes")
+ val starredStoryHashes: Set = HashSet()) : NewsBlurResponse()
\ No newline at end of file
diff --git a/clients/android/NewsBlur/src/com/newsblur/serialization/StoryTypeAdapter.java b/clients/android/NewsBlur/src/com/newsblur/serialization/StoryTypeAdapter.java
index 3a6f55e41..6f8dd6f77 100644
--- a/clients/android/NewsBlur/src/com/newsblur/serialization/StoryTypeAdapter.java
+++ b/clients/android/NewsBlur/src/com/newsblur/serialization/StoryTypeAdapter.java
@@ -24,7 +24,7 @@ public class StoryTypeAdapter implements JsonDeserializer {
// any characters we don't want in the short description, such as newlines or placeholders
private final static Pattern ShortContentExcludes = Pattern.compile("[\\uFFFC\\u000A\\u000B\\u000C\\u000D]");
- private final static Pattern httpSniff = Pattern.compile("(?:http):\\/\\/");
+ private final static Pattern httpSniff = Pattern.compile("(?:http):\\//");
public StoryTypeAdapter() {
this.gson = new GsonBuilder()
@@ -43,9 +43,11 @@ public class StoryTypeAdapter implements JsonDeserializer {
// replace http image urls with https
if (httpSniff.matcher(story.content).find() && story.secureImageUrls != null && story.secureImageUrls.size() > 0) {
- for (String httpUrl : story.secureImageUrls.keySet()) {
- String httpsUrl = story.secureImageUrls.get(httpUrl);
- story.content = story.content.replace(httpUrl, httpsUrl);
+ for (String url : story.secureImageUrls.keySet()) {
+ if (httpSniff.matcher(url).find()) {
+ String httpsUrl = story.secureImageUrls.get(url);
+ story.content = story.content.replace(url, httpsUrl);
+ }
}
}
diff --git a/clients/android/NewsBlur/src/com/newsblur/service/NBSyncService.java b/clients/android/NewsBlur/src/com/newsblur/service/NBSyncService.java
index 361538c7b..a0b5bdc29 100644
--- a/clients/android/NewsBlur/src/com/newsblur/service/NBSyncService.java
+++ b/clients/android/NewsBlur/src/com/newsblur/service/NBSyncService.java
@@ -133,6 +133,7 @@ public class NBSyncService extends JobService {
private List outstandingStartParams = new ArrayList();
private boolean mainSyncRunning = false;
CleanupService cleanupService;
+ StarredService starredService;
OriginalTextService originalTextService;
UnreadsService unreadsService;
ImagePrefetchService imagePrefetchService;
@@ -166,6 +167,7 @@ public class NBSyncService extends JobService {
dbHelper = new BlurDatabaseHelper(this);
iconCache = FileCache.asIconCache(this);
cleanupService = new CleanupService(this);
+ starredService = new StarredService(this);
originalTextService = new OriginalTextService(this);
unreadsService = new UnreadsService(this);
imagePrefetchService = new ImagePrefetchService(this);
@@ -530,6 +532,8 @@ public class NBSyncService extends JobService {
isPremium = feedResponse.isPremium;
isStaff = feedResponse.isStaff;
+ PrefsUtils.setPremium(this, feedResponse.isPremium, feedResponse.premiumExpire);
+
// note all feeds that belong to some folder so we can find orphans
for (Folder folder : feedResponse.folders) {
debugFeedIdsFromFolders.addAll(folder.feedIds);
@@ -610,6 +614,7 @@ public class NBSyncService extends JobService {
UnreadsService.doMetadata();
unreadsService.start();
cleanupService.start();
+ starredService.start();
} finally {
FFSyncRunning = false;
@@ -950,6 +955,7 @@ public class NBSyncService extends JobService {
//Log.d(this, "checking completion");
if (mainSyncRunning) return;
if ((cleanupService != null) && cleanupService.isRunning()) return;
+ if ((starredService != null) && starredService.isRunning()) return;
if ((originalTextService != null) && originalTextService.isRunning()) return;
if ((unreadsService != null) && unreadsService.isRunning()) return;
if ((imagePrefetchService != null) && imagePrefetchService.isRunning()) return;
@@ -1034,6 +1040,7 @@ public class NBSyncService extends JobService {
if (HousekeepingRunning) return context.getResources().getString(R.string.sync_status_housekeeping);
if (FFSyncRunning) return context.getResources().getString(R.string.sync_status_ffsync);
if (CleanupService.activelyRunning) return context.getResources().getString(R.string.sync_status_cleanup);
+ if (StarredService.activelyRunning) return context.getResources().getString(R.string.sync_status_starred);
if (brief && !AppConstants.VERBOSE_LOG) return null;
if (ActionsRunning) return String.format(context.getResources().getString(R.string.sync_status_actions), lastActionCount);
if (RecountsRunning) return context.getResources().getString(R.string.sync_status_recounts);
@@ -1194,6 +1201,7 @@ public class NBSyncService extends JobService {
}
if (cleanupService != null) cleanupService.shutdown();
if (unreadsService != null) unreadsService.shutdown();
+ if (starredService != null) starredService.shutdown();
if (originalTextService != null) originalTextService.shutdown();
if (imagePrefetchService != null) imagePrefetchService.shutdown();
if (primaryExecutor != null) {
diff --git a/clients/android/NewsBlur/src/com/newsblur/service/StarredService.kt b/clients/android/NewsBlur/src/com/newsblur/service/StarredService.kt
new file mode 100644
index 000000000..c90423cd1
--- /dev/null
+++ b/clients/android/NewsBlur/src/com/newsblur/service/StarredService.kt
@@ -0,0 +1,37 @@
+package com.newsblur.service
+
+class StarredService(parent: NBSyncService) : SubService(parent) {
+
+ companion object {
+ @JvmField
+ var activelyRunning = false
+ }
+
+ override fun exec() {
+ activelyRunning = true
+
+ if (parent.stopSync()) return
+
+ // get all starred story hashes from remote db
+ val starredHashesResponse = parent.apiManager.starredStoryHashes
+
+ if (parent.stopSync()) return
+
+ // get all starred story hashes from local db
+ val localStoryHashes = parent.dbHelper.starredStoryHashes
+
+ if (parent.stopSync()) return
+
+ val newStarredHashes = starredHashesResponse.starredStoryHashes.minus(localStoryHashes)
+ val invalidStarredHashes = localStoryHashes.minus(starredHashesResponse.starredStoryHashes)
+
+ if (newStarredHashes.isNotEmpty()) {
+ parent.dbHelper.markStoryHashesStarred(newStarredHashes, true)
+ }
+ if (invalidStarredHashes.isNotEmpty()) {
+ parent.dbHelper.markStoryHashesStarred(invalidStarredHashes, false)
+ }
+
+ activelyRunning = false
+ }
+}
\ No newline at end of file
diff --git a/clients/android/NewsBlur/src/com/newsblur/service/TimeChangeReceiver.java b/clients/android/NewsBlur/src/com/newsblur/service/TimeChangeReceiver.java
index dc15599cb..4c57112e7 100644
--- a/clients/android/NewsBlur/src/com/newsblur/service/TimeChangeReceiver.java
+++ b/clients/android/NewsBlur/src/com/newsblur/service/TimeChangeReceiver.java
@@ -3,7 +3,7 @@ package com.newsblur.service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.newsblur.widget.WidgetUtils;
diff --git a/clients/android/NewsBlur/src/com/newsblur/util/AppConstants.java b/clients/android/NewsBlur/src/com/newsblur/util/AppConstants.java
index e37457a39..132426f4e 100644
--- a/clients/android/NewsBlur/src/com/newsblur/util/AppConstants.java
+++ b/clients/android/NewsBlur/src/com/newsblur/util/AppConstants.java
@@ -92,4 +92,13 @@ public class AppConstants {
// link to the web-based forgot password flow
public final static String FORGOT_PASWORD_URL = "http://www.newsblur.com/folder_rss/forgot_password";
+ // Shiloh photo
+ public final static String SHILOH_PHOTO_URL = "https://newsblur.com/media//img/reader/shiloh.jpg";
+
+ // Premium subscription SKU
+ public final static String PREMIUM_SKU = "nb.premium.36";
+
+ // Free standard account sites limit
+ public final static int FREE_ACCOUNT_SITE_LIMIT = 64;
+
}
diff --git a/clients/android/NewsBlur/src/com/newsblur/util/BetterLinkMovementMethod.java b/clients/android/NewsBlur/src/com/newsblur/util/BetterLinkMovementMethod.java
new file mode 100644
index 000000000..271c87fa9
--- /dev/null
+++ b/clients/android/NewsBlur/src/com/newsblur/util/BetterLinkMovementMethod.java
@@ -0,0 +1,202 @@
+package com.newsblur.util;
+
+import android.graphics.RectF;
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.method.LinkMovementMethod;
+import android.text.style.ClickableSpan;
+import android.text.style.URLSpan;
+import android.text.util.Linkify;
+import android.view.MotionEvent;
+import android.widget.TextView;
+
+public class BetterLinkMovementMethod extends LinkMovementMethod {
+
+ private static final int LINKIFY_NONE = -2;
+
+ private OnLinkClickListener onLinkClickListener;
+ private final RectF touchedLineBounds = new RectF();
+ private ClickableSpan clickableSpanUnderTouchOnActionDown;
+ private int activeTextViewHashcode;
+
+ public interface OnLinkClickListener {
+ /**
+ * @param textView The TextView on which a click was registered.
+ * @param url The clicked URL.
+ * @return True if this click was handled. False to let Android handle the URL.
+ */
+ boolean onClick(TextView textView, String url);
+ }
+
+ /**
+ * Return a new instance of BetterLinkMovementMethod.
+ */
+ public static BetterLinkMovementMethod newInstance() {
+ return new BetterLinkMovementMethod();
+ }
+
+ /**
+ * @param linkifyMask One of {@link Linkify#ALL}, {@link Linkify#PHONE_NUMBERS}, {@link Linkify#MAP_ADDRESSES},
+ * {@link Linkify#WEB_URLS} and {@link Linkify#EMAIL_ADDRESSES}.
+ * @param textViews The TextViews on which a {@link BetterLinkMovementMethod} should be registered.
+ * @return The registered {@link BetterLinkMovementMethod} on the TextViews.
+ */
+ public static BetterLinkMovementMethod linkify(int linkifyMask, TextView... textViews) {
+ BetterLinkMovementMethod movementMethod = newInstance();
+ for (TextView textView : textViews) {
+ addLinks(linkifyMask, movementMethod, textView);
+ }
+ return movementMethod;
+ }
+
+ private BetterLinkMovementMethod() {
+ }
+
+ /**
+ * Set a listener that will get called whenever any link is clicked on the TextView.
+ */
+ public void setOnLinkClickListener(OnLinkClickListener clickListener) {
+ this.onLinkClickListener = clickListener;
+ }
+
+ private static void addLinks(int linkifyMask, BetterLinkMovementMethod movementMethod, TextView textView) {
+ textView.setMovementMethod(movementMethod);
+ if (linkifyMask != LINKIFY_NONE) {
+ Linkify.addLinks(textView, linkifyMask);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(final TextView textView, Spannable text, MotionEvent event) {
+ if (activeTextViewHashcode != textView.hashCode()) {
+ // Bug workaround: TextView stops calling onTouchEvent() once any URL is highlighted.
+ // A hacky solution is to reset any "autoLink" property set in XML. But we also want
+ // to do this once per TextView.
+ activeTextViewHashcode = textView.hashCode();
+ textView.setAutoLinkMask(0);
+ }
+
+ final ClickableSpan clickableSpanUnderTouch = findClickableSpanUnderTouch(textView, text, event);
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ clickableSpanUnderTouchOnActionDown = clickableSpanUnderTouch;
+ }
+ final boolean touchStartedOverAClickableSpan = clickableSpanUnderTouchOnActionDown != null;
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+
+ case MotionEvent.ACTION_MOVE:
+ return touchStartedOverAClickableSpan;
+
+ case MotionEvent.ACTION_UP:
+ // Register a click only if the touch started and ended on the same URL.
+ if (touchStartedOverAClickableSpan && clickableSpanUnderTouch == clickableSpanUnderTouchOnActionDown) {
+ dispatchUrlClick(textView, clickableSpanUnderTouch);
+ }
+ cleanupOnTouchUp();
+
+ // Consume this event even if we could not find any spans to avoid letting Android handle this event.
+ // Android's TextView implementation has a bug where links get clicked even when there is no more text
+ // next to the link and the touch lies outside its bounds in the same direction.
+ return touchStartedOverAClickableSpan;
+
+ case MotionEvent.ACTION_CANCEL:
+ cleanupOnTouchUp();
+ return false;
+
+ default:
+ return false;
+ }
+ }
+
+ private void cleanupOnTouchUp() {
+ clickableSpanUnderTouchOnActionDown = null;
+ }
+
+ /**
+ * Determines the touched location inside the TextView's text and returns the ClickableSpan found under it (if any).
+ *
+ * @return The touched ClickableSpan or null.
+ */
+ protected ClickableSpan findClickableSpanUnderTouch(TextView textView, Spannable text, MotionEvent event) {
+ // So we need to find the location in text where touch was made, regardless of whether the TextView
+ // has scrollable text. That is, not the entire text is currently visible.
+ int touchX = (int) event.getX();
+ int touchY = (int) event.getY();
+
+ // Ignore padding.
+ touchX -= textView.getTotalPaddingLeft();
+ touchY -= textView.getTotalPaddingTop();
+
+ // Account for scrollable text.
+ touchX += textView.getScrollX();
+ touchY += textView.getScrollY();
+
+ final Layout layout = textView.getLayout();
+ final int touchedLine = layout.getLineForVertical(touchY);
+ final int touchOffset = layout.getOffsetForHorizontal(touchedLine, touchX);
+
+ touchedLineBounds.left = layout.getLineLeft(touchedLine);
+ touchedLineBounds.top = layout.getLineTop(touchedLine);
+ touchedLineBounds.right = layout.getLineWidth(touchedLine) + touchedLineBounds.left;
+ touchedLineBounds.bottom = layout.getLineBottom(touchedLine);
+
+ if (touchedLineBounds.contains(touchX, touchY)) {
+ // Find a ClickableSpan that lies under the touched area.
+ final Object[] spans = text.getSpans(touchOffset, touchOffset, ClickableSpan.class);
+ for (final Object span : spans) {
+ if (span instanceof ClickableSpan) {
+ return (ClickableSpan) span;
+ }
+ }
+ // No ClickableSpan found under the touched location.
+
+ }
+ return null;
+ }
+
+ protected void dispatchUrlClick(TextView textView, ClickableSpan clickableSpan) {
+ ClickableSpanWithText clickableSpanWithText = ClickableSpanWithText.ofSpan(textView, clickableSpan);
+ boolean handled = onLinkClickListener != null && onLinkClickListener.onClick(textView, clickableSpanWithText.text());
+
+ if (!handled) {
+ // Let Android handle this click.
+ clickableSpanWithText.span().onClick(textView);
+ }
+ }
+
+ /**
+ * A wrapper to support all {@link ClickableSpan}s that may or may not provide URLs.
+ */
+ protected static class ClickableSpanWithText {
+ private ClickableSpan span;
+ private String text;
+
+ protected static ClickableSpanWithText ofSpan(TextView textView, ClickableSpan span) {
+ Spanned s = (Spanned) textView.getText();
+ String text;
+ if (span instanceof URLSpan) {
+ text = ((URLSpan) span).getURL();
+ } else {
+ int start = s.getSpanStart(span);
+ int end = s.getSpanEnd(span);
+ text = s.subSequence(start, end).toString();
+ }
+ return new ClickableSpanWithText(span, text);
+ }
+
+ protected ClickableSpanWithText(ClickableSpan span, String text) {
+ this.span = span;
+ this.text = text;
+ }
+
+ protected ClickableSpan span() {
+ return span;
+ }
+
+ protected String text() {
+ return text;
+ }
+ }
+}
diff --git a/clients/android/NewsBlur/src/com/newsblur/util/FeedSet.java b/clients/android/NewsBlur/src/com/newsblur/util/FeedSet.java
index 30c90bad4..3f1cc4649 100644
--- a/clients/android/NewsBlur/src/com/newsblur/util/FeedSet.java
+++ b/clients/android/NewsBlur/src/com/newsblur/util/FeedSet.java
@@ -1,6 +1,6 @@
package com.newsblur.util;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import android.text.TextUtils;
import java.io.Serializable;
diff --git a/clients/android/NewsBlur/src/com/newsblur/util/FeedUtils.java b/clients/android/NewsBlur/src/com/newsblur/util/FeedUtils.java
index 070465bfe..5ebdbef4b 100644
--- a/clients/android/NewsBlur/src/com/newsblur/util/FeedUtils.java
+++ b/clients/android/NewsBlur/src/com/newsblur/util/FeedUtils.java
@@ -8,8 +8,9 @@ import java.util.Set;
import android.content.Context;
import android.content.Intent;
+import android.net.Uri;
import android.os.AsyncTask;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import android.text.TextUtils;
import com.newsblur.R;
@@ -22,6 +23,7 @@ import com.newsblur.domain.SocialFeed;
import com.newsblur.domain.StarredCount;
import com.newsblur.domain.Story;
import com.newsblur.fragment.ReadingActionConfirmationFragment;
+import com.newsblur.network.APIConstants;
import com.newsblur.network.APIManager;
import com.newsblur.network.domain.NewsBlurResponse;
import com.newsblur.service.NBSyncService;
@@ -594,4 +596,9 @@ public class FeedUtils {
public static StarredCount getStarredFeedByTag(String feedId) {
return dbHelper.getStarredFeedByTag(feedId);
}
+
+ public static void openStatistics(Context context, String feedId) {
+ String url = APIConstants.buildUrl(APIConstants.PATH_FEED_STATISTICS + feedId);
+ UIUtils.handleUri(context, Uri.parse(url));
+ }
}
diff --git a/clients/android/NewsBlur/src/com/newsblur/util/GestureAction.java b/clients/android/NewsBlur/src/com/newsblur/util/GestureAction.java
index b39c0b9e2..8d22bec1e 100644
--- a/clients/android/NewsBlur/src/com/newsblur/util/GestureAction.java
+++ b/clients/android/NewsBlur/src/com/newsblur/util/GestureAction.java
@@ -6,6 +6,7 @@ public enum GestureAction {
GEST_ACTION_MARKREAD,
GEST_ACTION_MARKUNREAD,
GEST_ACTION_SAVE,
- GEST_ACTION_UNSAVE;
+ GEST_ACTION_UNSAVE,
+ GEST_ACTION_STATISTICS
}
diff --git a/clients/android/NewsBlur/src/com/newsblur/util/NotificationUtils.java b/clients/android/NewsBlur/src/com/newsblur/util/NotificationUtils.java
index 8bac4bd04..79eaac70f 100644
--- a/clients/android/NewsBlur/src/com/newsblur/util/NotificationUtils.java
+++ b/clients/android/NewsBlur/src/com/newsblur/util/NotificationUtils.java
@@ -9,16 +9,15 @@ import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.os.Build;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.app.NotificationManagerCompat;
-import android.util.Pair;
+
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
import com.newsblur.R;
import com.newsblur.activity.FeedReading;
import com.newsblur.activity.Reading;
import com.newsblur.database.DatabaseConstants;
import com.newsblur.domain.Story;
-import com.newsblur.util.FileCache;
public class NotificationUtils {
@@ -46,6 +45,11 @@ public class NotificationUtils {
nm.cancel(story.hashCode());
continue;
}
+ if (StoryUtils.hasOldTimestamp(story.timestamp)) {
+ FeedUtils.dbHelper.putStoryDismissed(story.storyHash);
+ nm.cancel(story.hashCode());
+ continue;
+ }
if (count < MAX_CONCUR_NOTIFY) {
Notification n = buildStoryNotification(story, storiesFocus, context, iconCache);
nm.notify(story.hashCode(), n);
@@ -65,6 +69,11 @@ public class NotificationUtils {
nm.cancel(story.hashCode());
continue;
}
+ if (StoryUtils.hasOldTimestamp(story.timestamp)) {
+ FeedUtils.dbHelper.putStoryDismissed(story.storyHash);
+ nm.cancel(story.hashCode());
+ continue;
+ }
if (count < MAX_CONCUR_NOTIFY) {
Notification n = buildStoryNotification(story, storiesUnread, context, iconCache);
nm.notify(story.hashCode(), n);
@@ -133,15 +142,14 @@ public class NotificationUtils {
.setContentIntent(pendingIntent)
.setDeleteIntent(dismissPendingIntent)
.setAutoCancel(true)
+ .setOnlyAlertOnce(true)
.setWhen(story.timestamp)
.addAction(0, "Save", savePendingIntent)
- .addAction(0, "Mark Read", markreadPendingIntent);
+ .addAction(0, "Mark Read", markreadPendingIntent)
+ .setColor(NOTIFY_COLOUR);
if (feedIcon != null) {
nb.setLargeIcon(feedIcon);
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- nb.setColor(NOTIFY_COLOUR);
- }
return nb.build();
}
@@ -155,6 +163,4 @@ public class NotificationUtils {
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel(nid);
}
-
-
}
diff --git a/clients/android/NewsBlur/src/com/newsblur/util/PrefConstants.java b/clients/android/NewsBlur/src/com/newsblur/util/PrefConstants.java
index 7670c511c..7b6204b5a 100644
--- a/clients/android/NewsBlur/src/com/newsblur/util/PrefConstants.java
+++ b/clients/android/NewsBlur/src/com/newsblur/util/PrefConstants.java
@@ -25,6 +25,9 @@ public class PrefConstants {
public final static String USER_FOLLOWING_COUNT = "following_count";
public final static String USER_SUBSCRIBER_COUNT = "subscribers_count";
public final static String USER_SHARED_STORIES_COUNT = "shared_stories_count";
+
+ public static final String IS_PREMIUM = "is_premium";
+ public static final String PREMIUM_EXPIRE = "premium_expire";
public static final String PREFERENCE_TEXT_SIZE = "default_reading_text_size";
public static final String PREFERENCE_LIST_TEXT_SIZE = "list_text_size";
@@ -110,8 +113,9 @@ public class PrefConstants {
public static final String READING_FONT = "reading_font";
public static final String WIDGET_FEED_SET = "widget_feed_set";
- public static final String WIDGET_CONFIG_LIST_ORDER = "widget_config_list_order";
- public static final String WIDGET_CONFIG_FEED_ORDER = "widget_config_feed_order";
- public static final String WIDGET_CONFIG_FOLDER_VIEW = "widget_config_folder_view";
+ public static final String FEED_CHOOSER_LIST_ORDER = "feed_chooser_list_order";
+ public static final String FEED_CHOOSER_FEED_ORDER = "feed_chooser_feed_order";
+ public static final String FEED_CHOOSER_FOLDER_VIEW = "feed_chooser_folder_view";
public static final String WIDGET_BACKGROUND = "widget_background";
+ public static final String IN_APP_REVIEW = "in_app_review";
}
diff --git a/clients/android/NewsBlur/src/com/newsblur/util/PrefsUtils.java b/clients/android/NewsBlur/src/com/newsblur/util/PrefsUtils.java
index 5999de390..5aa221045 100644
--- a/clients/android/NewsBlur/src/com/newsblur/util/PrefsUtils.java
+++ b/clients/android/NewsBlur/src/com/newsblur/util/PrefsUtils.java
@@ -14,19 +14,16 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
-import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
-import android.support.annotation.Nullable;
-import android.support.v4.content.FileProvider;
+import androidx.annotation.Nullable;
+import androidx.core.content.FileProvider;
import android.util.Log;
import com.newsblur.R;
@@ -795,7 +792,7 @@ public class PrefsUtils {
public static ThemeValue getSelectedTheme(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
- String value = prefs.getString(PrefConstants.THEME, ThemeValue.LIGHT.name());
+ String value = prefs.getString(PrefConstants.THEME, ThemeValue.AUTO.name());
// check for legacy hard-coded values. this can go away once installs of v152 or earlier are minimized
if (value.equals("light")) {
setSelectedTheme(context, ThemeValue.LIGHT);
@@ -896,54 +893,45 @@ public class PrefsUtils {
if (prefs.contains(PrefConstants.WIDGET_FEED_SET)) {
editor.remove(PrefConstants.WIDGET_FEED_SET);
}
- if (prefs.contains(PrefConstants.WIDGET_CONFIG_FEED_ORDER)) {
- editor.remove(PrefConstants.WIDGET_CONFIG_FEED_ORDER);
- }
- if (prefs.contains(PrefConstants.WIDGET_CONFIG_LIST_ORDER)) {
- editor.remove(PrefConstants.WIDGET_CONFIG_LIST_ORDER);
- }
- if (prefs.contains(PrefConstants.WIDGET_CONFIG_FOLDER_VIEW)) {
- editor.remove(PrefConstants.WIDGET_CONFIG_FOLDER_VIEW);
- }
if (prefs.contains(PrefConstants.WIDGET_BACKGROUND)) {
editor.remove(PrefConstants.WIDGET_BACKGROUND);
}
editor.apply();
}
- public static FeedOrderFilter getWidgetConfigFeedOrder(Context context) {
+ public static FeedOrderFilter getFeedChooserFeedOrder(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
- return FeedOrderFilter.valueOf(preferences.getString(PrefConstants.WIDGET_CONFIG_FEED_ORDER, FeedOrderFilter.NAME.toString()));
+ return FeedOrderFilter.valueOf(preferences.getString(PrefConstants.FEED_CHOOSER_FEED_ORDER, FeedOrderFilter.NAME.toString()));
}
- public static void setWidgetConfigFeedOrder(Context context, FeedOrderFilter feedOrderFilter) {
+ public static void setFeedChooserFeedOrder(Context context, FeedOrderFilter feedOrderFilter) {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
Editor editor = prefs.edit();
- editor.putString(PrefConstants.WIDGET_CONFIG_FEED_ORDER, feedOrderFilter.toString());
+ editor.putString(PrefConstants.FEED_CHOOSER_FEED_ORDER, feedOrderFilter.toString());
editor.commit();
}
- public static ListOrderFilter getWidgetConfigListOrder(Context context) {
+ public static ListOrderFilter getFeedChooserListOrder(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
- return ListOrderFilter.valueOf(preferences.getString(PrefConstants.WIDGET_CONFIG_LIST_ORDER, ListOrderFilter.ASCENDING.name()));
+ return ListOrderFilter.valueOf(preferences.getString(PrefConstants.FEED_CHOOSER_LIST_ORDER, ListOrderFilter.ASCENDING.name()));
}
- public static void setWidgetConfigListOrder(Context context, ListOrderFilter listOrderFilter) {
+ public static void setFeedChooserListOrder(Context context, ListOrderFilter listOrderFilter) {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
Editor editor = prefs.edit();
- editor.putString(PrefConstants.WIDGET_CONFIG_LIST_ORDER, listOrderFilter.toString());
+ editor.putString(PrefConstants.FEED_CHOOSER_LIST_ORDER, listOrderFilter.toString());
editor.commit();
}
- public static FolderViewFilter getWidgetConfigFolderView(Context context) {
+ public static FolderViewFilter getFeedChooserFolderView(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
- return FolderViewFilter.valueOf(preferences.getString(PrefConstants.WIDGET_CONFIG_FOLDER_VIEW, FolderViewFilter.NESTED.name()));
+ return FolderViewFilter.valueOf(preferences.getString(PrefConstants.FEED_CHOOSER_FOLDER_VIEW, FolderViewFilter.NESTED.name()));
}
- public static void setWidgetConfigFolderView(Context context, FolderViewFilter folderViewFilter) {
+ public static void setFeedChooserFolderView(Context context, FolderViewFilter folderViewFilter) {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
Editor editor = prefs.edit();
- editor.putString(PrefConstants.WIDGET_CONFIG_FOLDER_VIEW, folderViewFilter.toString());
+ editor.putString(PrefConstants.FEED_CHOOSER_FOLDER_VIEW, folderViewFilter.toString());
editor.commit();
}
@@ -967,4 +955,36 @@ public class PrefsUtils {
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
return preferences.getString(PrefConstants.DEFAULT_BROWSER, DefaultBrowser.SYSTEM_DEFAULT.toString());
}
+
+ public static void setPremium(Context context, boolean isPremium, Long premiumExpire) {
+ SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
+ Editor editor = prefs.edit();
+ editor.putBoolean(PrefConstants.IS_PREMIUM, isPremium);
+ if (premiumExpire != null) {
+ editor.putLong(PrefConstants.PREMIUM_EXPIRE, premiumExpire);
+ }
+ editor.commit();
+ }
+
+ public static boolean getIsPremium(Context context) {
+ SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
+ return preferences.getBoolean(PrefConstants.IS_PREMIUM, false);
+ }
+
+ public static long getPremiumExpire(Context context) {
+ SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
+ return preferences.getLong(PrefConstants.PREMIUM_EXPIRE, -1);
+ }
+
+ public static boolean hasInAppReviewed(Context context) {
+ SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
+ return preferences.getBoolean(PrefConstants.IN_APP_REVIEW, false);
+ }
+
+ public static void setInAppReviewed(Context context) {
+ SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
+ Editor editor = preferences.edit();
+ editor.putBoolean(PrefConstants.IN_APP_REVIEW, true);
+ editor.commit();
+ }
}
diff --git a/clients/android/NewsBlur/src/com/newsblur/util/StoryUtils.java b/clients/android/NewsBlur/src/com/newsblur/util/StoryUtils.java
index 3a20beaca..b79119bac 100644
--- a/clients/android/NewsBlur/src/com/newsblur/util/StoryUtils.java
+++ b/clients/android/NewsBlur/src/com/newsblur/util/StoryUtils.java
@@ -189,4 +189,8 @@ public class StoryUtils {
return shortDateFormat.get().format(storyDate) +", " + timeFormat.format(storyDate);
}
}
+
+ public static boolean hasOldTimestamp(long storyTimestamp) {
+ return (System.currentTimeMillis() - storyTimestamp) > (2 * DateUtils.DAY_IN_MILLIS);
+ }
}
diff --git a/clients/android/NewsBlur/src/com/newsblur/util/UIUtils.java b/clients/android/NewsBlur/src/com/newsblur/util/UIUtils.java
index 271dfd688..4e72e9439 100644
--- a/clients/android/NewsBlur/src/com/newsblur/util/UIUtils.java
+++ b/clients/android/NewsBlur/src/com/newsblur/util/UIUtils.java
@@ -18,6 +18,7 @@ import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
+import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
@@ -541,4 +542,55 @@ public class UIUtils {
return result;
}
+ public static void handleUri(Context context, Uri uri) {
+ DefaultBrowser defaultBrowser = PrefsUtils.getDefaultBrowser(context);
+ if (defaultBrowser == DefaultBrowser.SYSTEM_DEFAULT) {
+ openSystemDefaultBrowser(context, uri);
+ } else if (defaultBrowser == DefaultBrowser.IN_APP_BROWSER) {
+ Intent intent = new Intent(context, InAppBrowser.class);
+ intent.putExtra(InAppBrowser.URI, uri);
+ context.startActivity(intent);
+ } else if (defaultBrowser == DefaultBrowser.CHROME) {
+ openExternalBrowserApp(context, uri, "com.android.chrome");
+ } else if (defaultBrowser == DefaultBrowser.FIREFOX) {
+ openExternalBrowserApp(context, uri, "org.mozilla.firefox");
+ } else if (defaultBrowser == DefaultBrowser.OPERA_MINI) {
+ openExternalBrowserApp(context, uri, "com.opera.mini.native");
+ }
+ }
+
+ public static void openSystemDefaultBrowser(Context context, Uri uri) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(uri);
+ context.startActivity(intent);
+ } catch (Exception e) {
+ com.newsblur.util.Log.e(context.getClass().getName(), "device cannot open URLs");
+ }
+ }
+
+ public static void openExternalBrowserApp(Context context, Uri uri, String packageName) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(uri);
+ intent.setPackage(packageName);
+ context.startActivity(intent);
+ } catch (Exception e) {
+ com.newsblur.util.Log.e(context.getClass().getName(), "apps not available to open URLs");
+ // fallback to system default if apps cannot be opened
+ openSystemDefaultBrowser(context, uri);
+ }
+ }
+
+ public static boolean needsPremiumAccess(Context context, FeedSet feedSet) {
+ boolean isPremium = PrefsUtils.getIsPremium(context);
+ boolean requiresPremium = feedSet.isFolder() || feedSet.isInfrequent() ||
+ feedSet.isAllNormal() || feedSet.isGlobalShared() || feedSet.isSingleSavedTag();
+ return !isPremium && requiresPremium;
+ }
+
+ public static void startPremiumActivity(Context context) {
+ Intent intent = new Intent(context, Premium.class);
+ context.startActivity(intent);
+ }
}
diff --git a/clients/android/NewsBlur/src/com/newsblur/view/NewsblurWebview.java b/clients/android/NewsBlur/src/com/newsblur/view/NewsblurWebview.java
index 51b661ca5..9093497f7 100644
--- a/clients/android/NewsBlur/src/com/newsblur/view/NewsblurWebview.java
+++ b/clients/android/NewsBlur/src/com/newsblur/view/NewsblurWebview.java
@@ -2,7 +2,6 @@ package com.newsblur.view;
import android.annotation.TargetApi;
import android.content.Context;
-import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.util.AttributeSet;
@@ -17,11 +16,9 @@ import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
-import com.newsblur.activity.InAppBrowser;
import com.newsblur.activity.Reading;
import com.newsblur.fragment.ReadingItemFragment;
-import com.newsblur.util.DefaultBrowser;
-import com.newsblur.util.PrefsUtils;
+import com.newsblur.util.UIUtils;
public class NewsblurWebview extends WebView {
@@ -93,14 +90,14 @@ public class NewsblurWebview extends WebView {
class NewsblurWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
- handleUri(Uri.parse(url));
+ UIUtils.handleUri(context, Uri.parse(url));
return true;
}
@TargetApi(Build.VERSION_CODES.N)
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
- handleUri(request.getUrl());
+ UIUtils.handleUri(context, request.getUrl());
return true;
}
@@ -113,46 +110,6 @@ public class NewsblurWebview extends WebView {
}
}
- private void handleUri(Uri uri) {
- DefaultBrowser defaultBrowser = PrefsUtils.getDefaultBrowser(context);
- if (defaultBrowser == DefaultBrowser.SYSTEM_DEFAULT) {
- openSystemDefaultBrowser(uri);
- } else if (defaultBrowser == DefaultBrowser.IN_APP_BROWSER) {
- Intent intent = new Intent(context, InAppBrowser.class);
- intent.putExtra(InAppBrowser.URI, uri);
- context.startActivity(intent);
- } else if (defaultBrowser == DefaultBrowser.CHROME) {
- openExternalBrowserApp(uri, "com.android.chrome");
- } else if (defaultBrowser == DefaultBrowser.FIREFOX) {
- openExternalBrowserApp(uri, "org.mozilla.firefox");
- } else if (defaultBrowser == DefaultBrowser.OPERA_MINI) {
- openExternalBrowserApp(uri, "com.opera.mini.native");
- }
- }
-
- private void openSystemDefaultBrowser(Uri uri) {
- try {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setData(uri);
- context.startActivity(intent);
- } catch (Exception e) {
- com.newsblur.util.Log.e(this.getClass().getName(), "device cannot open URLs");
- }
- }
-
- private void openExternalBrowserApp(Uri uri, String packageName) {
- try {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setData(uri);
- intent.setPackage(packageName);
- context.startActivity(intent);
- } catch (Exception e) {
- com.newsblur.util.Log.e(this.getClass().getName(), "apps not available to open URLs");
- // fallback to system default if apps cannot be opened
- openSystemDefaultBrowser(uri);
- }
- }
-
// this WCC implements the bare minimum callbacks to get HTML5 fullscreen video working
class NewsblurWebChromeClient extends WebChromeClient {
public View customView;
diff --git a/clients/android/NewsBlur/src/com/newsblur/widget/WidgetProvider.java b/clients/android/NewsBlur/src/com/newsblur/widget/WidgetProvider.java
index c3c69ec12..e65803195 100644
--- a/clients/android/NewsBlur/src/com/newsblur/widget/WidgetProvider.java
+++ b/clients/android/NewsBlur/src/com/newsblur/widget/WidgetProvider.java
@@ -7,7 +7,7 @@ import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
-import android.support.v4.content.ContextCompat;
+import androidx.core.content.ContextCompat;
import android.util.Log;
import com.newsblur.R;
diff --git a/clients/android/NewsBlur/src/com/newsblur/widget/WidgetRemoteViews.java b/clients/android/NewsBlur/src/com/newsblur/widget/WidgetRemoteViews.java
index 6a9144dd5..38d7aef84 100644
--- a/clients/android/NewsBlur/src/com/newsblur/widget/WidgetRemoteViews.java
+++ b/clients/android/NewsBlur/src/com/newsblur/widget/WidgetRemoteViews.java
@@ -1,6 +1,6 @@
package com.newsblur.widget;
-import android.support.annotation.ColorInt;
+import androidx.annotation.ColorInt;
import android.widget.RemoteViews;
class WidgetRemoteViews extends RemoteViews {
diff --git a/clients/android/NewsBlur/src/com/newsblur/widget/WidgetRemoteViewsFactory.java b/clients/android/NewsBlur/src/com/newsblur/widget/WidgetRemoteViewsFactory.java
index 030bf4ab8..804ae785c 100644
--- a/clients/android/NewsBlur/src/com/newsblur/widget/WidgetRemoteViewsFactory.java
+++ b/clients/android/NewsBlur/src/com/newsblur/widget/WidgetRemoteViewsFactory.java
@@ -6,9 +6,9 @@ import android.content.Intent;
import android.database.Cursor;
import android.graphics.Color;
import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v4.content.Loader;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.loader.content.Loader;
import android.text.TextUtils;
import android.view.View;
import android.widget.RemoteViews;
diff --git a/clients/android/NewsBlur/src/com/newsblur/widget/WidgetUpdateReceiver.java b/clients/android/NewsBlur/src/com/newsblur/widget/WidgetUpdateReceiver.java
index 6285f2bf5..af3bf7393 100644
--- a/clients/android/NewsBlur/src/com/newsblur/widget/WidgetUpdateReceiver.java
+++ b/clients/android/NewsBlur/src/com/newsblur/widget/WidgetUpdateReceiver.java
@@ -5,7 +5,7 @@ import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.newsblur.R;
import com.newsblur.util.Log;
diff --git a/clients/ios/Classes/FeedDetailViewController.h b/clients/ios/Classes/FeedDetailViewController.h
index b35bfe712..202998307 100644
--- a/clients/ios/Classes/FeedDetailViewController.h
+++ b/clients/ios/Classes/FeedDetailViewController.h
@@ -50,6 +50,7 @@
@property (nonatomic) IBOutlet UIBarButtonItem * titleImageBarButton;
@property (nonatomic, retain) NBNotifier *notifier;
@property (nonatomic, retain) StoriesCollection *storiesCollection;
+@property (nonatomic) UIRefreshControl *refreshControl;
@property (nonatomic) UISearchBar *searchBar;
@property (nonatomic) IBOutlet UIView *messageView;
@property (nonatomic) IBOutlet UILabel *messageLabel;
diff --git a/clients/ios/Classes/FeedDetailViewController.m b/clients/ios/Classes/FeedDetailViewController.m
index c17719274..348b65565 100644
--- a/clients/ios/Classes/FeedDetailViewController.m
+++ b/clients/ios/Classes/FeedDetailViewController.m
@@ -44,6 +44,8 @@
@property (nonatomic) NSUInteger scrollingMarkReadRow;
@property (nonatomic, readonly) BOOL isMarkReadOnScroll;
+@property (nonatomic, readonly) BOOL canPullToRefresh;
+@property (readwrite) BOOL inPullToRefresh_;
@property (nonatomic, strong) NSString *restoringFolder;
@property (nonatomic, strong) NSString *restoringFeedID;
@@ -104,6 +106,11 @@
initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
spacer2BarButton.width = 0;
+ self.refreshControl = [UIRefreshControl new];
+ self.refreshControl.tintColor = UIColorFromLightDarkRGB(0x0, 0xffffff);
+ self.refreshControl.backgroundColor = UIColorFromRGB(0xE3E6E0);
+ [self.refreshControl addTarget:self action:@selector(refresh:) forControlEvents:UIControlEventValueChanged];
+
self.searchBar = [[UISearchBar alloc]
initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.storyTitlesTable.frame), 44.)];
self.searchBar.delegate = self;
@@ -402,10 +409,6 @@
} else {
feedMarkReadButton.enabled = YES;
}
-
- if (self.isPhoneOrCompact) {
- [self fadeSelectedCell:NO];
- }
[self.notifier setNeedsLayout];
[appDelegate hideShareView:YES];
@@ -437,8 +440,18 @@
[self.searchBar setShowsCancelButton:NO animated:YES];
}
+ if (self.canPullToRefresh) {
+ self.storyTitlesTable.refreshControl = self.refreshControl;
+ } else {
+ self.storyTitlesTable.refreshControl = nil;
+ }
+
[self updateTheme];
+ if (self.isPhoneOrCompact) {
+ [self fadeSelectedCell:NO];
+ }
+
if (storiesCollection.activeFeed != nil) {
[appDelegate donateFeed];
} else if (storiesCollection.activeFolder != nil) {
@@ -1885,7 +1898,7 @@ heightForRowAtIndexPath:(NSIndexPath *)indexPath {
}
CGPoint topRowPoint = self.storyTitlesTable.contentOffset;
- topRowPoint.y = topRowPoint.y + 80.f;
+ topRowPoint.y = topRowPoint.y + (self.textSize != FeedDetailTextSizeTitleOnly ? 80.f : 60.f);
NSIndexPath *indexPath = [self.storyTitlesTable indexPathForRowAtPoint:topRowPoint];
BOOL markReadOnScroll = self.isMarkReadOnScroll;
@@ -2737,10 +2750,13 @@ didEndSwipingSwipingWithState:(MCSwipeTableViewCellState)state
[super updateTheme];
self.navigationController.navigationBar.tintColor = [UINavigationBar appearance].tintColor;
- self.navigationController.navigationBar.backItem.backBarButtonItem.tintColor = UIColorFromRGB(0x8F918B);
+// self.navigationController.navigationBar.backItem.backBarButtonItem.tintColor = UIColorFromRGB(0x8F918B);
self.navigationController.navigationBar.barTintColor = [UINavigationBar appearance].barTintColor;
self.navigationController.toolbar.barTintColor = [UINavigationBar appearance].barTintColor;
+ self.refreshControl.tintColor = UIColorFromLightDarkRGB(0x0, 0xffffff);
+ self.refreshControl.backgroundColor = UIColorFromRGB(0xE3E6E0);
+
self.searchBar.backgroundColor = UIColorFromRGB(0xE3E6E0);
self.searchBar.tintColor = UIColorFromRGB(0xffffff);
self.searchBar.nb_searchField.textColor = UIColorFromRGB(NEWSBLUR_BLACK_COLOR);
@@ -2806,10 +2822,12 @@ didEndSwipingSwipingWithState:(MCSwipeTableViewCellState)state
// [self cancelRequests];
[appDelegate GET:urlString parameters:nil success:^(NSURLSessionTask *task, id responseObject) {
[self renderStories:[responseObject objectForKey:@"stories"]];
+ [self finishRefresh];
} failure:^(NSURLSessionTask *operation, NSError *error) {
NSLog(@"Fail: %@", error);
[self informError:[operation error]];
[self fetchFeedDetail:1 withCallback:nil];
+ [self finishRefresh];
}];
[storiesCollection setStories:nil];
@@ -2819,6 +2837,32 @@ didEndSwipingSwipingWithState:(MCSwipeTableViewCellState)state
[storyTitlesTable scrollRectToVisible:CGRectMake(0, CGRectGetHeight(self.searchBar.frame), 1, 1) animated:YES];
}
+#pragma mark -
+#pragma mark PullToRefresh
+
+- (BOOL)canPullToRefresh {
+ BOOL river = appDelegate.storiesCollection.isRiverView;
+ BOOL infrequent = [self isInfrequent];
+ BOOL read = appDelegate.storiesCollection.isReadView;
+ BOOL saved = appDelegate.storiesCollection.isSavedView;
+
+ return appDelegate.storiesCollection.activeFeed != nil && !river && !infrequent && !saved && !read;
+}
+
+- (void)refresh:(UIRefreshControl *)refreshControl {
+ if (self.canPullToRefresh) {
+ self.inPullToRefresh_ = YES;
+ [self instafetchFeed];
+ } else {
+ [self finishRefresh];
+ }
+}
+
+- (void)finishRefresh {
+ self.inPullToRefresh_ = NO;
+ [self.refreshControl endRefreshing];
+}
+
#pragma mark -
#pragma mark loadSocial Feeds
diff --git a/clients/ios/Classes/FeedsMenuViewController.m b/clients/ios/Classes/FeedsMenuViewController.m
index 0392a9666..f74e54595 100644
--- a/clients/ios/Classes/FeedsMenuViewController.m
+++ b/clients/ios/Classes/FeedsMenuViewController.m
@@ -315,7 +315,7 @@
[[ThemeManager themeManager] updateSegmentedControl:self.fontSizeSegment];
- [cell addSubview:self.fontSizeSegment];
+ [cell.contentView addSubview:self.fontSizeSegment];
return cell;
}
@@ -370,7 +370,7 @@
self.themeSegmentedControl.selectedSegmentTintColor = [UIColor clearColor];
}
- [cell addSubview:self.themeSegmentedControl];
+ [cell.contentView addSubview:self.themeSegmentedControl];
return cell;
}
diff --git a/clients/ios/Classes/FontSettingsViewController.m b/clients/ios/Classes/FontSettingsViewController.m
index 9eff986b6..8b48fc9df 100644
--- a/clients/ios/Classes/FontSettingsViewController.m
+++ b/clients/ios/Classes/FontSettingsViewController.m
@@ -476,7 +476,7 @@
[[ThemeManager themeManager] updateSegmentedControl:self.fontSizeSegment];
- [cell addSubview:self.fontSizeSegment];
+ [cell.contentView addSubview:self.fontSizeSegment];
return cell;
}
@@ -498,7 +498,7 @@
[[ThemeManager themeManager] updateSegmentedControl:self.lineSpacingSegment];
- [cell addSubview:self.lineSpacingSegment];
+ [cell.contentView addSubview:self.lineSpacingSegment];
return cell;
}
@@ -520,7 +520,7 @@
[[ThemeManager themeManager] updateSegmentedControl:self.fullscreenSegment];
- [cell addSubview:self.fullscreenSegment];
+ [cell.contentView addSubview:self.fullscreenSegment];
return cell;
}
@@ -542,7 +542,7 @@
[[ThemeManager themeManager] updateSegmentedControl:self.autoscrollSegment];
- [cell addSubview:self.autoscrollSegment];
+ [cell.contentView addSubview:self.autoscrollSegment];
return cell;
}
@@ -564,7 +564,7 @@
[[ThemeManager themeManager] updateSegmentedControl:self.scrollOrientationSegment];
- [cell addSubview:self.scrollOrientationSegment];
+ [cell.contentView addSubview:self.scrollOrientationSegment];
return cell;
}
@@ -597,7 +597,7 @@
[[ThemeManager themeManager] updateThemeSegmentedControl:self.themeSegment];
- [cell addSubview:self.themeSegment];
+ [cell.contentView addSubview:self.themeSegment];
return cell;
}
diff --git a/clients/ios/Classes/MenuViewController.m b/clients/ios/Classes/MenuViewController.m
index aa0d1b123..3e3397f1e 100644
--- a/clients/ios/Classes/MenuViewController.m
+++ b/clients/ios/Classes/MenuViewController.m
@@ -176,7 +176,7 @@ NSString * const MenuHandler = @"handler";
segmentedControl.selectedSegmentIndex = valueIndex;
- [cell addSubview:segmentedControl];
+ [cell.contentView addSubview:segmentedControl];
return cell;
}
@@ -214,7 +214,7 @@ NSString * const MenuHandler = @"handler";
[[ThemeManager themeManager] updateSegmentedControl:segmentedControl];
- [cell addSubview:segmentedControl];
+ [cell.contentView addSubview:segmentedControl];
return cell;
}
diff --git a/clients/ios/Classes/NBNotifier.h b/clients/ios/Classes/NBNotifier.h
index 106c535c3..6456029c7 100644
--- a/clients/ios/Classes/NBNotifier.h
+++ b/clients/ios/Classes/NBNotifier.h
@@ -35,6 +35,7 @@ typedef enum {
@property (nonatomic, strong) UIView *accessoryView;
@property (nonatomic, strong) NSString *title;
@property (nonatomic, assign) BOOL showing;
+@property (nonatomic, assign) BOOL pendingHide;
@property (nonatomic, retain) UIView *progressBar;
@property (nonatomic) NSLayoutConstraint *topOffsetConstraint;
diff --git a/clients/ios/Classes/NBNotifier.m b/clients/ios/Classes/NBNotifier.m
index 446ba97d9..bd6de00f6 100644
--- a/clients/ios/Classes/NBNotifier.m
+++ b/clients/ios/Classes/NBNotifier.m
@@ -223,7 +223,8 @@
return;
}
- showing = YES;
+ self.showing = YES;
+ self.pendingHide = NO;
// CGRect frame = self.frame;
// frame.size.width = self.view.frame.size.width;
// self.frame = frame;
@@ -252,6 +253,7 @@
- (void)hideIn:(float)seconds {
if (!self.window) {
+ self.pendingHide = YES;
return;
}
@@ -267,7 +269,8 @@
// self.hidden = YES;
}];
- showing = NO;
+ self.showing = NO;
+ self.pendingHide = NO;
}
- (void)drawRect:(CGRect)rect{
diff --git a/clients/ios/Classes/NewsBlurAppDelegate.m b/clients/ios/Classes/NewsBlurAppDelegate.m
index ec9f59285..9580a4ad9 100644
--- a/clients/ios/Classes/NewsBlurAppDelegate.m
+++ b/clients/ios/Classes/NewsBlurAppDelegate.m
@@ -2124,6 +2124,10 @@
}
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:edgeURL] options:@{} completionHandler:nil];
+ } else if ([storyBrowser isEqualToString:@"brave"]){
+ NSString *encodedURL = [url.absoluteString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]];
+ NSString *braveURL = [NSString stringWithFormat:@"%@%@", @"brave://open-url?url=", encodedURL];
+ [[UIApplication sharedApplication] openURL:[NSURL URLWithString:braveURL] options:@{} completionHandler:nil];
} else if ([storyBrowser isEqualToString:@"inappsafari"]) {
[self showSafariViewControllerWithURL:url useReader:NO];
} else if ([storyBrowser isEqualToString:@"inappsafarireader"]) {
diff --git a/clients/ios/Classes/NewsBlurViewController.m b/clients/ios/Classes/NewsBlurViewController.m
index d1ee77900..aecb19b81 100644
--- a/clients/ios/Classes/NewsBlurViewController.m
+++ b/clients/ios/Classes/NewsBlurViewController.m
@@ -253,7 +253,10 @@ static NSArray *NewsBlurTopSectionNames;
// [self.feedTitlesTable selectRowAtIndexPath:self.currentRowAtIndexPath
// animated:NO
// scrollPosition:UITableViewScrollPositionNone];
- [self hideNotifier];
+
+ if (self.notifier.pendingHide) {
+ [self hideNotifier];
+ }
}
if (self.searchFeedIds) {
@@ -1240,6 +1243,7 @@ static NSArray *NewsBlurTopSectionNames;
NSString *folderName = [appDelegate.dictFoldersArray objectAtIndex:indexPath.section];
id feedId = [[appDelegate.dictFolders objectForKey:folderName] objectAtIndex:indexPath.row];
NSString *feedIdStr = [NSString stringWithFormat:@"%@",feedId];
+ BOOL isSavedSearch = [appDelegate isSavedSearch:feedIdStr];
NSString *searchQuery = [appDelegate searchQueryForFeedId:feedIdStr];
NSString *searchFolder = [appDelegate searchFolderForFeedId:feedIdStr];
feedIdStr = [appDelegate feedIdWithoutSearchQuery:feedIdStr];
@@ -1253,7 +1257,7 @@ static NSArray *NewsBlurTopSectionNames;
if (self.searchFeedIds && !isSaved) {
isOmitted = ![self.searchFeedIds containsObject:feedIdStr];
} else {
- isOmitted = [appDelegate isFolderCollapsed:folderName] || ![self isFeedVisible:feedIdStr];
+ isOmitted = [appDelegate isFolderCollapsed:folderName] || !([self isFeedVisible:feedIdStr] || isSavedSearch);
}
if (isOmitted) {
@@ -1797,7 +1801,9 @@ heightForHeaderInSection:(NSInteger)section {
NSDictionary *unreadCounts = self.appDelegate.dictUnreadCounts[feedId];
NSIndexPath *stillVisible = self.stillVisibleFeeds[feedId];
if (!stillVisible && self.appDelegate.isSavedStoriesIntelligenceMode) {
- return [self.appDelegate savedStoriesCountForFeed:feedId] > 0 || [self.appDelegate isSavedFeed:feedId];
+ return [self.appDelegate savedStoriesCountForFeed:feedId] > 0 || [self.appDelegate isSavedFeed:feedId] || [self.appDelegate isSavedSearch:feedId];
+ } else if (!stillVisible && [self.appDelegate isSavedSearch:feedId]) {
+ return YES;
} else if (!stillVisible &&
appDelegate.selectedIntelligence >= 1 &&
[[unreadCounts objectForKey:@"ps"] intValue] <= 0) {
diff --git a/clients/ios/Classes/StoryDetailViewController.m b/clients/ios/Classes/StoryDetailViewController.m
index 9204b1bbe..46d459428 100644
--- a/clients/ios/Classes/StoryDetailViewController.m
+++ b/clients/ios/Classes/StoryDetailViewController.m
@@ -33,7 +33,6 @@
@interface StoryDetailViewController ()
@property (nonatomic, strong) NSString *fullStoryHTML;
-@property (nonatomic) BOOL isBarHideSwiping;
@end
@@ -297,8 +296,6 @@
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
- [self.navigationController.barHideOnSwipeGestureRecognizer removeTarget:self action:@selector(barHideSwipe:)];
-
if (!appDelegate.showingSafariViewController &&
appDelegate.navigationController.visibleViewController != (UIViewController *)appDelegate.shareViewController &&
appDelegate.navigationController.visibleViewController != (UIViewController *)appDelegate.trainerViewController &&
@@ -320,8 +317,6 @@
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
- [self.navigationController.barHideOnSwipeGestureRecognizer addTarget:self action:@selector(barHideSwipe:)];
-
if (!self.isPhoneOrCompact) {
[appDelegate.feedDetailViewController.view endEditing:YES];
}
@@ -529,7 +524,7 @@
NSDictionary *feed = [appDelegate getFeed:feedIdStr];
NSString *storyClassSuffix = @"";
- if (feed[@"is_newsletter"]) {
+ if ([feed[@"is_newsletter"] isEqualToNumber:[NSNumber numberWithInt:1]]) {
storyClassSuffix = @" NB-newsletter";
}
@@ -628,23 +623,17 @@
}
- (void)drawFeedGradient {
- NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
- BOOL navigationBarHidden = self.navigationController.navigationBarHidden;
- BOOL shouldHideStatusBar = [preferences boolForKey:@"story_hide_status_bar"];
- UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
- BOOL shouldOffsetFeedGradient = !self.isBarHideSwiping && UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone && !UIInterfaceOrientationIsLandscape(orientation) && navigationBarHidden && !shouldHideStatusBar;
- CGFloat offset = 0;
-
- if (shouldOffsetFeedGradient) {
- offset = appDelegate.storyPageControl.statusBarBackgroundView.bounds.size.height;
- }
-
- CGFloat yOffset = offset - 1;
+ BOOL shouldHideStatusBar = appDelegate.storyPageControl.shouldHideStatusBar;
+ CGFloat yOffset = -1;
NSString *feedIdStr = [NSString stringWithFormat:@"%@",
[self.activeStory
objectForKey:@"story_feed_id"]];
NSDictionary *feed = [appDelegate getFeed:feedIdStr];
+ if (appDelegate.storyPageControl.currentlyTogglingNavigationBar && !appDelegate.storyPageControl.isNavigationBarHidden) {
+ yOffset -= 25;
+ }
+
if (self.feedTitleGradient) {
[self.feedTitleGradient removeFromSuperview];
self.feedTitleGradient = nil;
@@ -673,11 +662,11 @@
[self.webView insertSubview:feedTitleGradient aboveSubview:self.webView.scrollView];
if (@available(iOS 11.0, *)) {
- if (self.view.safeAreaInsets.top > 0.0 && UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone && shouldHideStatusBar) {
- feedTitleGradient.alpha = self.navigationController.navigationBarHidden ? 1 : 0;
+ if (appDelegate.storyPageControl.view.safeAreaInsets.top > 0.0 && UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone && shouldHideStatusBar) {
+ feedTitleGradient.alpha = appDelegate.storyPageControl.isNavigationBarHidden ? 1 : 0;
[UIView animateWithDuration:0.3 animations:^{
- feedTitleGradient.alpha = self.navigationController.navigationBarHidden ? 0 : 1;
+ feedTitleGradient.alpha = appDelegate.storyPageControl.isNavigationBarHidden ? 0 : 1;
}];
}
}
@@ -1339,7 +1328,7 @@
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqual:@"contentOffset"]) {
BOOL isHorizontal = appDelegate.storyPageControl.isHorizontal;
- BOOL isNavBarHidden = self.navigationController.navigationBarHidden;
+ BOOL isNavBarHidden = appDelegate.storyPageControl.isNavigationBarHidden;
if (self.webView.scrollView.contentOffset.y < (-1 * self.feedTitleGradient.frame.size.height + 1 + self.webView.scrollView.scrollIndicatorInsets.top)) {
// Pulling
@@ -2237,21 +2226,6 @@
return [super canPerformAction:action withSender:sender];
}
-- (void)barHideSwipe:(UISwipeGestureRecognizer *)recognizer {
- if (recognizer.state == UIGestureRecognizerStateEnded) {
- self.isBarHideSwiping = NO;
-
- NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
- BOOL shouldHideStatusBar = [preferences boolForKey:@"story_hide_status_bar"];
-
- if (!shouldHideStatusBar) {
- [self drawFeedGradient];
- }
- } else {
- self.isBarHideSwiping = YES;
- }
-}
-
# pragma mark -
# pragma mark Subscribing to blurblog
diff --git a/clients/ios/Classes/StoryPageControl.h b/clients/ios/Classes/StoryPageControl.h
index ad773092a..a2244d23e 100644
--- a/clients/ios/Classes/StoryPageControl.h
+++ b/clients/ios/Classes/StoryPageControl.h
@@ -40,6 +40,7 @@
@property (nonatomic) StoryDetailViewController *previousPage;
@property (nonatomic, strong) IBOutlet UIScrollView *scrollView;
@property (nonatomic, strong) IBOutlet UIPageControl *pageControl;
+@property (weak, nonatomic) IBOutlet NSLayoutConstraint *scrollViewTopConstraint;
@property (weak, nonatomic) IBOutlet UIView *autoscrollView;
@property (weak, nonatomic) IBOutlet UIImageView *autoscrollBackgroundImageView;
@@ -88,6 +89,8 @@
@property (nonatomic, strong) NBNotifier *notifier;
@property (nonatomic) NSInteger scrollingToPage;
@property (nonatomic, strong) id standardInteractivePopGestureDelegate;
+@property (nonatomic, readonly) BOOL shouldHideStatusBar;
+@property (nonatomic, readonly) BOOL isNavigationBarHidden;
@property (nonatomic, readonly) BOOL allowFullscreen;
@property (nonatomic) BOOL forceNavigationBarShown;
@property (nonatomic) BOOL currentlyTogglingNavigationBar;
diff --git a/clients/ios/Classes/StoryPageControl.m b/clients/ios/Classes/StoryPageControl.m
index 6f8976734..cbce6208c 100644
--- a/clients/ios/Classes/StoryPageControl.m
+++ b/clients/ios/Classes/StoryPageControl.m
@@ -28,6 +28,7 @@
@interface StoryPageControl ()
@property (nonatomic) CGFloat statusBarHeight;
+@property (nonatomic) BOOL wasNavigationBarHidden;
@property (nonatomic, strong) NSTimer *autoscrollTimer;
@property (nonatomic, strong) NSTimer *autoscrollViewTimer;
@property (nonatomic, strong) NSString *restoringStoryId;
@@ -284,6 +285,8 @@
BOOL swipeEnabled = [[userPreferences stringForKey:@"story_detail_swipe_left_edge"]
isEqualToString:@"pop_to_story_list"];;
self.navigationController.hidesBarsOnSwipe = self.allowFullscreen;
+ [self.navigationController.barHideOnSwipeGestureRecognizer addTarget:self action:@selector(barHideSwipe:)];
+
self.navigationController.interactivePopGestureRecognizer.enabled = swipeEnabled;
self.navigationController.interactivePopGestureRecognizer.delegate = self;
@@ -398,6 +401,8 @@
[super viewDidDisappear:animated];
self.navigationItem.leftBarButtonItem = nil;
+
+ [self.navigationController.barHideOnSwipeGestureRecognizer removeTarget:self action:@selector(barHideSwipe:)];
}
- (void)viewWillDisappear:(BOOL)animated {
@@ -468,27 +473,38 @@
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
+
+ if (self.isNavigationBarHidden && !self.shouldHideStatusBar) {
+ self.scrollViewTopConstraint.constant = self.statusBarHeight;
+ } else {
+ self.scrollViewTopConstraint.constant = 0;
+ }
+
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
[self layoutForInterfaceOrientation:orientation];
[self adjustDragBar:orientation];
}
-- (void)updateStatusBarState {
+- (BOOL)shouldHideStatusBar {
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
- BOOL shouldHideStatusBar = [preferences boolForKey:@"story_hide_status_bar"];
- BOOL isNavBarHidden = self.navigationController.navigationBarHidden;
- self.statusBarBackgroundView.hidden = shouldHideStatusBar || !isNavBarHidden || !appDelegate.isPortrait;
+ return [preferences boolForKey:@"story_hide_status_bar"];
+}
+
+- (BOOL)isNavigationBarHidden {
+ return self.navigationController.navigationBarHidden;
+}
+
+- (void)updateStatusBarState {
+ BOOL isNavBarHidden = self.isNavigationBarHidden;
+
+ self.statusBarBackgroundView.hidden = self.shouldHideStatusBar || !isNavBarHidden || !appDelegate.isPortrait;
}
- (BOOL)prefersStatusBarHidden {
- NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
- BOOL shouldHideStatusBar = [preferences boolForKey:@"story_hide_status_bar"];
- BOOL isNavBarHidden = self.navigationController.navigationBarHidden;
-
[self updateStatusBarState];
- return shouldHideStatusBar && isNavBarHidden;
+ return self.shouldHideStatusBar && self.isNavigationBarHidden;
}
- (BOOL)allowFullscreen {
@@ -511,6 +527,7 @@
}
self.currentlyTogglingNavigationBar = YES;
+ self.wasNavigationBarHidden = hide;
[self.navigationController setNavigationBarHidden:hide animated:YES];
@@ -640,6 +657,22 @@
return [[[NSUserDefaults standardUserDefaults] objectForKey:@"scroll_stories_horizontally"] boolValue];
}
+- (void)barHideSwipe:(UIPanGestureRecognizer *)recognizer {
+ BOOL isBarHidden = self.isNavigationBarHidden;
+
+ if (recognizer.state == UIGestureRecognizerStateEnded && isBarHidden != self.wasNavigationBarHidden) {
+ self.wasNavigationBarHidden = isBarHidden;
+
+ if (!appDelegate.storyPageControl.shouldHideStatusBar) {
+ [currentPage drawFeedGradient];
+ }
+
+ if (!self.isHorizontal) {
+ [self reorientPages];
+ }
+ }
+}
+
- (void)resetPages {
self.navigationItem.titleView = nil;
@@ -890,6 +923,11 @@
}
pageFrame.size.height = CGRectGetHeight(self.scrollView.bounds);
pageFrame.size.width = CGRectGetWidth(self.scrollView.bounds);
+
+ if (self.currentlyTogglingNavigationBar && !self.isNavigationBarHidden) {
+ pageFrame.size.height -= 20.0;
+ }
+
pageController.view.hidden = NO;
pageController.view.frame = pageFrame;
} else {
@@ -1697,8 +1735,8 @@
- (void)tappedStory {
if (self.autoscrollAvailable) {
[self showAutoscrollBriefly:YES];
- } else {
- [self setNavigationBarHidden: !self.navigationController.navigationBarHidden];
+ } else if (self.allowFullscreen) {
+ [self setNavigationBarHidden: !self.isNavigationBarHidden];
}
}
diff --git a/clients/ios/Classes/StoryPageControl.xib b/clients/ios/Classes/StoryPageControl.xib
index 94ec203f3..c1c0bed2a 100644
--- a/clients/ios/Classes/StoryPageControl.xib
+++ b/clients/ios/Classes/StoryPageControl.xib
@@ -1,9 +1,9 @@
-
+
-
+
@@ -28,6 +28,7 @@
+
diff --git a/clients/ios/NewsBlur.xcodeproj/project.pbxproj b/clients/ios/NewsBlur.xcodeproj/project.pbxproj
index b8ff04f57..b636fd4f7 100755
--- a/clients/ios/NewsBlur.xcodeproj/project.pbxproj
+++ b/clients/ios/NewsBlur.xcodeproj/project.pbxproj
@@ -2799,7 +2799,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1120;
- LastUpgradeCheck = 1150;
+ LastUpgradeCheck = 1210;
ORGANIZATIONNAME = NewsBlur;
TargetAttributes = {
1749390F1C251BFE003D98AA = {
@@ -3605,7 +3605,7 @@
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- CURRENT_PROJECT_VERSION = 112;
+ CURRENT_PROJECT_VERSION = 115;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = HR7P97SD72;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -3625,7 +3625,7 @@
INFOPLIST_FILE = "Share Extension/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
- MARKETING_VERSION = 10.0.2;
+ MARKETING_VERSION = 10.1.1;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.newsblur.NewsBlur.Share-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -3655,7 +3655,7 @@
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- CURRENT_PROJECT_VERSION = 112;
+ CURRENT_PROJECT_VERSION = 115;
DEVELOPMENT_TEAM = HR7P97SD72;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -3669,7 +3669,7 @@
INFOPLIST_FILE = "Share Extension/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
- MARKETING_VERSION = 10.0.2;
+ MARKETING_VERSION = 10.1.1;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.newsblur.NewsBlur.Share-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -3701,7 +3701,7 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 112;
+ CURRENT_PROJECT_VERSION = 115;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = HR7P97SD72;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -3716,7 +3716,7 @@
INFOPLIST_FILE = "Widget Extension/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
- MARKETING_VERSION = 10.0.2;
+ MARKETING_VERSION = 10.1.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.newsblur.NewsBlur.widget;
@@ -3750,7 +3750,7 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 112;
+ CURRENT_PROJECT_VERSION = 115;
DEVELOPMENT_TEAM = HR7P97SD72;
ENABLE_NS_ASSERTIONS = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -3759,7 +3759,7 @@
INFOPLIST_FILE = "Widget Extension/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
- MARKETING_VERSION = 10.0.2;
+ MARKETING_VERSION = 10.1.1;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.newsblur.NewsBlur.widget;
@@ -3782,7 +3782,7 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 112;
+ CURRENT_PROJECT_VERSION = 115;
DEVELOPMENT_TEAM = HR7P97SD72;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@@ -3802,7 +3802,7 @@
"\"$(SRCROOT)\"",
"\"$(SRCROOT)/Other Sources\"",
);
- MARKETING_VERSION = 10.0.2;
+ MARKETING_VERSION = 10.1.1;
OTHER_CPLUSPLUSFLAGS = "$(OTHER_CFLAGS)";
OTHER_LDFLAGS = (
"-lsqlite3.0",
@@ -3831,7 +3831,7 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = YES;
- CURRENT_PROJECT_VERSION = 112;
+ CURRENT_PROJECT_VERSION = 115;
DEVELOPMENT_TEAM = HR7P97SD72;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@@ -3850,7 +3850,7 @@
"\"$(SRCROOT)\"",
"\"$(SRCROOT)/Other Sources\"",
);
- MARKETING_VERSION = 10.0.2;
+ MARKETING_VERSION = 10.1.1;
OTHER_LDFLAGS = (
"-lsqlite3.0",
"-ObjC",
@@ -3976,7 +3976,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 112;
+ CURRENT_PROJECT_VERSION = 115;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = HR7P97SD72;
GCC_C_LANGUAGE_STANDARD = gnu99;
@@ -3991,7 +3991,7 @@
INFOPLIST_FILE = "Story Notification Service Extension/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 10.1;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
- MARKETING_VERSION = 10.0.2;
+ MARKETING_VERSION = 10.1.1;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.newsblur.NewsBlur.Story-Notification-Service-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -4016,7 +4016,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 112;
+ CURRENT_PROJECT_VERSION = 115;
DEVELOPMENT_TEAM = HR7P97SD72;
ENABLE_NS_ASSERTIONS = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
@@ -4025,7 +4025,7 @@
INFOPLIST_FILE = "Story Notification Service Extension/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 10.1;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
- MARKETING_VERSION = 10.0.2;
+ MARKETING_VERSION = 10.1.1;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.newsblur.NewsBlur.Story-Notification-Service-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
diff --git a/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/NewsBlur.xcscheme b/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/NewsBlur.xcscheme
index 826621cbc..7051e3dde 100644
--- a/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/NewsBlur.xcscheme
+++ b/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/NewsBlur.xcscheme
@@ -1,6 +1,6 @@
Opera Mini
Firefox
Edge
+ Brave
DefaultValue
inappsafari
@@ -750,6 +751,7 @@
opera_mini
firefox
edge
+ brave
Key
story_browser
diff --git a/clients/ios/Resources/Settings.bundle/Root~ipad.plist b/clients/ios/Resources/Settings.bundle/Root~ipad.plist
index abc3b8ebb..ec0ca8470 100644
--- a/clients/ios/Resources/Settings.bundle/Root~ipad.plist
+++ b/clients/ios/Resources/Settings.bundle/Root~ipad.plist
@@ -757,6 +757,7 @@
Opera Mini
Firefox
Edge
+ Brave
DefaultValue
inappsafari
@@ -770,6 +771,7 @@
opera_mini
firefox
edge
+ brave
Key
story_browser
diff --git a/clients/ios/static/sample_text.html b/clients/ios/static/sample_text.html
index 443c9fc17..09c186b08 100644
--- a/clients/ios/static/sample_text.html
+++ b/clients/ios/static/sample_text.html
@@ -6,7 +6,10 @@
-
+
+
-