mirror of
https://github.com/viq/NewsBlur.git
synced 2025-08-31 22:20:12 +00:00
Merge branch 'django3.0' into docker_django3.0
* django3.0: (184 commits) Removing log override Moving logging over to the newsblur log. Fixing search indexer background task for new celery. Attempting to add gunicorn errors to console/log. Better handling of missing subs. Handling missing user sub on feed delete. Correct encoding for strings on systems that don't have utf-8 as default encoding. Writing in the real urllib3 dependency for requests. Upgrading requests due to urllib3 incompatibility. Login required should use the next parameter. Upgrading django oauth toolkit for django 1.11. Handling newsletters with multiple recipients. Extracting image urls sometimes fails. Handling ajax errors in json views. Adding timeouts to most outbound requests. Sentry SDK 0.19.4. Removing imperfect proxy warning for every story. Found four more GET/POST crosses. Feed unread count may need a POST. Namespacing settings. ...
This commit is contained in:
commit
05756155b1
7261 changed files with 4918 additions and 940647 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -12,6 +12,7 @@ media/iphone/build
|
||||||
build/
|
build/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
**/*.perspectivev*
|
**/*.perspectivev*
|
||||||
|
.vscode/*
|
||||||
data/
|
data/
|
||||||
config/certificates
|
config/certificates
|
||||||
**/*.xcuserstate
|
**/*.xcuserstate
|
||||||
|
@ -71,3 +72,5 @@ clients/android/NewsBlur/build.gradle
|
||||||
clients/android/NewsBlur/gradle*
|
clients/android/NewsBlur/gradle*
|
||||||
clients/android/NewsBlur/settings.gradle
|
clients/android/NewsBlur/settings.gradle
|
||||||
/docker/volumes/*
|
/docker/volumes/*
|
||||||
|
|
||||||
|
**/node_modules
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
from celery import Task
|
from newsblur.celeryapp import app
|
||||||
from utils import log as logging
|
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()
|
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,20 @@
|
||||||
from celery import Task
|
from newsblur.celeryapp import app
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from apps.feed_import.models import UploadedOPML, OPMLImporter
|
from apps.feed_import.models import UploadedOPML, OPMLImporter
|
||||||
from apps.reader.models import UserSubscription
|
from apps.reader.models import UserSubscription
|
||||||
from utils import log as logging
|
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):
|
feed_count = UserSubscription.objects.filter(user=user).count()
|
||||||
user = User.objects.get(pk=user_id)
|
user.profile.send_upload_opml_finished_email(feed_count)
|
||||||
logging.user(user, "~FR~SBOPML upload (task) starting...")
|
logging.user(user, "~FR~SBOPML upload (task): ~SK%s~SN~SB~FR feeds" % (feed_count))
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
|
|
|
@ -31,9 +31,26 @@ class EmailNewsletter:
|
||||||
return
|
return
|
||||||
usf.add_folder('', 'Newsletters')
|
usf.add_folder('', 'Newsletters')
|
||||||
|
|
||||||
|
# First look for the email address
|
||||||
try:
|
try:
|
||||||
feed = Feed.objects.get(feed_address=feed_address)
|
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:
|
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 = Feed.objects.create(feed_address=feed_address,
|
||||||
feed_link='http://' + sender_domain,
|
feed_link='http://' + sender_domain,
|
||||||
feed_title=sender_name,
|
feed_title=sender_name,
|
||||||
|
@ -148,8 +165,8 @@ class EmailNewsletter:
|
||||||
|
|
||||||
return profile.user
|
return profile.user
|
||||||
|
|
||||||
def _feed_address(self, user, sender):
|
def _feed_address(self, user, sender_email):
|
||||||
return 'newsletter:%s:%s' % (user.pk, sender)
|
return 'newsletter:%s:%s' % (user.pk, sender_email)
|
||||||
|
|
||||||
def _split_sender(self, sender):
|
def _split_sender(self, sender):
|
||||||
tokens = re.search('(.*?) <(.*?)@(.*?)>', sender)
|
tokens = re.search('(.*?) <(.*?)@(.*?)>', sender)
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
from celery import Task
|
from newsblur.celeryapp import app
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from apps.notifications.models import MUserFeedNotification
|
from apps.notifications.models import MUserFeedNotification
|
||||||
from utils import log as logging
|
from utils import log as logging
|
||||||
|
|
||||||
|
|
||||||
class QueueNotifications(Task):
|
@app.task()
|
||||||
|
def QueueNotifications(feed_id, new_stories):
|
||||||
def run(self, feed_id, new_stories):
|
MUserFeedNotification.push_feed_notifications(feed_id, new_stories)
|
||||||
MUserFeedNotification.push_feed_notifications(feed_id, new_stories)
|
|
||||||
|
|
|
@ -9,8 +9,6 @@ from apps.profile.models import change_password, blank_authenticate, MGiftCode,
|
||||||
from apps.social.models import MSocialProfile
|
from apps.social.models import MSocialProfile
|
||||||
|
|
||||||
PLANS = [
|
PLANS = [
|
||||||
("newsblur-premium-12", mark_safe("$12 / year <span class='NB-small'>($1/month)</span>")),
|
|
||||||
("newsblur-premium-24", mark_safe("$24 / year <span class='NB-small'>($2/month)</span>")),
|
|
||||||
("newsblur-premium-36", mark_safe("$36 / year <span class='NB-small'>($3/month)</span>")),
|
("newsblur-premium-36", mark_safe("$36 / year <span class='NB-small'>($3/month)</span>")),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -35,7 +33,7 @@ class StripePlusPaymentForm(StripePaymentForm):
|
||||||
email = forms.EmailField(widget=forms.TextInput(attrs=dict(maxlength=75)),
|
email = forms.EmailField(widget=forms.TextInput(attrs=dict(maxlength=75)),
|
||||||
label='Email address',
|
label='Email address',
|
||||||
required=False)
|
required=False)
|
||||||
plan = forms.ChoiceField(required=False, widget=HorizRadioRenderer,
|
plan = forms.ChoiceField(required=False, widget=forms.RadioSelect,
|
||||||
choices=PLANS, label='Plan')
|
choices=PLANS, label='Plan')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -492,7 +492,7 @@ class Profile(models.Model):
|
||||||
|
|
||||||
return ipn[0].payer_email
|
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,
|
payments = PaymentHistory.objects.filter(user=self.user,
|
||||||
payment_identifier=transaction_identifier,
|
payment_identifier=transaction_identifier,
|
||||||
payment_date__gte=datetime.datetime.now()-datetime.timedelta(days=3))
|
payment_date__gte=datetime.datetime.now()-datetime.timedelta(days=3))
|
||||||
|
@ -512,7 +512,30 @@ class Profile(models.Model):
|
||||||
if not self.is_premium:
|
if not self.is_premium:
|
||||||
self.activate_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
|
return True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -1,90 +1,76 @@
|
||||||
import datetime
|
import datetime
|
||||||
from celery import Task
|
from newsblur.celeryapp import app
|
||||||
from apps.profile.models import Profile, RNewUserQueue
|
from apps.profile.models import Profile, RNewUserQueue
|
||||||
from utils import log as logging
|
from utils import log as logging
|
||||||
from apps.reader.models import UserSubscription, UserSubscriptionFolders
|
from apps.reader.models import UserSubscription, UserSubscriptionFolders
|
||||||
from apps.social.models import MSocialServices, MActivity, MInteraction
|
from apps.social.models import MSocialServices, MActivity, MInteraction
|
||||||
|
|
||||||
class EmailNewUser(Task):
|
@app.task(name="email-new-user")
|
||||||
|
def EmailNewUser(user_id):
|
||||||
def run(self, user_id):
|
user_profile = Profile.objects.get(user__pk=user_id)
|
||||||
user_profile = Profile.objects.get(user__pk=user_id)
|
user_profile.send_new_user_email()
|
||||||
user_profile.send_new_user_email()
|
|
||||||
|
|
||||||
class EmailNewPremium(Task):
|
@app.task(name="email-new-premium")
|
||||||
|
def EmailNewPremium(user_id):
|
||||||
def run(self, user_id):
|
user_profile = Profile.objects.get(user__pk=user_id)
|
||||||
user_profile = Profile.objects.get(user__pk=user_id)
|
user_profile.send_new_premium_email()
|
||||||
user_profile.send_new_premium_email()
|
|
||||||
|
|
||||||
class PremiumExpire(Task):
|
@app.task(name="premium-expire")
|
||||||
name = 'premium-expire'
|
def PremiumExpire(**kwargs):
|
||||||
|
# Get expired but grace period users
|
||||||
def run(self, **kwargs):
|
two_days_ago = datetime.datetime.now() - datetime.timedelta(days=2)
|
||||||
# Get expired but grace period users
|
thirty_days_ago = datetime.datetime.now() - datetime.timedelta(days=30)
|
||||||
two_days_ago = datetime.datetime.now() - datetime.timedelta(days=2)
|
expired_profiles = Profile.objects.filter(is_premium=True,
|
||||||
thirty_days_ago = datetime.datetime.now() - datetime.timedelta(days=30)
|
premium_expire__lte=two_days_ago,
|
||||||
expired_profiles = Profile.objects.filter(is_premium=True,
|
premium_expire__gt=thirty_days_ago)
|
||||||
premium_expire__lte=two_days_ago,
|
logging.debug(" ---> %s users have expired premiums, emailing grace..." % expired_profiles.count())
|
||||||
premium_expire__gt=thirty_days_ago)
|
for profile in expired_profiles:
|
||||||
logging.debug(" ---> %s users have expired premiums, emailing grace..." % expired_profiles.count())
|
if profile.grace_period_email_sent():
|
||||||
for profile in expired_profiles:
|
continue
|
||||||
if profile.grace_period_email_sent():
|
profile.setup_premium_history()
|
||||||
continue
|
if profile.premium_expire < two_days_ago:
|
||||||
profile.setup_premium_history()
|
profile.send_premium_expire_grace_period_email()
|
||||||
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)
|
|
||||||
|
|
||||||
try:
|
# Get fully expired users
|
||||||
ss = MSocialServices.objects.get(user_id=user_id)
|
expired_profiles = Profile.objects.filter(is_premium=True,
|
||||||
except MSocialServices.DoesNotExist:
|
premium_expire__lte=thirty_days_ago)
|
||||||
logging.debug(" ---> ~FRCleaning up user, can't find social_services for user_id: ~SB%s" % user_id)
|
logging.debug(" ---> %s users have expired premiums, deactivating and emailing..." % expired_profiles.count())
|
||||||
return
|
for profile in expired_profiles:
|
||||||
ss.sync_twitter_photo()
|
profile.setup_premium_history()
|
||||||
|
if profile.premium_expire < thirty_days_ago:
|
||||||
|
profile.send_premium_expire_email()
|
||||||
|
profile.deactivate_premium()
|
||||||
|
|
||||||
class CleanSpam(Task):
|
@app.task(name="activate-next-new-user")
|
||||||
name = 'clean-spam'
|
def ActivateNextNewUser():
|
||||||
|
RNewUserQueue.activate_next()
|
||||||
|
|
||||||
def run(self, **kwargs):
|
@app.task(name="cleanup-user")
|
||||||
logging.debug(" ---> Finding spammers...")
|
def CleanupUser(user_id):
|
||||||
Profile.clear_dead_spammers(confirm=True)
|
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):
|
@app.task(name="clean-spam")
|
||||||
name = 'reimport-stripe-history'
|
def CleanSpam():
|
||||||
|
logging.debug(" ---> Finding spammers...")
|
||||||
|
Profile.clear_dead_spammers(confirm=True)
|
||||||
|
|
||||||
def run(self, **kwargs):
|
@app.task(name="reimport-stripe-history")
|
||||||
logging.debug(" ---> Reimporting Stripe history...")
|
def ReimportStripeHistory():
|
||||||
Profile.reimport_stripe_history(limit=10, days=1)
|
logging.debug(" ---> Reimporting Stripe history...")
|
||||||
|
Profile.reimport_stripe_history(limit=10, days=1)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ urlpatterns = [
|
||||||
url(r'^never_expire_premium/?', views.never_expire_premium, name='profile-never-expire-premium'),
|
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'^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_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'^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'^delete_account/?', views.delete_account, name='profile-delete-account'),
|
||||||
url(r'^forgot_password_return/?', views.forgot_password_return, name='profile-forgot-password-return'),
|
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'^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'^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'^ios_subscription_status/?', views.ios_subscription_status, name='profile-ios-subscription-status'),
|
||||||
|
url(r'debug/?', views.trigger_error, name='trigger-error'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -98,7 +98,8 @@ def login(request):
|
||||||
|
|
||||||
return render(request, 'accounts/login.html', {
|
return render(request, 'accounts/login.html', {
|
||||||
'form': form,
|
'form': form,
|
||||||
'next': request.POST.get('next', "")})
|
'next': request.POST.get('next', "") or request.GET.get('next', "")
|
||||||
|
})
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def signup(request):
|
def signup(request):
|
||||||
|
@ -106,16 +107,17 @@ def signup(request):
|
||||||
recaptcha = request.POST.get('g-recaptcha-response', None)
|
recaptcha = request.POST.get('g-recaptcha-response', None)
|
||||||
recaptcha_error = None
|
recaptcha_error = None
|
||||||
|
|
||||||
if not recaptcha:
|
if settings.ENFORCE_SIGNUP_CAPTCHA:
|
||||||
recaptcha_error = "Please hit the \"I'm not a robot\" button."
|
if not recaptcha:
|
||||||
else:
|
recaptcha_error = "Please hit the \"I'm not a robot\" button."
|
||||||
response = requests.post('https://www.google.com/recaptcha/api/siteverify', {
|
else:
|
||||||
'secret': settings.RECAPTCHA_SECRET_KEY,
|
response = requests.post('https://www.google.com/recaptcha/api/siteverify', {
|
||||||
'response': recaptcha,
|
'secret': settings.RECAPTCHA_SECRET_KEY,
|
||||||
})
|
'response': recaptcha,
|
||||||
result = response.json()
|
})
|
||||||
if not result['success']:
|
result = response.json()
|
||||||
recaptcha_error = "Really, please hit the \"I'm not a robot\" button."
|
if not result['success']:
|
||||||
|
recaptcha_error = "Really, please hit the \"I'm not a robot\" button."
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = SignupForm(data=request.POST, prefix="signup")
|
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))
|
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:
|
if paid:
|
||||||
logging.user(request, "~BM~FBSending iOS Receipt email: %s %s" % (product_identifier, transaction_identifier))
|
logging.user(request, "~BM~FBSending iOS Receipt email: %s %s" % (product_identifier, transaction_identifier))
|
||||||
subject = "iOS Premium: %s (%s)" % (request.user.profile, product_identifier)
|
subject = "iOS Premium: %s (%s)" % (request.user.profile, product_identifier)
|
||||||
|
@ -343,13 +345,32 @@ def save_ios_receipt(request):
|
||||||
|
|
||||||
return request.user.profile
|
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
|
@login_required
|
||||||
def stripe_form(request):
|
def stripe_form(request):
|
||||||
user = request.user
|
user = request.user
|
||||||
success_updating = False
|
success_updating = False
|
||||||
stripe.api_key = settings.STRIPE_SECRET
|
stripe.api_key = settings.STRIPE_SECRET
|
||||||
plan = int(request.GET.get('plan', 2))
|
plan = PLANS[0][0]
|
||||||
plan = PLANS[plan-1][0]
|
|
||||||
renew = is_true(request.GET.get('renew', False))
|
renew = is_true(request.GET.get('renew', False))
|
||||||
error = None
|
error = None
|
||||||
|
|
||||||
|
@ -692,4 +713,9 @@ def ios_subscription_status(request):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"code": 1
|
"code": 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def trigger_error(request):
|
||||||
|
logging.user(request.user, "~BR~FW~SBTriggering divison by zero")
|
||||||
|
division_by_zero = 1 / 0
|
||||||
|
return HttpResponseRedirect(reverse('index'))
|
||||||
|
|
|
@ -52,11 +52,11 @@ def push_callback(request, push_id):
|
||||||
# XXX TODO: Optimize this by removing feedparser. It just needs to find out
|
# XXX TODO: Optimize this by removing feedparser. It just needs to find out
|
||||||
# the hub_url or topic has changed. ElementTree could do it.
|
# the hub_url or topic has changed. ElementTree could do it.
|
||||||
if random.random() < 0.1:
|
if random.random() < 0.1:
|
||||||
parsed = feedparser.parse(request.raw_post_data)
|
parsed = feedparser.parse(request.body)
|
||||||
subscription.check_urls_against_pushed_data(parsed)
|
subscription.check_urls_against_pushed_data(parsed)
|
||||||
|
|
||||||
# Don't give fat ping, just fetch.
|
# 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:
|
if subscription.feed.active_premium_subscribers >= 1:
|
||||||
subscription.feed.queue_pushed_feed_xml("Fetch me", latest_push_date_delta=latest_push_date_delta)
|
subscription.feed.queue_pushed_feed_xml("Fetch me", latest_push_date_delta=latest_push_date_delta)
|
||||||
MFetchHistory.add(feed_id=subscription.feed_id,
|
MFetchHistory.add(feed_id=subscription.feed_id,
|
||||||
|
|
|
@ -154,7 +154,8 @@ class SignupForm(forms.Form):
|
||||||
|
|
||||||
new_user = User(username=username)
|
new_user = User(username=username)
|
||||||
new_user.set_password(password)
|
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.email = email
|
||||||
new_user.last_login = datetime.datetime.now()
|
new_user.last_login = datetime.datetime.now()
|
||||||
new_user.save()
|
new_user.save()
|
||||||
|
@ -184,4 +185,4 @@ class FeatureForm(forms.Form):
|
||||||
feature = Feature(description=self.cleaned_data['description'],
|
feature = Feature(description=self.cleaned_data['description'],
|
||||||
date=datetime.datetime.utcnow() + datetime.timedelta(minutes=1))
|
date=datetime.datetime.utcnow() + datetime.timedelta(minutes=1))
|
||||||
feature.save()
|
feature.save()
|
||||||
return feature
|
return feature
|
||||||
|
|
|
@ -771,6 +771,9 @@ class UserSubscription(models.Model):
|
||||||
except pymongo.errors.OperationFailure as e:
|
except pymongo.errors.OperationFailure as e:
|
||||||
stories_db = MStory.objects(story_hash__in=unread_story_hashes)[:100]
|
stories_db = MStory.objects(story_hash__in=unread_story_hashes)[:100]
|
||||||
stories = Feed.format_stories(stories_db, self.feed_id)
|
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 = []
|
unread_stories = []
|
||||||
for story in stories:
|
for story in stories:
|
||||||
|
@ -1192,7 +1195,7 @@ class RUserStory:
|
||||||
redis_commands(read_story_key)
|
redis_commands(read_story_key)
|
||||||
|
|
||||||
read_stories_list_key = 'lRS:%s' % user_id
|
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:
|
if ps and username:
|
||||||
ps.publish(username, 'story:unread:%s' % story_hash)
|
ps.publish(username, 'story:unread:%s' % story_hash)
|
||||||
|
@ -1428,15 +1431,16 @@ class UserSubscriptionFolders(models.Model):
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
if not multiples_found and deleted and commit_delete:
|
if not multiples_found and deleted and commit_delete:
|
||||||
|
user_sub = None
|
||||||
try:
|
try:
|
||||||
user_sub = UserSubscription.objects.get(user=self.user, feed=feed_id)
|
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)
|
duplicate_feed = DuplicateFeed.objects.filter(duplicate_feed_id=feed_id)
|
||||||
if duplicate_feed:
|
if duplicate_feed:
|
||||||
try:
|
try:
|
||||||
user_sub = UserSubscription.objects.get(user=self.user,
|
user_sub = UserSubscription.objects.get(user=self.user,
|
||||||
feed=duplicate_feed[0].feed)
|
feed=duplicate_feed[0].feed)
|
||||||
except Feed.DoesNotExist:
|
except (Feed.DoesNotExist, UserSubscription.DoesNotExist):
|
||||||
return
|
return
|
||||||
if user_sub:
|
if user_sub:
|
||||||
user_sub.delete()
|
user_sub.delete()
|
||||||
|
|
|
@ -1,46 +1,40 @@
|
||||||
import datetime
|
import datetime
|
||||||
from celery import Task
|
from newsblur.celeryapp import app
|
||||||
from utils import log as logging
|
from utils import log as logging
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from apps.reader.models import UserSubscription
|
from apps.reader.models import UserSubscription
|
||||||
from apps.social.models import MSocialSubscription
|
from apps.social.models import MSocialSubscription
|
||||||
|
|
||||||
|
@app.task(name='freshen-homepage')
|
||||||
class FreshenHomepage(Task):
|
def FreshenHomepage():
|
||||||
name = 'freshen-homepage'
|
day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1)
|
||||||
|
user = User.objects.get(username=settings.HOMEPAGE_USERNAME)
|
||||||
def run(self, **kwargs):
|
user.profile.last_seen_on = datetime.datetime.utcnow()
|
||||||
day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1)
|
user.profile.save()
|
||||||
user = User.objects.get(username=settings.HOMEPAGE_USERNAME)
|
|
||||||
user.profile.last_seen_on = datetime.datetime.utcnow()
|
usersubs = UserSubscription.objects.filter(user=user)
|
||||||
user.profile.save()
|
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)
|
socialsubs = MSocialSubscription.objects.filter(user_id=user.pk)
|
||||||
logging.debug(" ---> %s has %s feeds, freshening..." % (user.username, usersubs.count()))
|
logging.debug(" ---> %s has %s socialsubs, freshening..." % (user.username, socialsubs.count()))
|
||||||
for sub in usersubs:
|
for sub in socialsubs:
|
||||||
sub.mark_read_date = day_ago
|
sub.mark_read_date = day_ago
|
||||||
sub.needs_unread_recalc = True
|
sub.needs_unread_recalc = True
|
||||||
sub.save()
|
sub.save()
|
||||||
sub.calculate_feed_scores(silent=True)
|
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):
|
@app.task(name='clean_analytics', time_limit=720*10)
|
||||||
name = 'clean-analytics'
|
def CleanAnalytics():
|
||||||
hard = 720*10
|
logging.debug(" ---> Cleaning analytics... %s feed fetches" % (
|
||||||
|
settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.count(),
|
||||||
def run(self, **kwargs):
|
))
|
||||||
logging.debug(" ---> Cleaning analytics... %s feed fetches" % (
|
day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1)
|
||||||
settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.count(),
|
settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.delete_many({
|
||||||
))
|
"date": {"$lt": day_ago},
|
||||||
day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1)
|
})
|
||||||
settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.delete_many({
|
|
||||||
"date": {"$lt": day_ago},
|
|
||||||
})
|
|
||||||
|
|
|
@ -559,9 +559,11 @@ def interactions_count(request):
|
||||||
@ajax_login_required
|
@ajax_login_required
|
||||||
@json.json_view
|
@json.json_view
|
||||||
def feed_unread_count(request):
|
def feed_unread_count(request):
|
||||||
|
get_post = getattr(request, request.method)
|
||||||
start = time.time()
|
start = time.time()
|
||||||
user = request.user
|
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)
|
force = request.GET.get('force', False)
|
||||||
social_feed_ids = [feed_id for feed_id in feed_ids if 'social:' in feed_id]
|
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))
|
feed_ids = list(set(feed_ids) - set(social_feed_ids))
|
||||||
|
@ -1024,10 +1026,15 @@ def starred_story_hashes(request):
|
||||||
|
|
||||||
mstories = MStarredStory.objects(
|
mstories = MStarredStory.objects(
|
||||||
user_id=user.pk
|
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:
|
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:
|
else:
|
||||||
story_hashes = [s.story_hash for s in mstories]
|
story_hashes = [s.story_hash for s in mstories]
|
||||||
|
|
||||||
|
@ -1315,28 +1322,32 @@ def load_read_stories(request):
|
||||||
|
|
||||||
@json.json_view
|
@json.json_view
|
||||||
def load_river_stories__redis(request):
|
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()
|
start = time.time()
|
||||||
user = get_user(request)
|
user = get_user(request)
|
||||||
message = None
|
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]
|
feed_ids = [int(feed_id) for feed_id in feed_ids if feed_id]
|
||||||
if not feed_ids:
|
if not feed_ids:
|
||||||
feed_ids = request.GET.getlist('f') or request.GET.getlist('f[]')
|
feed_ids = get_post.getlist('f') or get_post.getlist('f[]')
|
||||||
feed_ids = [int(feed_id) for feed_id in request.GET.getlist('f') if feed_id]
|
feed_ids = [int(feed_id) for feed_id in get_post.getlist('f') if feed_id]
|
||||||
story_hashes = request.GET.getlist('h') or request.GET.getlist('h[]')
|
story_hashes = get_post.getlist('h') or get_post.getlist('h[]')
|
||||||
story_hashes = story_hashes[:100]
|
story_hashes = story_hashes[:100]
|
||||||
original_feed_ids = list(feed_ids)
|
original_feed_ids = list(feed_ids)
|
||||||
page = int(request.GET.get('page', 1))
|
page = int(get_post.get('page', 1))
|
||||||
order = request.GET.get('order', 'newest')
|
order = get_post.get('order', 'newest')
|
||||||
read_filter = request.GET.get('read_filter', 'unread')
|
read_filter = get_post.get('read_filter', 'unread')
|
||||||
query = request.GET.get('query', '').strip()
|
query = get_post.get('query', '').strip()
|
||||||
include_hidden = is_true(request.GET.get('include_hidden', False))
|
include_hidden = is_true(get_post.get('include_hidden', False))
|
||||||
include_feeds = is_true(request.GET.get('include_feeds', False))
|
include_feeds = is_true(get_post.get('include_feeds', False))
|
||||||
initial_dashboard = is_true(request.GET.get('initial_dashboard', False))
|
initial_dashboard = is_true(get_post.get('initial_dashboard', False))
|
||||||
infrequent = is_true(request.GET.get('infrequent', False))
|
infrequent = is_true(get_post.get('infrequent', False))
|
||||||
if infrequent:
|
if infrequent:
|
||||||
infrequent = request.GET.get('infrequent')
|
infrequent = get_post.get('infrequent')
|
||||||
now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone)
|
now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone)
|
||||||
usersubs = []
|
usersubs = []
|
||||||
code = 1
|
code = 1
|
||||||
|
@ -1567,9 +1578,9 @@ def complete_river(request):
|
||||||
@json.json_view
|
@json.json_view
|
||||||
def unread_story_hashes__old(request):
|
def unread_story_hashes__old(request):
|
||||||
user = get_user(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]
|
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 = {}
|
usersubs = {}
|
||||||
|
|
||||||
if not feed_ids:
|
if not feed_ids:
|
||||||
|
@ -2661,8 +2672,8 @@ def send_story_email(request):
|
||||||
|
|
||||||
share_user_profile.save_sent_email()
|
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' %
|
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',
|
(len(to_addresses), '' if len(to_addresses) == 1 else 's', to_addresses,
|
||||||
story['story_title'][:50], feed and feed.feed_title[:50]))
|
story['story_title'][:50], feed and feed.feed_title[:50]))
|
||||||
|
|
||||||
return {'code': code, 'message': message}
|
return {'code': code, 'message': message}
|
||||||
|
|
|
@ -110,4 +110,4 @@ def decline_feed(request):
|
||||||
recommended_feed.declined_date = datetime.datetime.now()
|
recommended_feed.declined_date = datetime.datetime.now()
|
||||||
recommended_feed.save()
|
recommended_feed.save()
|
||||||
|
|
||||||
return load_recommended_feed(request)
|
return load_recommended_feed(request)
|
||||||
|
|
|
@ -215,7 +215,7 @@ class IconImporter(object):
|
||||||
url = self._url_from_html(content)
|
url = self._url_from_html(content)
|
||||||
if not url:
|
if not url:
|
||||||
try:
|
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)
|
url = self._url_from_html(content)
|
||||||
except (AttributeError, SocketError, requests.ConnectionError,
|
except (AttributeError, SocketError, requests.ConnectionError,
|
||||||
requests.models.MissingSchema, requests.sessions.InvalidSchema,
|
requests.models.MissingSchema, requests.sessions.InvalidSchema,
|
||||||
|
@ -224,6 +224,7 @@ class IconImporter(object):
|
||||||
requests.models.ChunkedEncodingError,
|
requests.models.ChunkedEncodingError,
|
||||||
requests.models.ContentDecodingError,
|
requests.models.ContentDecodingError,
|
||||||
http.client.IncompleteRead,
|
http.client.IncompleteRead,
|
||||||
|
requests.adapters.ReadTimeout,
|
||||||
LocationParseError, OpenSSLError, PyAsn1Error,
|
LocationParseError, OpenSSLError, PyAsn1Error,
|
||||||
ValueError) as e:
|
ValueError) as e:
|
||||||
logging.debug(" ---> ~SN~FRFailed~FY to fetch ~FGfeed icon~FY: %s" % e)
|
logging.debug(" ---> ~SN~FRFailed~FY to fetch ~FGfeed icon~FY: %s" % e)
|
||||||
|
@ -276,14 +277,12 @@ class IconImporter(object):
|
||||||
@timelimit(30)
|
@timelimit(30)
|
||||||
def _1(url):
|
def _1(url):
|
||||||
headers = {
|
headers = {
|
||||||
'User-Agent': 'NewsBlur Favicon Fetcher - %s subscriber%s - %s '
|
'User-Agent': 'NewsBlur Favicon Fetcher - %s subscriber%s - %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)' %
|
|
||||||
(
|
(
|
||||||
self.feed.num_subscribers,
|
self.feed.num_subscribers,
|
||||||
's' if self.feed.num_subscribers != 1 else '',
|
's' if self.feed.num_subscribers != 1 else '',
|
||||||
self.feed.permalink
|
self.feed.permalink,
|
||||||
|
self.feed.fake_user_agent,
|
||||||
),
|
),
|
||||||
'Connection': 'close',
|
'Connection': 'close',
|
||||||
'Accept': 'image/png,image/x-icon,image/*;q=0.9,*/*;q=0.8'
|
'Accept': 'image/png,image/x-icon,image/*;q=0.9,*/*;q=0.8'
|
||||||
|
|
|
@ -409,6 +409,10 @@ class Feed(models.Model):
|
||||||
def favicon_fetching(self):
|
def favicon_fetching(self):
|
||||||
return bool(not (self.favicon_not_found or self.favicon_color))
|
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
|
@classmethod
|
||||||
def get_feed_from_url(cls, url, create=True, aggressive=False, fetch=True, offset=0, user=None, interactive=False):
|
def get_feed_from_url(cls, url, create=True, aggressive=False, fetch=True, offset=0, user=None, interactive=False):
|
||||||
feed = None
|
feed = None
|
||||||
|
@ -416,7 +420,10 @@ class Feed(models.Model):
|
||||||
original_url = url
|
original_url = url
|
||||||
|
|
||||||
if url and url.startswith('newsletter:'):
|
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):
|
if url and re.match('(https?://)?twitter.com/\w+/?', url):
|
||||||
without_rss = True
|
without_rss = True
|
||||||
if url and re.match(r'(https?://)?(www\.)?facebook.com/\w+/?$', url):
|
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.
|
# A known workaround is using facebook's user agent.
|
||||||
return 'facebookexternalhit/1.0 (+http://www.facebook.com/externalhit_uatext.php)'
|
return 'facebookexternalhit/1.0 (+http://www.facebook.com/externalhit_uatext.php)'
|
||||||
|
|
||||||
ua = ('NewsBlur Feed Fetcher - %s subscriber%s - %s '
|
ua = ('NewsBlur Feed Fetcher - %s subscriber%s - %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)' % (
|
|
||||||
self.num_subscribers,
|
self.num_subscribers,
|
||||||
's' if self.num_subscribers != 1 else '',
|
's' if self.num_subscribers != 1 else '',
|
||||||
self.permalink,
|
self.permalink,
|
||||||
|
self.fake_user_agent,
|
||||||
))
|
))
|
||||||
|
|
||||||
return ua
|
return ua
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fake_user_agent(self):
|
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
|
return ua
|
||||||
|
|
||||||
|
@ -2796,7 +2803,17 @@ class MStory(mongo.Document):
|
||||||
|
|
||||||
if len(image_urls):
|
if len(image_urls):
|
||||||
self.image_urls = [u for u in image_urls if u]
|
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
|
return self.image_urls
|
||||||
|
|
||||||
def fetch_original_text(self, force=False, request=None, debug=False):
|
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."""
|
mongoengine's inheritance model on every single row."""
|
||||||
user_id = mongo.IntField(unique_with=('story_guid',))
|
user_id = mongo.IntField(unique_with=('story_guid',))
|
||||||
starred_date = mongo.DateTimeField()
|
starred_date = mongo.DateTimeField()
|
||||||
|
starred_updated = mongo.DateTimeField()
|
||||||
story_feed_id = mongo.IntField()
|
story_feed_id = mongo.IntField()
|
||||||
story_date = mongo.DateTimeField()
|
story_date = mongo.DateTimeField()
|
||||||
story_title = mongo.StringField(max_length=1024)
|
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_z = zlib.compress(self.story_original_content)
|
||||||
self.story_original_content = None
|
self.story_original_content = None
|
||||||
self.story_hash = self.feed_guid_hash
|
self.story_hash = self.feed_guid_hash
|
||||||
|
self.starred_updated = datetime.datetime.now()
|
||||||
|
|
||||||
return super(MStarredStory, self).save(*args, **kwargs)
|
return super(MStarredStory, self).save(*args, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -3010,6 +3029,11 @@ class MStarredStoryCounts(mongo.Document):
|
||||||
secret_token = user.profile.secret_token
|
secret_token = user.profile.secret_token
|
||||||
|
|
||||||
slug = self.slug if self.slug else ""
|
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,
|
return "%s/reader/starred_rss/%s/%s/%s" % (settings.NEWSBLUR_URL, self.user_id,
|
||||||
secret_token, slug)
|
secret_token, slug)
|
||||||
|
|
||||||
|
|
|
@ -51,13 +51,11 @@ class PageImporter(object):
|
||||||
@property
|
@property
|
||||||
def headers(self):
|
def headers(self):
|
||||||
return {
|
return {
|
||||||
'User-Agent': 'NewsBlur Page Fetcher - %s subscriber%s - %s '
|
'User-Agent': 'NewsBlur Page Fetcher - %s subscriber%s - %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)' % (
|
|
||||||
self.feed.num_subscribers,
|
self.feed.num_subscribers,
|
||||||
's' if self.feed.num_subscribers != 1 else '',
|
's' if self.feed.num_subscribers != 1 else '',
|
||||||
self.feed.permalink,
|
self.feed.permalink,
|
||||||
|
self.feed.fake_user_agent,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,11 +90,12 @@ class PageImporter(object):
|
||||||
data = response.read()
|
data = response.read()
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
response = requests.get(feed_link, headers=self.headers)
|
response = requests.get(feed_link, headers=self.headers, timeout=10)
|
||||||
response.connection.close()
|
response.connection.close()
|
||||||
except requests.exceptions.TooManyRedirects:
|
except requests.exceptions.TooManyRedirects:
|
||||||
response = requests.get(feed_link)
|
response = requests.get(feed_link, timeout=10)
|
||||||
except (AttributeError, SocketError, OpenSSLError, PyAsn1Error, TypeError) as e:
|
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))
|
logging.debug(' ***> [%-30s] Page fetch failed using requests: %s' % (self.feed.log_title[:30], e))
|
||||||
self.save_no_page()
|
self.save_no_page()
|
||||||
return
|
return
|
||||||
|
@ -186,12 +185,18 @@ class PageImporter(object):
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(story_permalink, headers=self.headers)
|
response = requests.get(story_permalink, headers=self.headers, timeout=10)
|
||||||
response.connection.close()
|
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:
|
try:
|
||||||
response = requests.get(story_permalink)
|
response = requests.get(story_permalink, timeout=10)
|
||||||
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:
|
||||||
logging.debug(' ***> [%-30s] Original story fetch failed using requests: %s' % (self.feed.log_title[:30], e))
|
logging.debug(' ***> [%-30s] Original story fetch failed using requests: %s' % (self.feed.log_title[:30], e))
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
@ -293,7 +298,8 @@ class PageImporter(object):
|
||||||
feed_page.page_data = zlib.compress(html)
|
feed_page.page_data = zlib.compress(html)
|
||||||
feed_page.save()
|
feed_page.save()
|
||||||
except MFeedPage.DoesNotExist:
|
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
|
return feed_page
|
||||||
|
|
||||||
def save_page_node(self, html):
|
def save_page_node(self, html):
|
||||||
|
|
|
@ -3,7 +3,7 @@ import os
|
||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
import redis
|
import redis
|
||||||
from celery import Task
|
from newsblur.celeryapp import app
|
||||||
from celery.exceptions import SoftTimeLimitExceeded
|
from celery.exceptions import SoftTimeLimitExceeded
|
||||||
from utils import log as logging
|
from utils import log as logging
|
||||||
from utils import s3_utils as s3
|
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
|
from utils.redis_raw_log_middleware import RedisDumpMiddleware
|
||||||
FEED_TASKING_MAX = 10000
|
FEED_TASKING_MAX = 10000
|
||||||
|
|
||||||
class TaskFeeds(Task):
|
@app.task(name='task-feeds')
|
||||||
name = 'task-feeds'
|
def TaskFeeds():
|
||||||
|
from apps.rss_feeds.models import Feed
|
||||||
def run(self, **kwargs):
|
settings.LOG_TO_STREAM = True
|
||||||
from apps.rss_feeds.models import Feed
|
now = datetime.datetime.utcnow()
|
||||||
settings.LOG_TO_STREAM = True
|
start = time.time()
|
||||||
now = datetime.datetime.utcnow()
|
r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
|
||||||
start = time.time()
|
tasked_feeds_size = r.zcard('tasked_feeds')
|
||||||
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
|
|
||||||
|
|
||||||
def run(self, **kwargs):
|
hour_ago = now - datetime.timedelta(hours=1)
|
||||||
from apps.rss_feeds.models import Feed
|
r.zremrangebyscore('fetched_feeds_last_hour', 0, int(hour_ago.strftime('%s')))
|
||||||
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
|
|
||||||
|
|
||||||
def run(self, **kwargs):
|
now_timestamp = int(now.strftime("%s"))
|
||||||
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"
|
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')
|
@app.task(name='task-broken-feeds')
|
||||||
collections = COLLECTIONS.split(' ')
|
def TaskBrokenFeeds():
|
||||||
db_name = 'newsblur'
|
from apps.rss_feeds.models import Feed
|
||||||
dir_name = 'backup_mongo_%s' % date
|
settings.LOG_TO_STREAM = True
|
||||||
filename = '%s.tgz' % dir_name
|
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:
|
mongodb_replication_lag = int(MStatistics.get('mongodb_replication_lag', 0))
|
||||||
cmd = 'mongodump --db %s --collection %s -o %s' % (db_name, collection, dir_name)
|
compute_scores = bool(mongodb_replication_lag < 10)
|
||||||
logging.debug(' ---> ~FMDumping ~SB%s~SN: %s' % (collection, cmd))
|
|
||||||
os.system(cmd)
|
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)
|
os.system(cmd)
|
||||||
|
|
||||||
logging.debug(' ---> ~FRUploading ~SB~FM%s~SN~FR to S3...' % filename)
|
cmd = 'tar -jcf %s %s' % (filename, dir_name)
|
||||||
s3.save_file_in_s3(filename)
|
os.system(cmd)
|
||||||
shutil.rmtree(dir_name)
|
|
||||||
os.remove(filename)
|
logging.debug(' ---> ~FRUploading ~SB~FM%s~SN~FR to S3...' % filename)
|
||||||
logging.debug(' ---> ~FRFinished uploading ~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):
|
if not isinstance(feed_ids, list):
|
||||||
from apps.rss_feeds.models import Feed
|
feed_ids = [feed_ids]
|
||||||
|
|
||||||
if not isinstance(feed_ids, list):
|
Feed.schedule_feed_fetches_immediately(feed_ids, user_id=user_id)
|
||||||
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):
|
if not isinstance(feed_ids, list):
|
||||||
from apps.rss_feeds.models import Feed
|
feed_ids = [feed_ids]
|
||||||
|
|
||||||
if not isinstance(feed_ids, list):
|
|
||||||
feed_ids = [feed_ids]
|
|
||||||
|
|
||||||
Feed.setup_feeds_for_premium_subscribers(feed_ids)
|
|
||||||
|
|
||||||
class ScheduleCountTagsForUser(Task):
|
|
||||||
|
|
||||||
def run(self, user_id):
|
Feed.setup_feeds_for_premium_subscribers(feed_ids)
|
||||||
from apps.rss_feeds.models import MStarredStoryCounts
|
|
||||||
|
@app.task()
|
||||||
MStarredStoryCounts.count_for_user(user_id)
|
def ScheduleCountTagsForUser(user_id):
|
||||||
|
from apps.rss_feeds.models import MStarredStoryCounts
|
||||||
|
|
||||||
|
MStarredStoryCounts.count_for_user(user_id)
|
||||||
|
|
|
@ -37,13 +37,11 @@ class TextImporter:
|
||||||
def headers(self):
|
def headers(self):
|
||||||
num_subscribers = getattr(self.feed, 'num_subscribers', 0)
|
num_subscribers = getattr(self.feed, 'num_subscribers', 0)
|
||||||
return {
|
return {
|
||||||
'User-Agent': 'NewsBlur Content Fetcher - %s subscriber%s - %s '
|
'User-Agent': 'NewsBlur Content Fetcher - %s subscriber%s - %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)' % (
|
|
||||||
num_subscribers,
|
num_subscribers,
|
||||||
's' if num_subscribers != 1 else '',
|
'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
|
url = "https://www.newsblur.com/rss_feeds/original_text_fetcher?url=%s" % url
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.get(url, headers=headers)
|
r = requests.get(url, headers=headers, verify=False, timeout=15)
|
||||||
r.connection.close()
|
r.connection.close()
|
||||||
except (AttributeError, SocketError, requests.ConnectionError,
|
except (AttributeError, SocketError, requests.ConnectionError,
|
||||||
requests.models.MissingSchema, requests.sessions.InvalidSchema,
|
requests.models.MissingSchema, requests.sessions.InvalidSchema,
|
||||||
|
@ -211,6 +209,7 @@ class TextImporter:
|
||||||
requests.models.InvalidURL,
|
requests.models.InvalidURL,
|
||||||
requests.models.ChunkedEncodingError,
|
requests.models.ChunkedEncodingError,
|
||||||
requests.models.ContentDecodingError,
|
requests.models.ContentDecodingError,
|
||||||
|
requests.adapters.ReadTimeout,
|
||||||
urllib3.exceptions.LocationValueError,
|
urllib3.exceptions.LocationValueError,
|
||||||
LocationParseError, OpenSSLError, PyAsn1Error) as e:
|
LocationParseError, OpenSSLError, PyAsn1Error) as e:
|
||||||
logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: %s" % e)
|
logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: %s" % e)
|
||||||
|
|
|
@ -185,7 +185,7 @@ def load_feed_statistics_embedded(request, feed_id):
|
||||||
)
|
)
|
||||||
|
|
||||||
def assemble_statistics(user, feed_id):
|
def assemble_statistics(user, feed_id):
|
||||||
timezone = user.profile.timezone
|
user_timezone = user.profile.timezone
|
||||||
stats = dict()
|
stats = dict()
|
||||||
feed = get_object_or_404(Feed, pk=feed_id)
|
feed = get_object_or_404(Feed, pk=feed_id)
|
||||||
feed.update_all_statistics()
|
feed.update_all_statistics()
|
||||||
|
@ -201,7 +201,7 @@ def assemble_statistics(user, feed_id):
|
||||||
if feed.is_push:
|
if feed.is_push:
|
||||||
try:
|
try:
|
||||||
stats['push_expires'] = localtime_for_timezone(feed.push.lease_expires,
|
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:
|
except PushSubscription.DoesNotExist:
|
||||||
stats['push_expires'] = 'Missing push'
|
stats['push_expires'] = 'Missing push'
|
||||||
feed.is_push = False
|
feed.is_push = False
|
||||||
|
@ -233,7 +233,7 @@ def assemble_statistics(user, feed_id):
|
||||||
stats['story_count_history'] = story_count_history
|
stats['story_count_history'] = story_count_history
|
||||||
|
|
||||||
# Rotate hours to match user's timezone offset
|
# 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)
|
hours_offset = int(localoffset.total_seconds() / 3600)
|
||||||
rotated_hours = {}
|
rotated_hours = {}
|
||||||
for hour, value in list(stats['story_hours_history'].items()):
|
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)
|
stats['classifier_counts'] = json.decode(feed.data.feed_classifier_counts)
|
||||||
|
|
||||||
# Fetch histories
|
# 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['feed_fetch_history'] = fetch_history['feed_fetch_history']
|
||||||
stats['page_fetch_history'] = fetch_history['page_fetch_history']
|
stats['page_fetch_history'] = fetch_history['page_fetch_history']
|
||||||
stats['feed_push_history'] = fetch_history['push_history']
|
stats['feed_push_history'] = fetch_history['push_history']
|
||||||
|
@ -515,11 +515,13 @@ def status(request):
|
||||||
|
|
||||||
@json.json_view
|
@json.json_view
|
||||||
def original_text(request):
|
def original_text(request):
|
||||||
story_id = request.GET.get('story_id')
|
# iOS sends a POST, web sends a GET
|
||||||
feed_id = request.GET.get('feed_id')
|
GET_POST = getattr(request, request.method)
|
||||||
story_hash = request.GET.get('story_hash', None)
|
story_id = GET_POST.get('story_id')
|
||||||
force = request.GET.get('force', False)
|
feed_id = GET_POST.get('feed_id')
|
||||||
debug = request.GET.get('debug', False)
|
story_hash = GET_POST.get('story_hash', None)
|
||||||
|
force = GET_POST.get('force', False)
|
||||||
|
debug = GET_POST.get('debug', False)
|
||||||
|
|
||||||
if story_hash:
|
if story_hash:
|
||||||
story, _ = MStory.find_story(story_hash=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,
|
'failed': not original_text or len(original_text) < 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
@required_params('story_hash')
|
@required_params('story_hash', method="GET")
|
||||||
def original_story(request):
|
def original_story(request):
|
||||||
story_hash = request.GET.get('story_hash')
|
story_hash = request.GET.get('story_hash')
|
||||||
force = request.GET.get('force', False)
|
force = request.GET.get('force', False)
|
||||||
|
@ -559,7 +561,7 @@ def original_story(request):
|
||||||
|
|
||||||
return HttpResponse(original_page or "")
|
return HttpResponse(original_page or "")
|
||||||
|
|
||||||
@required_params('story_hash')
|
@required_params('story_hash', method="GET")
|
||||||
@json.json_view
|
@json.json_view
|
||||||
def story_changes(request):
|
def story_changes(request):
|
||||||
story_hash = request.GET.get('story_hash', None)
|
story_hash = request.GET.get('story_hash', None)
|
||||||
|
|
|
@ -78,7 +78,7 @@ class MUserSearch(mongo.Document):
|
||||||
logging.user(user, "~FCIndexing ~SB%s feeds~SN in %s chunks..." %
|
logging.user(user, "~FCIndexing ~SB%s feeds~SN in %s chunks..." %
|
||||||
(total, len(feed_id_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
|
user_id=self.user_id
|
||||||
).set(queue='search_indexer')
|
).set(queue='search_indexer')
|
||||||
for feed_id_chunk in feed_id_chunks]
|
for feed_id_chunk in feed_id_chunks]
|
||||||
|
|
|
@ -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):
|
user_search = MUserSearch.get_user(user_id)
|
||||||
from apps.search.models import MUserSearch
|
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
|
user_search = MUserSearch.get_user(user_id)
|
||||||
|
user_search.index_subscriptions_chunk_for_search(feed_ids)
|
||||||
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)
|
|
||||||
|
|
||||||
class IndexFeedsForSearch(Task):
|
@app.task()
|
||||||
|
def IndexFeedsForSearch(feed_ids, user_id):
|
||||||
|
from apps.search.models import MUserSearch
|
||||||
|
|
||||||
def run(self, feed_ids, user_id):
|
MUserSearch.index_feeds_for_search(feed_ids, user_id)
|
||||||
from apps.search.models import MUserSearch
|
|
||||||
|
|
||||||
MUserSearch.index_feeds_for_search(feed_ids, user_id)
|
|
||||||
|
|
|
@ -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')]
|
image_sources = [img.get('src') for img in soup.findAll('img') if img and img.get('src')]
|
||||||
if len(image_sources) > 0:
|
if len(image_sources) > 0:
|
||||||
self.image_urls = image_sources
|
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()
|
self.save()
|
||||||
|
|
||||||
def calculate_image_sizes(self, force=False):
|
def calculate_image_sizes(self, force=False):
|
||||||
|
@ -2314,10 +2321,7 @@ class MSharedStory(mongo.DynamicDocument):
|
||||||
return self.image_sizes
|
return self.image_sizes
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
'User-Agent': 'NewsBlur Image Fetcher - %s '
|
'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)' % (
|
|
||||||
settings.NEWSBLUR_URL
|
settings.NEWSBLUR_URL
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -2328,7 +2332,7 @@ class MSharedStory(mongo.DynamicDocument):
|
||||||
for image_source in self.image_urls[:10]:
|
for image_source in self.image_urls[:10]:
|
||||||
if any(ignore in image_source for ignore in IGNORE_IMAGE_SOURCES):
|
if any(ignore in image_source for ignore in IGNORE_IMAGE_SOURCES):
|
||||||
continue
|
continue
|
||||||
req = requests.get(image_source, headers=headers, stream=True)
|
req = requests.get(image_source, headers=headers, stream=True, timeout=10)
|
||||||
try:
|
try:
|
||||||
datastream = BytesIO(req.content)
|
datastream = BytesIO(req.content)
|
||||||
width, height = ImageOps.image_size(datastream)
|
width, height = ImageOps.image_size(datastream)
|
||||||
|
@ -2713,7 +2717,7 @@ class MSocialServices(mongo.Document):
|
||||||
os.remove(filename)
|
os.remove(filename)
|
||||||
else:
|
else:
|
||||||
api.update_status(status=message)
|
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)
|
user = User.objects.get(pk=self.user_id)
|
||||||
logging.user(user, "~FRTwitter error: ~SB%s" % e)
|
logging.user(user, "~FRTwitter error: ~SB%s" % e)
|
||||||
return
|
return
|
||||||
|
@ -2728,7 +2732,7 @@ class MSocialServices(mongo.Document):
|
||||||
|
|
||||||
url = shared_story.image_urls[0]
|
url = shared_story.image_urls[0]
|
||||||
image_filename = os.path.basename(urllib.parse.urlparse(url).path)
|
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)
|
filename = "/tmp/%s-%s" % (shared_story.story_hash, image_filename)
|
||||||
|
|
||||||
if req.status_code == 200:
|
if req.status_code == 200:
|
||||||
|
|
|
@ -1,92 +1,79 @@
|
||||||
from bson.objectid import ObjectId
|
from bson.objectid import ObjectId
|
||||||
from celery import Task
|
from newsblur.celeryapp import app
|
||||||
from apps.social.models import MSharedStory, MSocialProfile, MSocialServices, MSocialSubscription
|
from apps.social.models import MSharedStory, MSocialProfile, MSocialServices, MSocialSubscription
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from utils import log as logging
|
from utils import log as logging
|
||||||
|
|
||||||
|
|
||||||
class PostToService(Task):
|
@app.task()
|
||||||
|
def PostToService(shared_story_id, service):
|
||||||
def run(self, shared_story_id, service):
|
try:
|
||||||
try:
|
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
|
||||||
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
|
shared_story.post_to_service(service)
|
||||||
shared_story.post_to_service(service)
|
except MSharedStory.DoesNotExist:
|
||||||
except MSharedStory.DoesNotExist:
|
logging.debug(" ---> Shared story not found (%s). Can't post to: %s" % (shared_story_id, service))
|
||||||
logging.debug(" ---> Shared story not found (%s). Can't post to: %s" % (shared_story_id, service))
|
|
||||||
|
|
||||||
class EmailNewFollower(Task):
|
@app.task()
|
||||||
|
def EmailNewFollower(follower_user_id, followee_user_id):
|
||||||
def run(self, follower_user_id, followee_user_id):
|
user_profile = MSocialProfile.get_user(followee_user_id)
|
||||||
user_profile = MSocialProfile.get_user(followee_user_id)
|
user_profile.send_email_for_new_follower(follower_user_id)
|
||||||
user_profile.send_email_for_new_follower(follower_user_id)
|
|
||||||
|
|
||||||
class EmailFollowRequest(Task):
|
@app.task()
|
||||||
|
def EmailFollowRequest(follower_user_id, followee_user_id):
|
||||||
def run(self, follower_user_id, followee_user_id):
|
user_profile = MSocialProfile.get_user(followee_user_id)
|
||||||
user_profile = MSocialProfile.get_user(followee_user_id)
|
user_profile.send_email_for_follow_request(follower_user_id)
|
||||||
user_profile.send_email_for_follow_request(follower_user_id)
|
|
||||||
|
|
||||||
class EmailFirstShare(Task):
|
@app.task()
|
||||||
|
def EmailFirstShare(user_id):
|
||||||
def run(self, user_id):
|
user = User.objects.get(pk=user_id)
|
||||||
user = User.objects.get(pk=user_id)
|
user.profile.send_first_share_to_blurblog_email()
|
||||||
user.profile.send_first_share_to_blurblog_email()
|
|
||||||
|
|
||||||
class EmailCommentReplies(Task):
|
@app.task()
|
||||||
|
def EmailCommentReplies(shared_story_id, reply_id):
|
||||||
def run(self, shared_story_id, reply_id):
|
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
|
||||||
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
|
shared_story.send_emails_for_new_reply(ObjectId(reply_id))
|
||||||
shared_story.send_emails_for_new_reply(ObjectId(reply_id))
|
|
||||||
|
|
||||||
class EmailStoryReshares(Task):
|
@app.task()
|
||||||
|
def EmailStoryReshares(shared_story_id):
|
||||||
def run(self, shared_story_id):
|
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
|
||||||
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
|
shared_story.send_email_for_reshare()
|
||||||
shared_story.send_email_for_reshare()
|
|
||||||
|
|
||||||
class SyncTwitterFriends(Task):
|
@app.task()
|
||||||
|
def SyncTwitterFriends(user_id):
|
||||||
def run(self, user_id):
|
social_services = MSocialServices.objects.get(user_id=user_id)
|
||||||
social_services = MSocialServices.objects.get(user_id=user_id)
|
social_services.sync_twitter_friends()
|
||||||
social_services.sync_twitter_friends()
|
|
||||||
|
|
||||||
class SyncFacebookFriends(Task):
|
@app.task()
|
||||||
|
def SyncFacebookFriends(user_id):
|
||||||
def run(self, user_id):
|
social_services = MSocialServices.objects.get(user_id=user_id)
|
||||||
social_services = MSocialServices.objects.get(user_id=user_id)
|
social_services.sync_facebook_friends()
|
||||||
social_services.sync_facebook_friends()
|
|
||||||
|
|
||||||
class SharePopularStories(Task):
|
@app.task(name="share-popular-stories")
|
||||||
name = 'share-popular-stories'
|
def SharePopularStories():
|
||||||
|
logging.debug(" ---> Sharing popular stories...")
|
||||||
def run(self, **kwargs):
|
MSharedStory.share_popular_stories(interactive=False)
|
||||||
logging.debug(" ---> Sharing popular stories...")
|
|
||||||
MSharedStory.share_popular_stories(interactive=False)
|
|
||||||
|
|
||||||
class CleanSocialSpam(Task):
|
@app.task(name='clean-social-spam')
|
||||||
name = 'clean-social-spam'
|
def CleanSocialSpam():
|
||||||
|
logging.debug(" ---> Finding social spammers...")
|
||||||
def run(self, **kwargs):
|
MSharedStory.count_potential_spammers(destroy=True)
|
||||||
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):
|
logging.debug(" ---> ~FM~SNFlipping unread recalc for ~SB%s~SN subscriptions to ~SB%s's blurblog~SN" % (
|
||||||
user = User.objects.get(pk=subscription_user_id)
|
socialsubs.count(),
|
||||||
socialsubs = MSocialSubscription.objects.filter(subscription_user_id=subscription_user_id)
|
user.username
|
||||||
try:
|
))
|
||||||
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
|
for socialsub in socialsubs:
|
||||||
except MSharedStory.DoesNotExist:
|
socialsub.needs_unread_recalc = True
|
||||||
return
|
socialsub.save()
|
||||||
|
|
||||||
logging.debug(" ---> ~FM~SNFlipping unread recalc for ~SB%s~SN subscriptions to ~SB%s's blurblog~SN" % (
|
shared_story.publish_update_to_subscribers()
|
||||||
socialsubs.count(),
|
|
||||||
user.username
|
|
||||||
))
|
|
||||||
for socialsub in socialsubs:
|
|
||||||
socialsub.needs_unread_recalc = True
|
|
||||||
socialsub.save()
|
|
||||||
|
|
||||||
shared_story.publish_update_to_subscribers()
|
|
||||||
|
|
|
@ -507,7 +507,7 @@ def load_social_page(request, user_id, username=None, **kwargs):
|
||||||
|
|
||||||
return render(request, template, params)
|
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):
|
def story_public_comments(request):
|
||||||
format = request.GET.get('format', 'json')
|
format = request.GET.get('format', 'json')
|
||||||
relative_user_id = request.GET.get('user_id', None)
|
relative_user_id = request.GET.get('user_id', None)
|
||||||
|
@ -1175,7 +1175,7 @@ def ignore_follower(request):
|
||||||
return {'code': code}
|
return {'code': code}
|
||||||
|
|
||||||
|
|
||||||
@required_params('query')
|
@required_params('query', method="GET")
|
||||||
@json.json_view
|
@json.json_view
|
||||||
def find_friends(request):
|
def find_friends(request):
|
||||||
query = request.GET['query']
|
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')
|
return HttpResponse(rss.writeString('utf-8'), content_type='application/rss+xml')
|
||||||
|
|
||||||
@required_params('user_id')
|
@required_params('user_id', method="GET")
|
||||||
@json.json_view
|
@json.json_view
|
||||||
def social_feed_trainer(request):
|
def social_feed_trainer(request):
|
||||||
social_user_id = request.GET['user_id']
|
social_user_id = request.GET['user_id']
|
||||||
|
|
|
@ -15,7 +15,7 @@ def faq(request):
|
||||||
return render(request, 'static/faq.xhtml')
|
return render(request, 'static/faq.xhtml')
|
||||||
|
|
||||||
def api(request):
|
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()
|
api_yml_file = open(filename).read()
|
||||||
data = yaml.load(api_yml_file)
|
data = yaml.load(api_yml_file)
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,17 @@
|
||||||
from celery import Task
|
from newsblur.celeryapp import app
|
||||||
from apps.statistics.models import MStatistics
|
from apps.statistics.models import MStatistics
|
||||||
from apps.statistics.models import MFeedback
|
from apps.statistics.models import MFeedback
|
||||||
# from utils import log as logging
|
from utils import log as logging
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CollectStats(Task):
|
@app.task(name='collect-stats')
|
||||||
name = 'collect-stats'
|
def CollectStats():
|
||||||
|
logging.debug(" ---> ~FBCollecting stats...")
|
||||||
def run(self, **kwargs):
|
MStatistics.collect_statistics()
|
||||||
# logging.debug(" ---> ~FBCollecting stats...")
|
|
||||||
MStatistics.collect_statistics()
|
|
||||||
|
|
||||||
|
|
||||||
class CollectFeedback(Task):
|
@app.task(name='collect-feedback')
|
||||||
name = 'collect-feedback'
|
def CollectFeedback():
|
||||||
|
logging.debug(" ---> ~FBCollecting feedback...")
|
||||||
def run(self, **kwargs):
|
MFeedback.collect_feedback()
|
||||||
# logging.debug(" ---> ~FBCollecting feedback...")
|
|
||||||
MFeedback.collect_feedback()
|
|
||||||
|
|
|
@ -128,6 +128,13 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activity.InAppBrowser" />
|
android:name=".activity.InAppBrowser" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".activity.Premium" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".activity.MuteConfig"
|
||||||
|
android:launchMode="singleTask"/>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activity.SearchForFeeds" android:launchMode="singleTop" >
|
android:name=".activity.SearchForFeeds" android:launchMode="singleTop" >
|
||||||
|
@ -174,7 +181,7 @@
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
</receiver>
|
</receiver>
|
||||||
<provider
|
<provider
|
||||||
android:name="android.support.v4.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="com.newsblur.fileprovider"
|
android:authorities="com.newsblur.fileprovider"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
buildscript {
|
buildscript {
|
||||||
|
ext.kotlin_version = '1.4.10'
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven {
|
maven {
|
||||||
|
@ -8,7 +9,8 @@ buildscript {
|
||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
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'
|
url 'https://maven.google.com'
|
||||||
}
|
}
|
||||||
jcenter()
|
jcenter()
|
||||||
|
google()
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'checkstyle'
|
apply plugin: 'checkstyle'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.android.support:support-core-utils:28.0.0'
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
implementation 'com.android.support:support-fragment:28.0.0'
|
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||||
implementation 'com.android.support:support-core-ui:28.0.0'
|
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||||
implementation 'com.squareup.okhttp3:okhttp:3.12.12'
|
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.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 {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 29
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.newsblur"
|
applicationId "com.newsblur"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 28
|
targetSdkVersion 29
|
||||||
versionCode 168
|
versionCode 177
|
||||||
versionName "10.1"
|
versionName "10.1.1"
|
||||||
}
|
}
|
||||||
compileOptions.with {
|
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 {
|
sourceSets {
|
||||||
main {
|
main {
|
||||||
|
|
2
clients/android/NewsBlur/gradle.properties
Normal file
2
clients/android/NewsBlur/gradle.properties
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
android.enableJetifier=true
|
||||||
|
android.useAndroidX=true
|
20
clients/android/NewsBlur/release/output.json
Normal file
20
clients/android/NewsBlur/release/output.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -9,8 +9,7 @@
|
||||||
<item
|
<item
|
||||||
android:top="0.5dp"
|
android:top="0.5dp"
|
||||||
android:bottom="0.5dp">
|
android:bottom="0.5dp">
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
<shape android:shape="rectangle">
|
||||||
android:shape="rectangle">
|
|
||||||
<solid android:color="@color/dark_feed_background_selected_end"/>
|
<solid android:color="@color/dark_feed_background_selected_end"/>
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
|
|
|
@ -9,8 +9,7 @@
|
||||||
<item
|
<item
|
||||||
android:top="0.5dp"
|
android:top="0.5dp"
|
||||||
android:bottom="0.5dp">
|
android:bottom="0.5dp">
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
<shape android:shape="rectangle">
|
||||||
android:shape="rectangle">
|
|
||||||
<solid android:color="@color/feed_background_selected_end"/>
|
<solid android:color="@color/feed_background_selected_end"/>
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
|
|
9
clients/android/NewsBlur/res/drawable/ic_bookmark.xml
Normal file
9
clients/android/NewsBlur/res/drawable/ic_bookmark.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#A6A6A6"
|
||||||
|
android:pathData="M19,18l2,1V3c0,-1.1 -0.9,-2 -2,-2H8.99C7.89,1 7,1.9 7,3h10c1.1,0 2,0.9 2,2v13zM15,5H5c-1.1,0 -2,0.9 -2,2v16l7,-3 7,3V7c0,-1.1 -0.9,-2 -2,-2z" />
|
||||||
|
</vector>
|
9
clients/android/NewsBlur/res/drawable/ic_dining.xml
Normal file
9
clients/android/NewsBlur/res/drawable/ic_dining.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#A6A6A6"
|
||||||
|
android:pathData="M8.1,13.34l2.83,-2.83L3.91,3.5c-1.56,1.56 -1.56,4.09 0,5.66l4.19,4.18zM14.88,11.53c1.53,0.71 3.68,0.21 5.27,-1.38 1.91,-1.91 2.28,-4.65 0.81,-6.12 -1.46,-1.46 -4.2,-1.1 -6.12,0.81 -1.59,1.59 -2.09,3.74 -1.38,5.27L3.7,19.87l1.41,1.41L12,14.41l6.88,6.88 1.41,-1.41L13.41,13l1.47,-1.47z" />
|
||||||
|
</vector>
|
9
clients/android/NewsBlur/res/drawable/ic_folder.xml
Normal file
9
clients/android/NewsBlur/res/drawable/ic_folder.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#A6A6A6"
|
||||||
|
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z" />
|
||||||
|
</vector>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#A6A6A6"
|
||||||
|
android:pathData="M21,9A1,1 0,0 1,22 10A1,1 0,0 1,21 11H16.53L16.4,12.21L14.2,17.15C14,17.65 13.47,18 12.86,18H8.5C7.7,18 7,17.27 7,16.5V10C7,9.61 7.16,9.26 7.43,9L11.63,4.1L12.4,4.84C12.6,5.03 12.72,5.29 12.72,5.58L12.69,5.8L11,9H21M2,18V10H5V18H2Z" />
|
||||||
|
</vector>
|
9
clients/android/NewsBlur/res/drawable/ic_lock.xml
Normal file
9
clients/android/NewsBlur/res/drawable/ic_lock.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#A6A6A6"
|
||||||
|
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z" />
|
||||||
|
</vector>
|
12
clients/android/NewsBlur/res/drawable/ic_rss_feed.xml
Normal file
12
clients/android/NewsBlur/res/drawable/ic_rss_feed.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#A6A6A6"
|
||||||
|
android:pathData="M6.18,17.82m-2.18,0a2.18,2.18 0,1 1,4.36 0a2.18,2.18 0,1 1,-4.36 0" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#A6A6A6"
|
||||||
|
android:pathData="M4,4.44v2.83c7.03,0 12.73,5.7 12.73,12.73h2.83c0,-8.59 -6.97,-15.56 -15.56,-15.56zM4,10.1v2.83c3.9,0 7.07,3.17 7.07,7.07h2.83c0,-5.47 -4.43,-9.9 -9.9,-9.9z" />
|
||||||
|
</vector>
|
9
clients/android/NewsBlur/res/drawable/ic_search.xml
Normal file
9
clients/android/NewsBlur/res/drawable/ic_search.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#A6A6A6"
|
||||||
|
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
|
||||||
|
</vector>
|
9
clients/android/NewsBlur/res/drawable/ic_sync.xml
Normal file
9
clients/android/NewsBlur/res/drawable/ic_sync.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#A6A6A6"
|
||||||
|
android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z" />
|
||||||
|
</vector>
|
9
clients/android/NewsBlur/res/drawable/ic_text.xml
Normal file
9
clients/android/NewsBlur/res/drawable/ic_text.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#A6A6A6"
|
||||||
|
android:pathData="M14,17L4,17v2h10v-2zM20,9L4,9v2h16L20,9zM4,15h16v-2L4,13v2zM4,5v2h16L20,5L4,5z" />
|
||||||
|
</vector>
|
BIN
clients/android/NewsBlur/res/drawable/mute_feed_off.webp
Normal file
BIN
clients/android/NewsBlur/res/drawable/mute_feed_off.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 828 B |
BIN
clients/android/NewsBlur/res/drawable/mute_feed_on.webp
Normal file
BIN
clients/android/NewsBlur/res/drawable/mute_feed_on.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 686 B |
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:orientation="vertical" android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
</LinearLayout>
|
|
|
@ -27,6 +27,13 @@
|
||||||
android:layout_below="@id/itemlist_search_query"
|
android:layout_below="@id/itemlist_search_query"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<include layout="@layout/row_fleuron"
|
||||||
|
android:id="@+id/footer_fleuron"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/itemlist_search_query"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/itemlist_sync_status"
|
android:id="@+id/itemlist_sync_status"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
|
|
|
@ -167,7 +167,7 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- The scrollable and pull-able feed list. -->
|
<!-- The scrollable and pull-able feed list. -->
|
||||||
<android.support.v4.widget.SwipeRefreshLayout
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/swipe_container"
|
android:id="@+id/swipe_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -180,7 +180,7 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:tag="folderFeedListFragment" />
|
android:tag="folderFeedListFragment" />
|
||||||
|
|
||||||
</android.support.v4.widget.SwipeRefreshLayout>
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
<!-- top_bar_border -->
|
<!-- top_bar_border -->
|
||||||
<View
|
<View
|
||||||
|
|
60
clients/android/NewsBlur/res/layout/activity_mute_config.xml
Normal file
60
clients/android/NewsBlur/res/layout/activity_mute_config.xml
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:animateLayoutChanges="true">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/container_sites_count"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="4dp"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_reset_sites"
|
||||||
|
style="?linkText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:text="@string/mute_config_reset_button"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_sites"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end|center_vertical"
|
||||||
|
android:text="@string/mute_config_sites"
|
||||||
|
android:textColor="@color/positive"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<ExpandableListView
|
||||||
|
android:id="@+id/list_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:groupIndicator="@null" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_sync_status"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:background="@color/status_overlay_background"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="2dp"
|
||||||
|
android:textColor="@color/status_overlay_text"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
293
clients/android/NewsBlur/res/layout/activity_premium.xml
Normal file
293
clients/android/NewsBlur/res/layout/activity_premium.xml
Normal file
|
@ -0,0 +1,293 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:animateLayoutChanges="true">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/container_going_premium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:text="@string/premium_title_going_premium"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:lineSpacingExtra="@dimen/extra_line_spacing"
|
||||||
|
android:text="@string/premium_subtitle"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_policies"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:lineSpacingExtra="@dimen/extra_line_spacing"
|
||||||
|
android:text="@string/premium_policies"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="40dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:src="@drawable/ic_hand_pointing_right" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/container_sub"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="40dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_sub_title"
|
||||||
|
style="?linkText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:text="@string/premium_subscription_title"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_sub_price"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/premium_subscription_price"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_loading"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="@string/loading"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="40dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:src="@drawable/ic_sync" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="40dp"
|
||||||
|
android:text="@string/premium_sync" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:src="@drawable/ic_folder" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="40dp"
|
||||||
|
android:text="@string/premium_read_by_folder" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:src="@drawable/ic_search" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="40dp"
|
||||||
|
android:text="@string/premium_search" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:src="@drawable/ic_bookmark" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="40dp"
|
||||||
|
android:text="@string/premium_searchable_tags" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:src="@drawable/ic_lock" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="40dp"
|
||||||
|
android:text="@string/premium_privacy_options" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:src="@drawable/ic_rss_feed" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="40dp"
|
||||||
|
android:text="@string/premium_custom_rss" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:src="@drawable/ic_text" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="40dp"
|
||||||
|
android:text="@string/premium_text_view" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:src="@drawable/ic_dining" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="40dp"
|
||||||
|
android:text="@string/premium_shiloh" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/img_shiloh"
|
||||||
|
android:layout_width="104dp"
|
||||||
|
android:layout_height="104dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:scaleType="centerCrop" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/container_gone_premium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="120dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:text="@string/premium_title_gone_premium"
|
||||||
|
android:textSize="40sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_subscription_renewal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="320dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:lineSpacingExtra="@dimen/extra_line_spacing"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<nl.dionsegijn.konfetti.KonfettiView
|
||||||
|
android:id="@+id/konfetti"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
|
@ -10,13 +10,13 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<android.support.v4.view.ViewPager
|
<androidx.viewpager.widget.ViewPager
|
||||||
android:id="@+id/activity_details_pager"
|
android:id="@+id/activity_details_pager"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_below="@id/profile_details">
|
android:layout_below="@id/profile_details">
|
||||||
|
|
||||||
<android.support.v4.view.PagerTitleStrip
|
<androidx.viewpager.widget.PagerTitleStrip
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="top"
|
android:layout_gravity="top"
|
||||||
|
@ -24,6 +24,6 @@
|
||||||
android:paddingTop="4dp"
|
android:paddingTop="4dp"
|
||||||
android:paddingBottom="4dp" />
|
android:paddingBottom="4dp" />
|
||||||
|
|
||||||
</android.support.v4.view.ViewPager>
|
</androidx.viewpager.widget.ViewPager>
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
|
@ -78,11 +78,11 @@
|
||||||
android:layout_height="1dp"
|
android:layout_height="1dp"
|
||||||
android:background="?android:attr/listDivider" />
|
android:background="?android:attr/listDivider" />
|
||||||
|
|
||||||
<android.support.v7.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recycler_view_folders"
|
android:id="@+id/recycler_view_folders"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
app:layoutManager="android.support.v7.widget.LinearLayoutManager" />
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
|
@ -8,7 +8,7 @@
|
||||||
android:id="@+id/choose_folders_list"
|
android:id="@+id/choose_folders_list"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
style="?divider"
|
style="?android:listDivider"
|
||||||
android:dividerHeight="2dp" />
|
android:dividerHeight="2dp" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
android:layout_height="6dp"
|
android:layout_height="6dp"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<android.support.v7.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/itemgridfragment_grid"
|
android:id="@+id/itemgridfragment_grid"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
|
|
@ -130,7 +130,7 @@
|
||||||
android:id="@+id/share_bar_underline"
|
android:id="@+id/share_bar_underline"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="3dp"
|
android:layout_height="3dp"
|
||||||
style="?divider"
|
style="?android:divider"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
>
|
>
|
||||||
|
|
||||||
<android.support.v4.view.ViewPager
|
<androidx.viewpager.widget.ViewPager
|
||||||
android:id="@+id/reading_pager"
|
android:id="@+id/reading_pager"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
|
|
@ -2,17 +2,41 @@
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:gravity="center"
|
||||||
android:gravity="center"
|
android:orientation="vertical">
|
||||||
>
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/fleuron"
|
android:id="@+id/fleuron"
|
||||||
android:src="@drawable/fleuron"
|
|
||||||
android:layout_height="32dp"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="32dp"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:layout_marginBottom="4dp"
|
android:layout_marginBottom="4dp"
|
||||||
/>
|
android:src="@drawable/fleuron" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/container_subscribe"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_subscription"
|
||||||
|
style="?defaultText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:text="@string/premium_subscribers_folder" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="?linkText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:text="@string/premium_subscribers" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
android:id="@+id/check_box"
|
android:id="@+id/check_box"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
android:clickable="false"
|
android:clickable="false"
|
||||||
android:focusable="false" />
|
android:focusable="false" />
|
||||||
|
|
||||||
|
@ -20,7 +21,6 @@
|
||||||
android:layout_width="19dp"
|
android:layout_width="19dp"
|
||||||
android:layout_height="19dp"
|
android:layout_height="19dp"
|
||||||
android:layout_gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
android:layout_marginStart="4dp"
|
|
||||||
android:contentDescription="@string/description_row_feed_icon"
|
android:contentDescription="@string/description_row_feed_icon"
|
||||||
android:scaleType="centerCrop" />
|
android:scaleType="centerCrop" />
|
||||||
|
|
||||||
|
@ -39,4 +39,13 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/img_toggle"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:contentDescription="@string/description_row_feed_mute_toggle"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
|
@ -63,6 +63,9 @@
|
||||||
</group>
|
</group>
|
||||||
</menu>
|
</menu>
|
||||||
</item>
|
</item>
|
||||||
|
<item android:id="@+id/menu_statistics"
|
||||||
|
android:title="@string/menu_statistics"
|
||||||
|
android:showAsAction="never" />
|
||||||
<item android:id="@+id/menu_delete_feed"
|
<item android:id="@+id/menu_delete_feed"
|
||||||
android:title="@string/menu_delete_feed"
|
android:title="@string/menu_delete_feed"
|
||||||
android:showAsAction="never" />
|
android:showAsAction="never" />
|
||||||
|
|
|
@ -21,6 +21,10 @@
|
||||||
android:title="@string/settings"
|
android:title="@string/settings"
|
||||||
android:showAsAction="never" />
|
android:showAsAction="never" />
|
||||||
|
|
||||||
|
<item android:id="@+id/menu_mute_sites"
|
||||||
|
android:title="@string/mute_sites"
|
||||||
|
android:showAsAction="never" />
|
||||||
|
|
||||||
<item android:id="@+id/menu_widget"
|
<item android:id="@+id/menu_widget"
|
||||||
android:title="@string/widget"
|
android:title="@string/widget"
|
||||||
android:showAsAction="never" />
|
android:showAsAction="never" />
|
||||||
|
@ -43,6 +47,10 @@
|
||||||
android:title="@string/menu_loginas"
|
android:title="@string/menu_loginas"
|
||||||
android:showAsAction="never"
|
android:showAsAction="never"
|
||||||
android:visible="false"/>
|
android:visible="false"/>
|
||||||
|
|
||||||
|
<item android:id="@+id/menu_premium_account"
|
||||||
|
android:title="@string/menu_premium_account"
|
||||||
|
android:showAsAction="never" />
|
||||||
|
|
||||||
<item android:id="@+id/menu_logout"
|
<item android:id="@+id/menu_logout"
|
||||||
android:title="@string/menu_logout"
|
android:title="@string/menu_logout"
|
||||||
|
|
|
@ -54,6 +54,12 @@
|
||||||
</group>
|
</group>
|
||||||
</menu>
|
</menu>
|
||||||
</item>
|
</item>
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_mute_all"
|
||||||
|
android:title="@string/menu_mute_all" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_mute_none"
|
||||||
|
android:title="@string/menu_mute_none" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_select_all"
|
android:id="@+id/menu_select_all"
|
||||||
android:title="@string/menu_select_all" />
|
android:title="@string/menu_select_all" />
|
|
@ -23,7 +23,6 @@
|
||||||
<attr name="commentsHeader" format="string" />
|
<attr name="commentsHeader" format="string" />
|
||||||
<attr name="rowBorderTop" format="string" />
|
<attr name="rowBorderTop" format="string" />
|
||||||
<attr name="rowBorderBottom" format="string" />
|
<attr name="rowBorderBottom" format="string" />
|
||||||
<attr name="divider" format="string" />
|
|
||||||
<attr name="profileCount" format="string" />
|
<attr name="profileCount" format="string" />
|
||||||
<attr name="profileActivityList" format="string" />
|
<attr name="profileActivityList" format="string" />
|
||||||
<attr name="itemHeaderDivider" format="string" />
|
<attr name="itemHeaderDivider" format="string" />
|
||||||
|
|
|
@ -2,4 +2,5 @@
|
||||||
<resources>
|
<resources>
|
||||||
<dimen name="thumbnails_small_size">50dp</dimen>
|
<dimen name="thumbnails_small_size">50dp</dimen>
|
||||||
<dimen name="thumbnails_size">90dp</dimen>
|
<dimen name="thumbnails_size">90dp</dimen>
|
||||||
|
<dimen name="extra_line_spacing">4dp</dimen>
|
||||||
</resources>
|
</resources>
|
|
@ -16,8 +16,7 @@
|
||||||
<string name="login_next">Next</string>
|
<string name="login_next">Next</string>
|
||||||
|
|
||||||
<string name="title_feed_search">Search for feeds</string>
|
<string name="title_feed_search">Search for feeds</string>
|
||||||
<string name="add_feed_message">Add \"%s\" to your feeds?</string>
|
|
||||||
|
|
||||||
<string name="loading">Loading…</string>
|
<string name="loading">Loading…</string>
|
||||||
<string name="orig_text_loading">Fetching story text…</string>
|
<string name="orig_text_loading">Fetching story text…</string>
|
||||||
|
|
||||||
|
@ -29,6 +28,7 @@
|
||||||
<string name="description_profile_picture">The user\'s profile picture</string>
|
<string name="description_profile_picture">The user\'s profile picture</string>
|
||||||
<string name="description_row_folder_icon">folder icon</string>
|
<string name="description_row_folder_icon">folder icon</string>
|
||||||
<string name="description_row_feed_icon">feed icon</string>
|
<string name="description_row_feed_icon">feed icon</string>
|
||||||
|
<string name="description_row_feed_mute_toggle">feed mute toggle</string>
|
||||||
<string name="description_activity_icon">An icon illustrating the user\'s activity</string>
|
<string name="description_activity_icon">An icon illustrating the user\'s activity</string>
|
||||||
<string name="description_follow_button">Follow or unfollow a user</string>
|
<string name="description_follow_button">Follow or unfollow a user</string>
|
||||||
<string name="description_comment_user">Comment user image</string>
|
<string name="description_comment_user">Comment user image</string>
|
||||||
|
@ -135,6 +135,7 @@
|
||||||
<string name="menu_send_story_full">Send story to…</string>
|
<string name="menu_send_story_full">Send story to…</string>
|
||||||
<string name="menu_mark_feed_as_read">Mark feed as read</string>
|
<string name="menu_mark_feed_as_read">Mark feed as read</string>
|
||||||
<string name="menu_delete_feed">Delete feed</string>
|
<string name="menu_delete_feed">Delete feed</string>
|
||||||
|
<string name="menu_statistics">Statistics</string>
|
||||||
<string name="menu_delete_saved_search">Delete saved search</string>
|
<string name="menu_delete_saved_search">Delete saved search</string>
|
||||||
<string name="menu_unfollow">Unfollow user</string>
|
<string name="menu_unfollow">Unfollow user</string>
|
||||||
<string name="menu_choose_folders">Choose folders</string>
|
<string name="menu_choose_folders">Choose folders</string>
|
||||||
|
@ -184,6 +185,8 @@
|
||||||
<string name="menu_folder_view">Folder View</string>
|
<string name="menu_folder_view">Folder View</string>
|
||||||
<string name="menu_folder_view_nested">Nested</string>
|
<string name="menu_folder_view_nested">Nested</string>
|
||||||
<string name="menu_folder_view_flat">Flat</string>
|
<string name="menu_folder_view_flat">Flat</string>
|
||||||
|
<string name="menu_mute_all">Mute All</string>
|
||||||
|
<string name="menu_mute_none">Mute None</string>
|
||||||
<string name="menu_select_all">Select All</string>
|
<string name="menu_select_all">Select All</string>
|
||||||
<string name="menu_select_none">Select None</string>
|
<string name="menu_select_none">Select None</string>
|
||||||
<string name="menu_widget_background">Widget Background</string>
|
<string name="menu_widget_background">Widget Background</string>
|
||||||
|
@ -213,6 +216,7 @@
|
||||||
<string name="menu_feedback_post">Create a feedback post</string>
|
<string name="menu_feedback_post">Create a feedback post</string>
|
||||||
<string name="menu_feedback_email">Email a bug report</string>
|
<string name="menu_feedback_email">Email a bug report</string>
|
||||||
<string name="menu_theme_choose">Theme…</string>
|
<string name="menu_theme_choose">Theme…</string>
|
||||||
|
<string name="menu_premium_account">Premium Account</string>
|
||||||
|
|
||||||
<string name="description_add_new_folder_icon">Add new folder icon</string>
|
<string name="description_add_new_folder_icon">Add new folder icon</string>
|
||||||
|
|
||||||
|
@ -255,10 +259,34 @@
|
||||||
<string name="feed_stories_per_month">%d stories/month</string>
|
<string name="feed_stories_per_month">%d stories/month</string>
|
||||||
|
|
||||||
<string name="settings">Preferences</string>
|
<string name="settings">Preferences</string>
|
||||||
|
<string name="mute_sites">Mute Sites</string>
|
||||||
<string name="widget">Widget</string>
|
<string name="widget">Widget</string>
|
||||||
<string name="title_widget_setup">Tap to setup in NewsBlur</string>
|
<string name="title_widget_setup">Tap to setup in NewsBlur</string>
|
||||||
<string name="title_no_subscriptions">No active subscriptions detected</string>
|
<string name="title_no_subscriptions">No active subscriptions detected</string>
|
||||||
<string name="title_widget_loading">Loading...</string>
|
<string name="title_widget_loading">Loading...</string>
|
||||||
|
|
||||||
|
<string name="premium_subscribers_folder">Reading by folder is only available to</string>
|
||||||
|
<string name="premium_subscribers_search">Search is only available to</string>
|
||||||
|
<string name="premium_subscribers">premium subscribers</string>
|
||||||
|
<string name="premium_toolbar_title">NewsBlur Premium</string>
|
||||||
|
<string name="premium_title_going_premium">Thank you so much for going premium!</string>
|
||||||
|
<string name="premium_title_gone_premium">Thank you for going premium!</string>
|
||||||
|
<string name="premium_subscription_renewal">Your premium subscription is set to\nrenew on %s</string>
|
||||||
|
<string name="premium_subscription_expiration">Your premium subscription is set\nto expire on %s</string>
|
||||||
|
<string name="premium_subscription_no_expiration">Your premium subscription is set\nto never expire. Whoa!</string>
|
||||||
|
<string name="premium_subtitle">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.</string>
|
||||||
|
<string name="premium_policies"><![CDATA[See NewsBlur\'s <a href="https://newsblur.com/privacy">privacy policy</a> and <a href="https://newsblur.com/tos">terms of use</a> for details.]]></string>
|
||||||
|
<string name="premium_subscription_title">NewsBlur Premium Subscription</string>
|
||||||
|
<string name="premium_subscription_price">$35.99 per year ($3.00/month)</string>
|
||||||
|
<string name="premium_subscription_details_error">Error retrieving subscription details</string>
|
||||||
|
<string name="premium_sync">Sites updated up to 10x more often</string>
|
||||||
|
<string name="premium_read_by_folder">River of News (reading by folder)</string>
|
||||||
|
<string name="premium_search">Search sites and folders</string>
|
||||||
|
<string name="premium_searchable_tags">Save stories with searchable tags</string>
|
||||||
|
<string name="premium_privacy_options">Privacy options for your blurblog</string>
|
||||||
|
<string name="premium_custom_rss">Custom RSS feeds for folders and saves stories</string>
|
||||||
|
<string name="premium_text_view">Text view conveniently extracts the story</string>
|
||||||
|
<string name="premium_shiloh">You feed Shiloh, my poor, hungry dog, for a month</string>
|
||||||
|
|
||||||
<string name="settings_cat_offline">Offline</string>
|
<string name="settings_cat_offline">Offline</string>
|
||||||
<string name="settings_enable_offline">Download Stories</string>
|
<string name="settings_enable_offline">Download Stories</string>
|
||||||
|
@ -268,6 +296,12 @@
|
||||||
<string name="settings_keep_old_stories">Keep Stories after Reading</string>
|
<string name="settings_keep_old_stories">Keep Stories after Reading</string>
|
||||||
<string name="settings_keep_old_stories_sum">Disable to reduce storage usage</string>
|
<string name="settings_keep_old_stories_sum">Disable to reduce storage usage</string>
|
||||||
|
|
||||||
|
<string name="mute_config_title">You can follow up to 64 sites with a free standard account</string>
|
||||||
|
<string name="mute_config_message">Please mute %d sites or reset to most popular sites.</string>
|
||||||
|
<string name="mute_config_reset_button">RESET TO POPULAR SITES</string>
|
||||||
|
<string name="mute_config_upgrade">UPGRADE</string>
|
||||||
|
<string name="mute_config_sites">%1$s/%2$s</string>
|
||||||
|
|
||||||
<string name="menu_network_select">Download Using</string>
|
<string name="menu_network_select">Download Using</string>
|
||||||
<string name="menu_network_select_sum">Restrict background data to chosen networks</string>
|
<string name="menu_network_select_sum">Restrict background data to chosen networks</string>
|
||||||
<string name="menu_network_select_opt_any">Any Network</string>
|
<string name="menu_network_select_opt_any">Any Network</string>
|
||||||
|
@ -364,7 +398,6 @@
|
||||||
<string name="settings_reading">Reading</string>
|
<string name="settings_reading">Reading</string>
|
||||||
<string name="settings_immersive_enter_single_tap">Immersive Mode Via Single Tap</string>
|
<string name="settings_immersive_enter_single_tap">Immersive Mode Via Single Tap</string>
|
||||||
<string name="settings_show_content_preview">Show Content Preview Text</string>
|
<string name="settings_show_content_preview">Show Content Preview Text</string>
|
||||||
<string name="settings_show_thumbnails">Show Image Preview Thumbnails</string>
|
|
||||||
<string name="settings_thumbnails_style">Image Preview Thumbnails</string>
|
<string name="settings_thumbnails_style">Image Preview Thumbnails</string>
|
||||||
<string name="settings_notifications">Notifications</string>
|
<string name="settings_notifications">Notifications</string>
|
||||||
<string name="settings_enable_notifications">Enable Notifications</string>
|
<string name="settings_enable_notifications">Enable Notifications</string>
|
||||||
|
@ -414,6 +447,7 @@
|
||||||
<string name="sync_status_recounts">Catching up reading actions...</string>
|
<string name="sync_status_recounts">Catching up reading actions...</string>
|
||||||
<string name="sync_status_ffsync">On its way...</string>
|
<string name="sync_status_ffsync">On its way...</string>
|
||||||
<string name="sync_status_cleanup">Cleaning up...</string>
|
<string name="sync_status_cleanup">Cleaning up...</string>
|
||||||
|
<string name="sync_status_starred">Sync saved stories actions…</string>
|
||||||
<string name="sync_status_stories">Fetching fresh stories...</string>
|
<string name="sync_status_stories">Fetching fresh stories...</string>
|
||||||
<string name="sync_status_unreads">Storing%sunread stories...</string>
|
<string name="sync_status_unreads">Storing%sunread stories...</string>
|
||||||
<string name="sync_status_text">Storing text for %s stories...</string>
|
<string name="sync_status_text">Storing text for %s stories...</string>
|
||||||
|
@ -461,12 +495,14 @@
|
||||||
<string name="gest_action_markunread">Mark Story Unread</string>
|
<string name="gest_action_markunread">Mark Story Unread</string>
|
||||||
<string name="gest_action_save">Save Story</string>
|
<string name="gest_action_save">Save Story</string>
|
||||||
<string name="gest_action_unsave">Unsave Story</string>
|
<string name="gest_action_unsave">Unsave Story</string>
|
||||||
|
<string name="gest_action_statistics">Statistics</string>
|
||||||
<string-array name="ltr_gesture_action_entries">
|
<string-array name="ltr_gesture_action_entries">
|
||||||
<item>@string/gest_action_none</item>
|
<item>@string/gest_action_none</item>
|
||||||
<item>@string/gest_action_markread</item>
|
<item>@string/gest_action_markread</item>
|
||||||
<item>@string/gest_action_markunread</item>
|
<item>@string/gest_action_markunread</item>
|
||||||
<item>@string/gest_action_save</item>
|
<item>@string/gest_action_save</item>
|
||||||
<item>@string/gest_action_unsave</item>
|
<item>@string/gest_action_unsave</item>
|
||||||
|
<item>@string/gest_action_statistics</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="ltr_gesture_action_values">
|
<string-array name="ltr_gesture_action_values">
|
||||||
<item>GEST_ACTION_NONE</item>
|
<item>GEST_ACTION_NONE</item>
|
||||||
|
@ -474,6 +510,7 @@
|
||||||
<item>GEST_ACTION_MARKUNREAD</item>
|
<item>GEST_ACTION_MARKUNREAD</item>
|
||||||
<item>GEST_ACTION_SAVE</item>
|
<item>GEST_ACTION_SAVE</item>
|
||||||
<item>GEST_ACTION_UNSAVE</item>
|
<item>GEST_ACTION_UNSAVE</item>
|
||||||
|
<item>GEST_ACTION_STATISTICS</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string name="ltr_gesture_action_value">GEST_ACTION_MARKREAD</string>
|
<string name="ltr_gesture_action_value">GEST_ACTION_MARKREAD</string>
|
||||||
|
|
||||||
|
@ -484,6 +521,7 @@
|
||||||
<item>@string/gest_action_markunread</item>
|
<item>@string/gest_action_markunread</item>
|
||||||
<item>@string/gest_action_save</item>
|
<item>@string/gest_action_save</item>
|
||||||
<item>@string/gest_action_unsave</item>
|
<item>@string/gest_action_unsave</item>
|
||||||
|
<item>@string/gest_action_statistics</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="rtl_gesture_action_values">
|
<string-array name="rtl_gesture_action_values">
|
||||||
<item>GEST_ACTION_NONE</item>
|
<item>GEST_ACTION_NONE</item>
|
||||||
|
@ -491,6 +529,7 @@
|
||||||
<item>GEST_ACTION_MARKUNREAD</item>
|
<item>GEST_ACTION_MARKUNREAD</item>
|
||||||
<item>GEST_ACTION_SAVE</item>
|
<item>GEST_ACTION_SAVE</item>
|
||||||
<item>GEST_ACTION_UNSAVE</item>
|
<item>GEST_ACTION_UNSAVE</item>
|
||||||
|
<item>GEST_ACTION_STATISTICS</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string name="rtl_gesture_action_value">GEST_ACTION_MARKUNREAD</string>
|
<string name="rtl_gesture_action_value">GEST_ACTION_MARKUNREAD</string>
|
||||||
|
|
||||||
|
@ -556,7 +595,5 @@
|
||||||
|
|
||||||
<string name="story_notification_channel_id">story_notification_channel</string>
|
<string name="story_notification_channel_id">story_notification_channel</string>
|
||||||
<string name="story_notification_channel_name">New Stories</string>
|
<string name="story_notification_channel_name">New Stories</string>
|
||||||
<string name="save_widget">Save Widget</string>
|
|
||||||
<string name="select_feed">Select Feed</string>
|
|
||||||
<string name="go_to_feed">Go to feed</string>
|
<string name="go_to_feed">Go to feed</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package com.newsblur.activity;
|
package com.newsblur.activity;
|
||||||
|
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.support.v4.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import android.support.v4.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import android.support.v4.app.FragmentPagerAdapter;
|
import androidx.fragment.app.FragmentPagerAdapter;
|
||||||
|
|
||||||
import com.newsblur.R;
|
import com.newsblur.R;
|
||||||
import com.newsblur.domain.UserDetails;
|
import com.newsblur.domain.UserDetails;
|
||||||
|
@ -21,7 +21,7 @@ public class ActivityDetailsPagerAdapter extends FragmentPagerAdapter {
|
||||||
private final Profile profile;
|
private final Profile profile;
|
||||||
|
|
||||||
public ActivityDetailsPagerAdapter(FragmentManager fragmentManager, Profile profile) {
|
public ActivityDetailsPagerAdapter(FragmentManager fragmentManager, Profile profile) {
|
||||||
super(fragmentManager);
|
super(fragmentManager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||||
|
|
||||||
this.profile = profile;
|
this.profile = profile;
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ package com.newsblur.activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.newsblur.R;
|
import com.newsblur.R;
|
||||||
|
|
|
@ -2,8 +2,8 @@ package com.newsblur.activity;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import android.support.v4.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
@ -46,6 +46,7 @@ public class AddSocial extends NbActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, intent);
|
||||||
switch (resultCode) {
|
switch (resultCode) {
|
||||||
case AddTwitter.TWITTER_AUTHED:
|
case AddTwitter.TWITTER_AUTHED:
|
||||||
addSocialFragment.setTwitterAuthed();
|
addSocialFragment.setTwitterAuthed();
|
||||||
|
|
|
@ -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<Feed> feeds;
|
||||||
|
protected ArrayList<Folder> folders;
|
||||||
|
protected Map<String, Feed> feedMap = new HashMap<>();
|
||||||
|
protected ArrayList<String> folderNames = new ArrayList<>();
|
||||||
|
protected ArrayList<ArrayList<Feed>> 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<Cursor> loader = FeedUtils.dbHelper.getFeedsLoader();
|
||||||
|
loader.registerListener(loader.getId(), (loader1, cursor) -> processFeeds(cursor));
|
||||||
|
loader.startLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadFolders() {
|
||||||
|
Loader<Cursor> loader = FeedUtils.dbHelper.getFoldersLoader();
|
||||||
|
loader.registerListener(loader.getId(), (loader1, cursor) -> processFolders(cursor));
|
||||||
|
loader.startLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processFolders(Cursor cursor) {
|
||||||
|
ArrayList<Folder> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String> feedIds = new HashSet<>();
|
||||||
|
protected ArrayList<String> folderNames = new ArrayList<>();
|
||||||
|
protected ArrayList<ArrayList<Feed>> 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<String> activeFoldersNames, ArrayList<ArrayList<Feed>> activeFolderChildren, ArrayList<Feed> 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<Feed> feedList : this.folderChildren) {
|
||||||
|
Collections.sort(feedList, getListComparator());
|
||||||
|
}
|
||||||
|
this.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setFeedIds(Set<String> feedIds) {
|
||||||
|
this.feedIds.clear();
|
||||||
|
this.feedIds.addAll(feedIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void replaceFeedIds(Set<String> feedIds) {
|
||||||
|
setFeedIds(feedIds);
|
||||||
|
this.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Comparator<Feed> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,10 +3,14 @@ package com.newsblur.activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
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.R;
|
||||||
import com.newsblur.domain.Feed;
|
import com.newsblur.domain.Feed;
|
||||||
import com.newsblur.fragment.DeleteFeedFragment;
|
import com.newsblur.fragment.DeleteFeedFragment;
|
||||||
|
@ -14,6 +18,7 @@ import com.newsblur.fragment.FeedIntelTrainerFragment;
|
||||||
import com.newsblur.fragment.RenameDialogFragment;
|
import com.newsblur.fragment.RenameDialogFragment;
|
||||||
import com.newsblur.util.FeedSet;
|
import com.newsblur.util.FeedSet;
|
||||||
import com.newsblur.util.FeedUtils;
|
import com.newsblur.util.FeedUtils;
|
||||||
|
import com.newsblur.util.PrefsUtils;
|
||||||
import com.newsblur.util.UIUtils;
|
import com.newsblur.util.UIUtils;
|
||||||
|
|
||||||
public class FeedItemsList extends ItemsList {
|
public class FeedItemsList extends ItemsList {
|
||||||
|
@ -22,6 +27,8 @@ public class FeedItemsList extends ItemsList {
|
||||||
public static final String EXTRA_FOLDER_NAME = "folderName";
|
public static final String EXTRA_FOLDER_NAME = "folderName";
|
||||||
private Feed feed;
|
private Feed feed;
|
||||||
private String folderName;
|
private String folderName;
|
||||||
|
private ReviewManager reviewManager;
|
||||||
|
private ReviewInfo reviewInfo;
|
||||||
|
|
||||||
public static void startActivity(Context context, FeedSet feedSet,
|
public static void startActivity(Context context, FeedSet feedSet,
|
||||||
Feed feed, String folderName) {
|
Feed feed, String folderName) {
|
||||||
|
@ -36,13 +43,28 @@ public class FeedItemsList extends ItemsList {
|
||||||
protected void onCreate(Bundle bundle) {
|
protected void onCreate(Bundle bundle) {
|
||||||
feed = (Feed) getIntent().getSerializableExtra(EXTRA_FEED);
|
feed = (Feed) getIntent().getSerializableExtra(EXTRA_FEED);
|
||||||
folderName = getIntent().getStringExtra(EXTRA_FOLDER_NAME);
|
folderName = getIntent().getStringExtra(EXTRA_FOLDER_NAME);
|
||||||
|
|
||||||
super.onCreate(bundle);
|
super.onCreate(bundle);
|
||||||
|
|
||||||
UIUtils.setCustomActionBar(this, feed.faviconUrl, feed.title);
|
UIUtils.setCustomActionBar(this, feed.faviconUrl, feed.title);
|
||||||
}
|
checkInAppReview();
|
||||||
|
}
|
||||||
|
|
||||||
public void deleteFeed() {
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
// see checkInAppReview()
|
||||||
|
if (reviewInfo != null) {
|
||||||
|
Task<Void> 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);
|
DialogFragment deleteFeedFragment = DeleteFeedFragment.newInstance(feed, folderName);
|
||||||
deleteFeedFragment.show(getSupportFragmentManager(), "dialog");
|
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,
|
// 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.
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,4 +148,16 @@ public class FeedItemsList extends ItemsList {
|
||||||
String getSaveSearchFeedId() {
|
String getSaveSearchFeedId() {
|
||||||
return "feed:" + feed.feedId;
|
return "feed:" + feed.feedId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void checkInAppReview() {
|
||||||
|
if (!PrefsUtils.hasInAppReviewed(this)) {
|
||||||
|
reviewManager = ReviewManagerFactory.create(this);
|
||||||
|
Task<ReviewInfo> request = reviewManager.requestReviewFlow();
|
||||||
|
request.addOnCompleteListener(task -> {
|
||||||
|
if (task.isSuccessful()) {
|
||||||
|
reviewInfo = task.getResult();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,13 @@ package com.newsblur.activity;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.FragmentActivity;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.webkit.WebChromeClient;
|
import android.webkit.WebChromeClient;
|
||||||
import android.webkit.WebView;
|
import android.webkit.WebView;
|
||||||
import android.webkit.WebViewClient;
|
import android.webkit.WebViewClient;
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
|
||||||
import com.newsblur.databinding.ActivityInAppBrowserBinding;
|
import com.newsblur.databinding.ActivityInAppBrowserBinding;
|
||||||
import com.newsblur.util.PrefsUtils;
|
import com.newsblur.util.PrefsUtils;
|
||||||
|
|
||||||
|
@ -53,13 +54,4 @@ public class InAppBrowser extends FragmentActivity {
|
||||||
|
|
||||||
binding.webView.loadUrl(url);
|
binding.webView.loadUrl(url);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
if (binding.webView.canGoBack()) {
|
|
||||||
binding.webView.goBack();
|
|
||||||
} else {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,8 @@
|
||||||
package com.newsblur.activity;
|
package com.newsblur.activity;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import android.support.v4.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
|
@ -97,7 +97,7 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
|
||||||
if (activeSearchQuery != null) {
|
if (activeSearchQuery != null) {
|
||||||
binding.itemlistSearchQuery.setText(activeSearchQuery);
|
binding.itemlistSearchQuery.setText(activeSearchQuery);
|
||||||
binding.itemlistSearchQuery.setVisibility(View.VISIBLE);
|
binding.itemlistSearchQuery.setVisibility(View.VISIBLE);
|
||||||
fs.setSearchQuery(activeSearchQuery);
|
checkSearchQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.itemlistSearchQuery.setOnKeyListener(new OnKeyListener() {
|
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_instafetch_feed).setVisible(false);
|
||||||
menu.findItem(R.id.menu_intel).setVisible(false);
|
menu.findItem(R.id.menu_intel).setVisible(false);
|
||||||
menu.findItem(R.id.menu_rename_feed).setVisible(false);
|
menu.findItem(R.id.menu_rename_feed).setVisible(false);
|
||||||
|
menu.findItem(R.id.menu_statistics).setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.isInfrequent()) {
|
if (!fs.isInfrequent()) {
|
||||||
|
@ -349,11 +350,16 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkSearchQuery() {
|
private void checkSearchQuery() {
|
||||||
String oldQuery = fs.getSearchQuery();
|
|
||||||
String q = binding.itemlistSearchQuery.getText().toString().trim();
|
String q = binding.itemlistSearchQuery.getText().toString().trim();
|
||||||
if (q.length() < 1) {
|
if (q.length() < 1) {
|
||||||
|
updateFleuron(false);
|
||||||
q = null;
|
q = null;
|
||||||
|
} else if (!PrefsUtils.getIsPremium(this)) {
|
||||||
|
updateFleuron(true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String oldQuery = fs.getSearchQuery();
|
||||||
fs.setSearchQuery(q);
|
fs.setSearchQuery(q);
|
||||||
if (!TextUtils.equals(q, oldQuery)) {
|
if (!TextUtils.equals(q, oldQuery)) {
|
||||||
FeedUtils.prepareReadingSession(fs, true);
|
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
|
@Override
|
||||||
public void storyOrderChanged(StoryOrder newValue) {
|
public void storyOrderChanged(StoryOrder newValue) {
|
||||||
updateStoryOrderPreference(newValue);
|
updateStoryOrderPreference(newValue);
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package com.newsblur.activity;
|
package com.newsblur.activity;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import android.support.v4.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import android.support.v4.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
|
|
||||||
import com.newsblur.R;
|
import com.newsblur.R;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package com.newsblur.activity;
|
package com.newsblur.activity;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import android.support.v4.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import android.support.v4.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
|
|
||||||
import com.newsblur.R;
|
import com.newsblur.R;
|
||||||
|
|
|
@ -5,9 +5,9 @@ import android.graphics.Bitmap;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.v4.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import android.support.v4.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import android.support.v4.widget.SwipeRefreshLayout;
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
import android.util.Log;
|
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) {
|
} else if (item.getItemId() == R.id.menu_theme_black) {
|
||||||
PrefsUtils.setSelectedTheme(this, ThemeValue.BLACK);
|
PrefsUtils.setSelectedTheme(this, ThemeValue.BLACK);
|
||||||
UIUtils.restartActivity(this);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<Feed> 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<Feed> 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<String> 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<String> activeFeedIds = new HashSet<>();
|
||||||
|
Set<String> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Feed> folderChild = MuteConfigAdapter.this.folderChildren.get(groupPosition);
|
||||||
|
boolean allAreMute = true;
|
||||||
|
for (Feed feed : folderChild) {
|
||||||
|
if (feed.active) {
|
||||||
|
allAreMute = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> 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<String> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,9 @@
|
||||||
package com.newsblur.activity;
|
package com.newsblur.activity;
|
||||||
|
|
||||||
import android.appwidget.AppWidgetManager;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.newsblur.R;
|
|
||||||
import com.newsblur.util.FeedUtils;
|
import com.newsblur.util.FeedUtils;
|
||||||
import com.newsblur.util.PrefsUtils;
|
import com.newsblur.util.PrefsUtils;
|
||||||
import com.newsblur.util.PrefConstants.ThemeValue;
|
import com.newsblur.util.PrefConstants.ThemeValue;
|
||||||
|
|
308
clients/android/NewsBlur/src/com/newsblur/activity/Premium.java
Normal file
308
clients/android/NewsBlur/src/com/newsblur/activity/Premium.java
Normal file
|
@ -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<String> 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<SkuDetails> 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<Void, Void, NewsBlurResponse>() {
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,9 +2,9 @@ package com.newsblur.activity;
|
||||||
|
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import android.support.v4.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
import android.support.v4.view.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,12 @@ import android.content.res.Configuration;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import android.support.v4.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
import android.support.v4.app.LoaderManager;
|
import androidx.loader.app.LoaderManager;
|
||||||
import android.support.v4.content.Loader;
|
import androidx.loader.content.Loader;
|
||||||
import android.support.v4.view.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
import android.support.v4.view.ViewPager.OnPageChangeListener;
|
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
|
@ -178,7 +178,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
||||||
transaction.commit();
|
transaction.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
getSupportLoaderManager().initLoader(0, null, this);
|
LoaderManager.getInstance(this).initLoader(0, null, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -436,7 +436,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
||||||
private void updateCursor() {
|
private void updateCursor() {
|
||||||
synchronized (STORIES_MUTEX) {
|
synchronized (STORIES_MUTEX) {
|
||||||
try {
|
try {
|
||||||
getSupportLoaderManager().restartLoader(0, null, this);
|
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||||
} catch (IllegalStateException ise) {
|
} catch (IllegalStateException ise) {
|
||||||
; // our heavy use of async can race loader calls, which it will gripe about, but this
|
; // 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.
|
// is only a refresh call, so dropping a refresh during creation is perfectly fine.
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package com.newsblur.activity;
|
package com.newsblur.activity;
|
||||||
|
|
||||||
import android.support.v4.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import android.support.v4.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
|
|
||||||
import com.newsblur.R;
|
import com.newsblur.R;
|
||||||
import com.newsblur.fragment.RegisterProgressFragment;
|
import com.newsblur.fragment.RegisterProgressFragment;
|
||||||
|
|
|
@ -8,9 +8,10 @@ import java.util.Set;
|
||||||
import android.app.SearchManager;
|
import android.app.SearchManager;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
import androidx.loader.app.LoaderManager;
|
||||||
import android.support.v4.content.Loader;
|
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||||
|
import androidx.loader.content.Loader;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
@ -55,7 +56,7 @@ public class SearchForFeeds extends NbActivity implements LoaderCallbacks<Search
|
||||||
resultsList.setEmptyView(emptyView);
|
resultsList.setEmptyView(emptyView);
|
||||||
resultsList.setOnItemClickListener(this);
|
resultsList.setOnItemClickListener(this);
|
||||||
resultsList.setItemsCanFocus(false);
|
resultsList.setItemsCanFocus(false);
|
||||||
searchLoader = getSupportLoaderManager().initLoader(0, new Bundle(), this);
|
searchLoader = LoaderManager.getInstance(this).initLoader(0, new Bundle(), this);
|
||||||
|
|
||||||
onSearchRequested();
|
onSearchRequested();
|
||||||
}
|
}
|
||||||
|
@ -70,6 +71,7 @@ public class SearchForFeeds extends NbActivity implements LoaderCallbacks<Search
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onNewIntent(Intent intent) {
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
setIntent(intent);
|
setIntent(intent);
|
||||||
handleIntent(intent);
|
handleIntent(intent);
|
||||||
}
|
}
|
||||||
|
@ -83,7 +85,7 @@ public class SearchForFeeds extends NbActivity implements LoaderCallbacks<Search
|
||||||
|
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putString(SearchAsyncTaskLoader.SEARCH_TERM, query);
|
bundle.putString(SearchAsyncTaskLoader.SEARCH_TERM, query);
|
||||||
searchLoader = getSupportLoaderManager().restartLoader(0, bundle, this);
|
searchLoader = LoaderManager.getInstance(this).restartLoader(0, bundle, this);
|
||||||
|
|
||||||
searchLoader.forceLoad();
|
searchLoader.forceLoad();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package com.newsblur.activity;
|
package com.newsblur.activity;
|
||||||
|
|
||||||
import android.support.v4.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
package com.newsblur.activity;
|
package com.newsblur.activity;
|
||||||
|
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.v4.content.Loader;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
@ -14,43 +10,18 @@ import com.newsblur.R;
|
||||||
import com.newsblur.databinding.ActivityWidgetConfigBinding;
|
import com.newsblur.databinding.ActivityWidgetConfigBinding;
|
||||||
import com.newsblur.domain.Feed;
|
import com.newsblur.domain.Feed;
|
||||||
import com.newsblur.domain.Folder;
|
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.PrefsUtils;
|
||||||
import com.newsblur.util.WidgetBackground;
|
|
||||||
import com.newsblur.widget.WidgetUtils;
|
import com.newsblur.widget.WidgetUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class WidgetConfig extends NbActivity {
|
public class WidgetConfig extends FeedChooser {
|
||||||
|
|
||||||
private WidgetConfigAdapter adapter;
|
|
||||||
private ArrayList<Feed> feeds;
|
|
||||||
private ArrayList<Folder> folders;
|
|
||||||
private Map<String, Feed> feedMap = new HashMap<>();
|
|
||||||
private ArrayList<String> folderNames = new ArrayList<>();
|
|
||||||
private ArrayList<ArrayList<Feed>> folderChildren = new ArrayList<>();
|
|
||||||
private ActivityWidgetConfigBinding binding;
|
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
|
@Override
|
||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
@ -61,127 +32,46 @@ public class WidgetConfig extends NbActivity {
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
MenuInflater inflater = getMenuInflater();
|
MenuInflater inflater = getMenuInflater();
|
||||||
inflater.inflate(R.menu.menu_widget, menu);
|
inflater.inflate(R.menu.menu_feed_chooser, menu);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||||
super.onPrepareOptionsMenu(menu);
|
super.onPrepareOptionsMenu(menu);
|
||||||
ListOrderFilter listOrderFilter = PrefsUtils.getWidgetConfigListOrder(this);
|
menu.findItem(R.id.menu_mute_all).setVisible(false);
|
||||||
if (listOrderFilter == ListOrderFilter.ASCENDING) {
|
menu.findItem(R.id.menu_mute_none).setVisible(false);
|
||||||
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);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
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:
|
case R.id.menu_select_all:
|
||||||
selectAllFeeds();
|
selectAllFeeds();
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_select_none:
|
case R.id.menu_select_none:
|
||||||
replaceWidgetFeedIds(Collections.<String>emptySet());
|
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);
|
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupList() {
|
@Override
|
||||||
|
void bindLayout() {
|
||||||
|
binding = ActivityWidgetConfigBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void setupList() {
|
||||||
adapter = new WidgetConfigAdapter(this);
|
adapter = new WidgetConfigAdapter(this);
|
||||||
binding.listView.setAdapter(adapter);
|
binding.listView.setAdapter(adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadFeeds() {
|
@Override
|
||||||
Loader<Cursor> loader = FeedUtils.dbHelper.getFeedsLoader();
|
void processFeeds(Cursor cursor) {
|
||||||
loader.registerListener(loader.getId(), new Loader.OnLoadCompleteListener<Cursor>() {
|
|
||||||
@Override
|
|
||||||
public void onLoadComplete(@NonNull Loader<Cursor> loader, @Nullable Cursor cursor) {
|
|
||||||
processFeeds(cursor);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
loader.startLoading();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadFolders() {
|
|
||||||
Loader<Cursor> loader = FeedUtils.dbHelper.getFoldersLoader();
|
|
||||||
loader.registerListener(loader.getId(), new Loader.OnLoadCompleteListener<Cursor>() {
|
|
||||||
@Override
|
|
||||||
public void onLoadComplete(@NonNull Loader<Cursor> loader, @Nullable Cursor cursor) {
|
|
||||||
processFolders(cursor);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
loader.startLoading();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processFeeds(Cursor cursor) {
|
|
||||||
ArrayList<Feed> feeds = new ArrayList<>();
|
ArrayList<Feed> feeds = new ArrayList<>();
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
Feed feed = Feed.fromCursor(cursor);
|
Feed feed = Feed.fromCursor(cursor);
|
||||||
|
@ -194,25 +84,25 @@ public class WidgetConfig extends NbActivity {
|
||||||
processData();
|
processData();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processFolders(Cursor cursor) {
|
@Override
|
||||||
ArrayList<Folder> folders = new ArrayList<>();
|
public void setAdapterData() {
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
Set<String> feedIds = PrefsUtils.getWidgetFeedIds(this);
|
||||||
Folder folder = Folder.fromCursor(cursor);
|
// by default select all feeds
|
||||||
if (!folder.feedIds.isEmpty()) {
|
if (feedIds == null) {
|
||||||
folders.add(folder);
|
feedIds = new HashSet<>(this.feeds.size());
|
||||||
|
for (Feed feed : this.feeds) {
|
||||||
|
feedIds.add(feed.feedId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.folders = folders;
|
adapter.setFeedIds(feedIds);
|
||||||
Collections.sort(this.folders, new Comparator<Folder>() {
|
|
||||||
@Override
|
super.setAdapterData();
|
||||||
public int compare(Folder o1, Folder o2) {
|
binding.listView.setVisibility(this.feeds.isEmpty() ? View.GONE : View.VISIBLE);
|
||||||
return Folder.compareFolderNames(o1.flatName(), o2.flatName());
|
binding.textNoSubscriptions.setVisibility(this.feeds.isEmpty() ? View.VISIBLE : View.GONE);
|
||||||
}
|
|
||||||
});
|
|
||||||
processData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processData() {
|
@Override
|
||||||
|
void processData() {
|
||||||
if (folders != null && feeds != null) {
|
if (folders != null && feeds != null) {
|
||||||
for (Folder folder : folders) {
|
for (Folder folder : folders) {
|
||||||
ArrayList<Feed> activeFeeds = new ArrayList<>();
|
ArrayList<Feed> activeFeeds = new ArrayList<>();
|
||||||
|
@ -226,7 +116,6 @@ public class WidgetConfig extends NbActivity {
|
||||||
folderChildren.add(activeFeeds);
|
folderChildren.add(activeFeeds);
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedFeeds();
|
|
||||||
setAdapterData();
|
setAdapterData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -243,44 +132,4 @@ public class WidgetConfig extends NbActivity {
|
||||||
PrefsUtils.setWidgetFeedIds(this, feedIds);
|
PrefsUtils.setWidgetFeedIds(this, feedIds);
|
||||||
adapter.replaceFeedIds(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<String> 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,296 +1,72 @@
|
||||||
package com.newsblur.activity;
|
package com.newsblur.activity;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.format.DateUtils;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.BaseExpandableListAdapter;
|
|
||||||
import android.widget.CheckBox;
|
import android.widget.CheckBox;
|
||||||
import android.widget.ExpandableListView;
|
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.newsblur.R;
|
import com.newsblur.R;
|
||||||
import com.newsblur.domain.Feed;
|
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 com.newsblur.util.PrefsUtils;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
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 {
|
public class WidgetConfigAdapter extends FeedChooserAdapter {
|
||||||
|
|
||||||
private final static int defaultTextSizeChild = 14;
|
|
||||||
private final static int defaultTextSizeGroup = 13;
|
|
||||||
|
|
||||||
private Set<String> feedIds = new HashSet<>();
|
|
||||||
private ArrayList<String> folderNames = new ArrayList<>();
|
|
||||||
private ArrayList<ArrayList<Feed>> folderChildren = new ArrayList<>();
|
|
||||||
|
|
||||||
private FolderViewFilter folderViewFilter;
|
|
||||||
private ListOrderFilter listOrderFilter;
|
|
||||||
private FeedOrderFilter feedOrderFilter;
|
|
||||||
|
|
||||||
private float textSize;
|
|
||||||
|
|
||||||
WidgetConfigAdapter(Context context) {
|
WidgetConfigAdapter(Context context) {
|
||||||
folderViewFilter = PrefsUtils.getWidgetConfigFolderView(context);
|
super(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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View getGroupView(final int groupPosition, boolean isExpanded, View convertView, final ViewGroup parent) {
|
public View getGroupView(final int groupPosition, boolean isExpanded, View convertView, final ViewGroup parent) {
|
||||||
String folderName = folderNames.get(groupPosition);
|
View groupView = super.getGroupView(groupPosition, isExpanded, convertView, parent);
|
||||||
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);
|
groupView.setOnClickListener(v -> {
|
||||||
|
ArrayList<Feed> folderChild = WidgetConfigAdapter.this.folderChildren.get(groupPosition);
|
||||||
convertView.setOnClickListener(new View.OnClickListener() {
|
// check all is selected
|
||||||
@Override
|
boolean allSelected = true;
|
||||||
public void onClick(View v) {
|
for (Feed feed : folderChild) {
|
||||||
ArrayList<Feed> folderChild = WidgetConfigAdapter.this.folderChildren.get(groupPosition);
|
if (!feedIds.contains(feed.feedId)) {
|
||||||
// check all is selected
|
allSelected = false;
|
||||||
boolean allSelected = true;
|
break;
|
||||||
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
|
@Override
|
||||||
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, final ViewGroup parent) {
|
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, final ViewGroup parent) {
|
||||||
if (convertView == null) {
|
View childView = super.getChildView(groupPosition, childPosition, isLastChild, convertView, parent);
|
||||||
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_feed, parent, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Feed feed = folderChildren.get(groupPosition).get(childPosition);
|
final Feed feed = folderChildren.get(groupPosition).get(childPosition);
|
||||||
TextView textTitle = convertView.findViewById(R.id.text_title);
|
final CheckBox checkBox = childView.findViewById(R.id.check_box);
|
||||||
TextView textDetails = convertView.findViewById(R.id.text_details);
|
final ImageView imgToggle = childView.findViewById(R.id.img_toggle);
|
||||||
final CheckBox checkBox = convertView.findViewById(R.id.check_box);
|
checkBox.setVisibility(View.VISIBLE);
|
||||||
ImageView img = convertView.findViewById(R.id.img);
|
imgToggle.setVisibility(View.GONE);
|
||||||
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) {
|
childView.setOnClickListener(v -> {
|
||||||
textDetails.setText(parent.getContext().getString(R.string.feed_opens, feed.feedOpens));
|
checkBox.setChecked(!checkBox.isChecked());
|
||||||
} else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS) {
|
if (checkBox.isChecked()) {
|
||||||
textDetails.setText(parent.getContext().getString(R.string.feed_subscribers, feed.subscribers));
|
feedIds.add(feed.feedId);
|
||||||
} else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH) {
|
} else {
|
||||||
textDetails.setText(parent.getContext().getString(R.string.feed_stories_per_month, feed.storiesPerMonth));
|
feedIds.remove(feed.feedId);
|
||||||
} 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());
|
|
||||||
}
|
}
|
||||||
|
setWidgetFeedIds(parent.getContext());
|
||||||
});
|
});
|
||||||
return convertView;
|
return childView;
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isChildSelectable(int groupPosition, int childPosition) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean areAllItemsEnabled() {
|
|
||||||
return super.areAllItemsEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setData(ArrayList<String> activeFoldersNames, ArrayList<ArrayList<Feed>> activeFolderChildren, ArrayList<Feed> 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<Feed> feedList : this.folderChildren) {
|
|
||||||
Collections.sort(feedList, getListComparator());
|
|
||||||
}
|
|
||||||
this.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setFeedIds(Set<String> feedIds) {
|
|
||||||
this.feedIds.clear();
|
|
||||||
this.feedIds.addAll(feedIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
void replaceFeedIds(Set<String> feedIds) {
|
|
||||||
setFeedIds(feedIds);
|
|
||||||
this.notifyDataSetChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setWidgetFeedIds(Context context) {
|
private void setWidgetFeedIds(Context context) {
|
||||||
PrefsUtils.setWidgetFeedIds(context, feedIds);
|
PrefsUtils.setWidgetFeedIds(context, feedIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Comparator<Feed> getListComparator() {
|
|
||||||
return new Comparator<Feed>() {
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,9 @@ import android.database.Cursor;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.CancellationSignal;
|
import android.os.CancellationSignal;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import android.support.v4.content.AsyncTaskLoader;
|
import androidx.loader.content.AsyncTaskLoader;
|
||||||
import android.support.v4.content.Loader;
|
import androidx.loader.content.Loader;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
@ -284,6 +284,19 @@ public class BlurDatabaseHelper {
|
||||||
return hashes;
|
return hashes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<String> getStarredStoryHashes() {
|
||||||
|
String q = "SELECT " + DatabaseConstants.STORY_HASH +
|
||||||
|
" FROM " + DatabaseConstants.STORY_TABLE +
|
||||||
|
" WHERE " + DatabaseConstants.STORY_STARRED + " = 1" ;
|
||||||
|
Cursor c = dbRO.rawQuery(q, null);
|
||||||
|
Set<String> hashes = new HashSet<>(c.getCount());
|
||||||
|
while (c.moveToNext()) {
|
||||||
|
hashes.add(c.getString(c.getColumnIndexOrThrow(DatabaseConstants.STORY_HASH)));
|
||||||
|
}
|
||||||
|
c.close();
|
||||||
|
return hashes;
|
||||||
|
}
|
||||||
|
|
||||||
public Set<String> getAllStoryImages() {
|
public Set<String> getAllStoryImages() {
|
||||||
Cursor c = dbRO.query(DatabaseConstants.STORY_TABLE, new String[]{DatabaseConstants.STORY_IMAGE_URLS}, null, null, null, null, null);
|
Cursor c = dbRO.query(DatabaseConstants.STORY_TABLE, new String[]{DatabaseConstants.STORY_IMAGE_URLS}, null, null, null, null, null);
|
||||||
Set<String> urls = new HashSet<String>(c.getCount());
|
Set<String> urls = new HashSet<String>(c.getCount());
|
||||||
|
@ -584,6 +597,22 @@ public class BlurDatabaseHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void markStoryHashesStarred(Collection<String> 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<String> feedIds, boolean active) {
|
public void setFeedsActive(Set<String> feedIds, boolean active) {
|
||||||
synchronized (RW_MUTEX) {
|
synchronized (RW_MUTEX) {
|
||||||
dbRW.beginTransaction();
|
dbRW.beginTransaction();
|
||||||
|
|
|
@ -75,6 +75,8 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
||||||
public int totalSocialNeutCount = 0;
|
public int totalSocialNeutCount = 0;
|
||||||
/** Total positive unreads for all social feeds. */
|
/** Total positive unreads for all social feeds. */
|
||||||
public int totalSocialPosiCount = 0;
|
public int totalSocialPosiCount = 0;
|
||||||
|
/** Total active feeds. */
|
||||||
|
public int totalActiveFeedCount = 0;
|
||||||
|
|
||||||
/** Feeds, indexed by feed ID. */
|
/** Feeds, indexed by feed ID. */
|
||||||
private Map<String,Feed> feeds = Collections.emptyMap();
|
private Map<String,Feed> feeds = Collections.emptyMap();
|
||||||
|
@ -557,6 +559,7 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
||||||
feedPosCounts = new HashMap<String,Integer>();
|
feedPosCounts = new HashMap<String,Integer>();
|
||||||
totalNeutCount = 0;
|
totalNeutCount = 0;
|
||||||
totalPosCount = 0;
|
totalPosCount = 0;
|
||||||
|
totalActiveFeedCount = 0;
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
Feed f = Feed.fromCursor(cursor);
|
Feed f = Feed.fromCursor(cursor);
|
||||||
feeds.put(f.feedId, f);
|
feeds.put(f.feedId, f);
|
||||||
|
@ -570,6 +573,9 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
||||||
feedNeutCounts.put(f.feedId, neut);
|
feedNeutCounts.put(f.feedId, neut);
|
||||||
totalNeutCount += neut;
|
totalNeutCount += neut;
|
||||||
}
|
}
|
||||||
|
if (f.active) {
|
||||||
|
totalActiveFeedCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
recountFeeds();
|
recountFeeds();
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
|
|
|
@ -4,7 +4,7 @@ import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.CancellationSignal;
|
import android.os.CancellationSignal;
|
||||||
import android.os.OperationCanceledException;
|
import android.os.OperationCanceledException;
|
||||||
import android.support.v4.content.AsyncTaskLoader;
|
import androidx.loader.content.AsyncTaskLoader;
|
||||||
|
|
||||||
import com.newsblur.util.AppConstants;
|
import com.newsblur.util.AppConstants;
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,10 @@ package com.newsblur.database;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.support.v4.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import android.support.v4.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import android.support.v4.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
import android.support.v4.view.PagerAdapter;
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
@ -178,7 +178,6 @@ public class ReadingAdapter extends PagerAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fragment.setMenuVisibility(false);
|
fragment.setMenuVisibility(false);
|
||||||
fragment.setUserVisibleHint(false);
|
|
||||||
if (curTransaction == null) {
|
if (curTransaction == null) {
|
||||||
curTransaction = fm.beginTransaction();
|
curTransaction = fm.beginTransaction();
|
||||||
}
|
}
|
||||||
|
@ -208,11 +207,9 @@ public class ReadingAdapter extends PagerAdapter {
|
||||||
if (fragment != lastActiveFragment) {
|
if (fragment != lastActiveFragment) {
|
||||||
if (lastActiveFragment != null) {
|
if (lastActiveFragment != null) {
|
||||||
lastActiveFragment.setMenuVisibility(false);
|
lastActiveFragment.setMenuVisibility(false);
|
||||||
lastActiveFragment.setUserVisibleHint(false);
|
|
||||||
}
|
}
|
||||||
if (fragment != null) {
|
if (fragment != null) {
|
||||||
fragment.setMenuVisibility(true);
|
fragment.setMenuVisibility(true);
|
||||||
fragment.setUserVisibleHint(true);
|
|
||||||
}
|
}
|
||||||
lastActiveFragment = fragment;
|
lastActiveFragment = fragment;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@ import android.database.Cursor;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import android.support.v7.util.DiffUtil;
|
import androidx.recyclerview.widget.DiffUtil;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.ContextMenu;
|
import android.view.ContextMenu;
|
||||||
import android.view.GestureDetector;
|
import android.view.GestureDetector;
|
||||||
|
@ -126,7 +126,11 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getStoryCount() {
|
public int getStoryCount() {
|
||||||
return stories.size();
|
if (fs != null && UIUtils.needsPremiumAccess(context, fs)) {
|
||||||
|
return Math.min(3, stories.size());
|
||||||
|
} else {
|
||||||
|
return stories.size();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -464,6 +468,9 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
||||||
case GEST_ACTION_UNSAVE:
|
case GEST_ACTION_UNSAVE:
|
||||||
FeedUtils.setStorySaved(story, false, context, null);
|
FeedUtils.setStorySaved(story, false, context, null);
|
||||||
break;
|
break;
|
||||||
|
case GEST_ACTION_STATISTICS:
|
||||||
|
FeedUtils.openStatistics(context, story.feedId);
|
||||||
|
break;
|
||||||
case GEST_ACTION_NONE:
|
case GEST_ACTION_NONE:
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package com.newsblur.domain;
|
||||||
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
|
@ -6,12 +6,12 @@ import android.app.Dialog;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import android.support.v4.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import android.support.v7.widget.DividerItemDecoration;
|
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import java.util.HashSet;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
|
|
|
@ -4,7 +4,7 @@ import android.app.AlertDialog;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
|
||||||
import com.newsblur.R;
|
import com.newsblur.R;
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import android.app.AlertDialog;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
|
@ -15,7 +15,7 @@ import android.app.AlertDialog;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
|
||||||
public class DeleteFeedFragment extends DialogFragment {
|
public class DeleteFeedFragment extends DialogFragment {
|
||||||
private static final String FEED_TYPE = "feed_type";
|
private static final String FEED_TYPE = "feed_type";
|
||||||
|
|
|
@ -4,9 +4,9 @@ import android.app.AlertDialog;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import android.support.v4.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import com.newsblur.R;
|
import com.newsblur.R;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue