Merge branch 'master' into dejal
12
.vscode/settings.json
vendored
|
@ -4,7 +4,7 @@
|
|||
"python.linting.flake8Enabled": true,
|
||||
"python.linting.pylamaEnabled": false,
|
||||
"python.linting.flake8Args": [
|
||||
"--ignore=E501,W293,W503,W504,E302,E722,E226,E221"
|
||||
"--ignore=E501,W293,W503,W504,E302,E722,E226,E221,E402,E401"
|
||||
],
|
||||
"python.pythonPath": "~/.virtualenvs/newsblur3/bin/python",
|
||||
"git.ignoreLimitWarning": true,
|
||||
|
@ -16,7 +16,7 @@
|
|||
"ansible/playbooks/*/*": true,
|
||||
// "archive/*": true,
|
||||
"logs/*": true,
|
||||
// "static/*": true,
|
||||
"static/*": true,
|
||||
"media/fonts": true,
|
||||
"static/*.css": true,
|
||||
"static/js/*.*.js": true,
|
||||
|
@ -25,4 +25,12 @@
|
|||
"docker/volumes": true,
|
||||
"requirements.txt": true, // It's just a symlink to config/requirements.txt, which has git history
|
||||
},
|
||||
"python.formatting.provider": "black",
|
||||
"python.formatting.blackArgs": [
|
||||
"--line-length=110",
|
||||
"--skip-string-normalization"
|
||||
],
|
||||
"files.associations": {
|
||||
"*.yml": "ansible"
|
||||
},
|
||||
}
|
||||
|
|
1
Makefile
|
@ -21,6 +21,7 @@ rebuild:
|
|||
|
||||
collectstatic:
|
||||
- rm -fr static
|
||||
- docker pull newsblur/newsblur_deploy
|
||||
- docker run --rm -v $(shell pwd):/srv/newsblur newsblur/newsblur_deploy
|
||||
|
||||
#creates newsblur, builds new images, and creates/refreshes SSL keys
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
- name: Update Sentry release
|
||||
connection: local
|
||||
run_once: yes
|
||||
shell: >
|
||||
curl {{ sentry_web_release_webhook }}/ \
|
||||
-X POST \
|
||||
|
@ -80,15 +81,6 @@
|
|||
tags:
|
||||
- never
|
||||
- static
|
||||
|
||||
- name: Pull newsblur_web github
|
||||
git:
|
||||
repo: https://github.com/samuelclay/NewsBlur.git
|
||||
dest: /srv/newsblur/
|
||||
version: master
|
||||
register: pulled
|
||||
tags:
|
||||
- static
|
||||
|
||||
- name: Downloading JS/CSS assets from S3
|
||||
vars:
|
||||
|
@ -114,8 +106,17 @@
|
|||
tags:
|
||||
- never
|
||||
- static
|
||||
|
||||
- name: Pull newsblur_web github
|
||||
git:
|
||||
repo: https://github.com/samuelclay/NewsBlur.git
|
||||
dest: /srv/newsblur/
|
||||
version: master
|
||||
register: pulled
|
||||
tags:
|
||||
- static
|
||||
|
||||
- name: Reload gunicorn
|
||||
- name: Reload gunicorn due to no git upstream changes
|
||||
become: yes
|
||||
block:
|
||||
- name: Find gunicorn master process
|
||||
|
@ -123,7 +124,7 @@
|
|||
register: psaux
|
||||
- name: Reload gunicorn
|
||||
command: "kill -HUP {{ psaux.stdout }}"
|
||||
# when: pulled.changed
|
||||
when: not pulled.changed
|
||||
tags:
|
||||
- static
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
- service_name: task-celery
|
||||
- service_name: task-work
|
||||
rescue:
|
||||
- name: Restart celery
|
||||
- name: Start celery
|
||||
become: yes
|
||||
command: "docker start {{ item.service_name }}"
|
||||
when: item.service_name in inventory_hostname
|
||||
|
|
|
@ -10,5 +10,4 @@
|
|||
dest: "/srv/newsblur/config/certificates/{{ item }}"
|
||||
mode: 0400
|
||||
with_items:
|
||||
- aps.pem
|
||||
- aps.p12.pem
|
||||
|
|
7
ansible/roles/sentry/handlers/main.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
- name: reload sentry
|
||||
become: yes
|
||||
command:
|
||||
chdir: /srv/sentry/
|
||||
cmd: ./install.sh
|
||||
listen: reload sentry
|
|
@ -4,6 +4,7 @@
|
|||
repo: https://github.com/getsentry/self-hosted.git
|
||||
dest: /srv/sentry/
|
||||
version: master
|
||||
notify: reload sentry
|
||||
|
||||
- name: Register sentry in consul
|
||||
tags: consul
|
||||
|
|
|
@ -12,5 +12,6 @@ urlpatterns = [
|
|||
url(r'^share_story/(?P<token>\w+)', views.share_story, name='api-share-story'),
|
||||
url(r'^save_story/(?P<token>\w+)', views.save_story, name='api-save-story'),
|
||||
url(r'^share_story/?$', views.share_story),
|
||||
url(r'^save_story/?$', views.save_story),
|
||||
url(r'^ip_addresses/?$', views.ip_addresses),
|
||||
]
|
||||
|
|
|
@ -9,10 +9,16 @@ from django.contrib.auth.models import User
|
|||
from django.contrib.sites.models import Site
|
||||
from django.template.loader import render_to_string
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
|
||||
# from django.utils.html import strip_tags
|
||||
from apps.rss_feeds.models import MStory, Feed
|
||||
from apps.reader.models import UserSubscription
|
||||
from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag
|
||||
from apps.analyzer.models import (
|
||||
MClassifierTitle,
|
||||
MClassifierAuthor,
|
||||
MClassifierFeed,
|
||||
MClassifierTag,
|
||||
)
|
||||
from apps.analyzer.models import compute_story_score
|
||||
from utils.view_functions import is_true
|
||||
from utils.story_functions import truncate_chars
|
||||
|
@ -24,6 +30,7 @@ from apns2.payload import Payload
|
|||
from bs4 import BeautifulSoup
|
||||
import urllib.parse
|
||||
|
||||
|
||||
class NotificationFrequency(enum.Enum):
|
||||
immediately = 1
|
||||
hour_1 = 2
|
||||
|
@ -31,20 +38,25 @@ class NotificationFrequency(enum.Enum):
|
|||
hour_12 = 4
|
||||
hour_24 = 5
|
||||
|
||||
|
||||
class MUserNotificationTokens(mongo.Document):
|
||||
'''A user's push notification tokens'''
|
||||
user_id = mongo.IntField()
|
||||
ios_tokens = mongo.ListField(mongo.StringField(max_length=1024))
|
||||
use_sandbox = mongo.BooleanField(default=False)
|
||||
|
||||
|
||||
user_id = mongo.IntField()
|
||||
ios_tokens = mongo.ListField(mongo.StringField(max_length=1024))
|
||||
use_sandbox = mongo.BooleanField(default=False)
|
||||
|
||||
meta = {
|
||||
'collection': 'notification_tokens',
|
||||
'indexes': [{'fields': ['user_id'],
|
||||
'unique': True,
|
||||
}],
|
||||
'indexes': [
|
||||
{
|
||||
'fields': ['user_id'],
|
||||
'unique': True,
|
||||
}
|
||||
],
|
||||
'allow_inheritance': False,
|
||||
}
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_tokens_for_user(cls, user_id):
|
||||
try:
|
||||
|
@ -54,35 +66,43 @@ class MUserNotificationTokens(mongo.Document):
|
|||
|
||||
return tokens
|
||||
|
||||
|
||||
class MUserFeedNotification(mongo.Document):
|
||||
'''A user's notifications of a single feed.'''
|
||||
user_id = mongo.IntField()
|
||||
feed_id = mongo.IntField()
|
||||
frequency = mongoengine_fields.IntEnumField(NotificationFrequency)
|
||||
is_focus = mongo.BooleanField()
|
||||
last_notification_date = mongo.DateTimeField(default=datetime.datetime.now)
|
||||
is_email = mongo.BooleanField()
|
||||
is_web = mongo.BooleanField()
|
||||
is_ios = mongo.BooleanField()
|
||||
is_android = mongo.BooleanField()
|
||||
ios_tokens = mongo.ListField(mongo.StringField(max_length=1024))
|
||||
|
||||
|
||||
|
||||
user_id = mongo.IntField()
|
||||
feed_id = mongo.IntField()
|
||||
frequency = mongoengine_fields.IntEnumField(NotificationFrequency)
|
||||
is_focus = mongo.BooleanField()
|
||||
last_notification_date = mongo.DateTimeField(default=datetime.datetime.now)
|
||||
is_email = mongo.BooleanField()
|
||||
is_web = mongo.BooleanField()
|
||||
is_ios = mongo.BooleanField()
|
||||
is_android = mongo.BooleanField()
|
||||
ios_tokens = mongo.ListField(mongo.StringField(max_length=1024))
|
||||
|
||||
meta = {
|
||||
'collection': 'notifications',
|
||||
'indexes': ['feed_id',
|
||||
{'fields': ['user_id', 'feed_id'],
|
||||
'unique': True,
|
||||
}],
|
||||
'indexes': [
|
||||
'feed_id',
|
||||
{
|
||||
'fields': ['user_id', 'feed_id'],
|
||||
'unique': True,
|
||||
},
|
||||
],
|
||||
'allow_inheritance': False,
|
||||
}
|
||||
|
||||
|
||||
def __str__(self):
|
||||
notification_types = []
|
||||
if self.is_email: notification_types.append('email')
|
||||
if self.is_web: notification_types.append('web')
|
||||
if self.is_ios: notification_types.append('ios')
|
||||
if self.is_android: notification_types.append('android')
|
||||
if self.is_email:
|
||||
notification_types.append('email')
|
||||
if self.is_web:
|
||||
notification_types.append('web')
|
||||
if self.is_ios:
|
||||
notification_types.append('ios')
|
||||
if self.is_android:
|
||||
notification_types.append('android')
|
||||
|
||||
return "%s/%s: %s -> %s" % (
|
||||
User.objects.get(pk=self.user_id).username,
|
||||
|
@ -90,17 +110,17 @@ class MUserFeedNotification(mongo.Document):
|
|||
','.join(notification_types),
|
||||
self.last_notification_date,
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def feed_has_users(cls, feed_id):
|
||||
return cls.users_for_feed(feed_id).count()
|
||||
|
||||
|
||||
@classmethod
|
||||
def users_for_feed(cls, feed_id):
|
||||
notifications = cls.objects.filter(feed_id=feed_id)
|
||||
|
||||
|
||||
return notifications
|
||||
|
||||
|
||||
@classmethod
|
||||
def feeds_for_user(cls, user_id):
|
||||
notifications = cls.objects.filter(user_id=user_id)
|
||||
|
@ -111,37 +131,52 @@ class MUserFeedNotification(mongo.Document):
|
|||
'notification_types': [],
|
||||
'notification_filter': "focus" if feed.is_focus else "unread",
|
||||
}
|
||||
if feed.is_email: notifications_by_feed[feed.feed_id]['notification_types'].append('email')
|
||||
if feed.is_web: notifications_by_feed[feed.feed_id]['notification_types'].append('web')
|
||||
if feed.is_ios: notifications_by_feed[feed.feed_id]['notification_types'].append('ios')
|
||||
if feed.is_android: notifications_by_feed[feed.feed_id]['notification_types'].append('android')
|
||||
|
||||
if feed.is_email:
|
||||
notifications_by_feed[feed.feed_id]['notification_types'].append('email')
|
||||
if feed.is_web:
|
||||
notifications_by_feed[feed.feed_id]['notification_types'].append('web')
|
||||
if feed.is_ios:
|
||||
notifications_by_feed[feed.feed_id]['notification_types'].append('ios')
|
||||
if feed.is_android:
|
||||
notifications_by_feed[feed.feed_id]['notification_types'].append('android')
|
||||
|
||||
return notifications_by_feed
|
||||
|
||||
|
||||
@classmethod
|
||||
def push_feed_notifications(cls, feed_id, new_stories, force=False):
|
||||
feed = Feed.get_by_id(feed_id)
|
||||
notifications = MUserFeedNotification.users_for_feed(feed.pk)
|
||||
logging.debug(" ---> [%-30s] ~FCPushing out notifications to ~SB%s users~SN for ~FB~SB%s stories" % (
|
||||
feed, len(notifications), new_stories))
|
||||
logging.debug(
|
||||
" ---> [%-30s] ~FCPushing out notifications to ~SB%s users~SN for ~FB~SB%s stories"
|
||||
% (feed, len(notifications), new_stories)
|
||||
)
|
||||
r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL)
|
||||
|
||||
|
||||
latest_story_hashes = r.zrange("zF:%s" % feed.pk, -1 * new_stories, -1)
|
||||
mstories = MStory.objects.filter(story_hash__in=latest_story_hashes).order_by('-story_date')
|
||||
stories = Feed.format_stories(mstories)
|
||||
total_sent_count = 0
|
||||
|
||||
|
||||
for user_feed_notification in notifications:
|
||||
sent_count = 0
|
||||
try:
|
||||
user = User.objects.get(pk=user_feed_notification.user_id)
|
||||
except User.DoesNotExist:
|
||||
continue
|
||||
months_ago = datetime.datetime.now() - datetime.timedelta(days=90)
|
||||
if user.profile.last_seen_on < months_ago:
|
||||
logging.user(user, f"~FBSkipping notifications, last seen: ~SB{user.profile.last_seen_on}")
|
||||
continue
|
||||
last_notification_date = user_feed_notification.last_notification_date
|
||||
try:
|
||||
usersub = UserSubscription.objects.get(user=user_feed_notification.user_id,
|
||||
feed=user_feed_notification.feed_id)
|
||||
usersub = UserSubscription.objects.get(
|
||||
user=user_feed_notification.user_id, feed=user_feed_notification.feed_id
|
||||
)
|
||||
except UserSubscription.DoesNotExist:
|
||||
continue
|
||||
classifiers = user_feed_notification.classifiers(usersub)
|
||||
|
||||
if classifiers == None:
|
||||
if classifiers is None:
|
||||
if settings.DEBUG:
|
||||
logging.debug("Has no usersubs")
|
||||
continue
|
||||
|
@ -150,35 +185,47 @@ class MUserFeedNotification(mongo.Document):
|
|||
if sent_count >= 3:
|
||||
if settings.DEBUG:
|
||||
logging.debug("Sent too many, ignoring...")
|
||||
continue
|
||||
continue
|
||||
if story['story_date'] <= last_notification_date and not force:
|
||||
if settings.DEBUG:
|
||||
logging.debug("Story date older than last notification date: %s <= %s" % (story['story_date'], last_notification_date))
|
||||
logging.debug(
|
||||
"Story date older than last notification date: %s <= %s"
|
||||
% (story['story_date'], last_notification_date)
|
||||
)
|
||||
continue
|
||||
|
||||
|
||||
if story['story_date'] > user_feed_notification.last_notification_date:
|
||||
user_feed_notification.last_notification_date = story['story_date']
|
||||
user_feed_notification.save()
|
||||
|
||||
|
||||
story['story_content'] = html.unescape(story['story_content'])
|
||||
|
||||
|
||||
sent = user_feed_notification.push_story_notification(story, classifiers, usersub)
|
||||
if sent:
|
||||
if sent:
|
||||
sent_count += 1
|
||||
total_sent_count += 1
|
||||
return total_sent_count, len(notifications)
|
||||
|
||||
|
||||
def classifiers(self, usersub):
|
||||
classifiers = {}
|
||||
if usersub.is_trained:
|
||||
classifiers['feeds'] = list(MClassifierFeed.objects(user_id=self.user_id, feed_id=self.feed_id,
|
||||
social_user_id=0))
|
||||
classifiers['authors'] = list(MClassifierAuthor.objects(user_id=self.user_id, feed_id=self.feed_id))
|
||||
classifiers['titles'] = list(MClassifierTitle.objects(user_id=self.user_id, feed_id=self.feed_id))
|
||||
classifiers['tags'] = list(MClassifierTag.objects(user_id=self.user_id, feed_id=self.feed_id))
|
||||
|
||||
classifiers['feeds'] = list(
|
||||
MClassifierFeed.objects(
|
||||
user_id=self.user_id, feed_id=self.feed_id, social_user_id=0
|
||||
)
|
||||
)
|
||||
classifiers['authors'] = list(
|
||||
MClassifierAuthor.objects(user_id=self.user_id, feed_id=self.feed_id)
|
||||
)
|
||||
classifiers['titles'] = list(
|
||||
MClassifierTitle.objects(user_id=self.user_id, feed_id=self.feed_id)
|
||||
)
|
||||
classifiers['tags'] = list(
|
||||
MClassifierTag.objects(user_id=self.user_id, feed_id=self.feed_id)
|
||||
)
|
||||
|
||||
return classifiers
|
||||
|
||||
|
||||
def title_and_body(self, story, usersub, notification_title_only=False):
|
||||
def replace_with_newlines(element):
|
||||
text = ''
|
||||
|
@ -191,33 +238,36 @@ class MUserFeedNotification(mongo.Document):
|
|||
text += '\n\n'
|
||||
text = re.sub(r' +', ' ', text).strip()
|
||||
return text
|
||||
|
||||
|
||||
feed_title = usersub.user_title or usersub.feed.feed_title
|
||||
# title = "%s: %s" % (feed_title, story['story_title'])
|
||||
title = feed_title
|
||||
soup = BeautifulSoup(story['story_content'].strip(), features="lxml")
|
||||
if notification_title_only:
|
||||
subtitle = None
|
||||
body_title = html.unescape(story['story_title']).strip()
|
||||
body_content = replace_with_newlines(soup)
|
||||
if body_content:
|
||||
if body_title == body_content[:len(body_title)] or body_content[:100] == body_title[:100]:
|
||||
body_content = ""
|
||||
else:
|
||||
body_content = f"\n※ {body_content}"
|
||||
body = f"{body_title}{body_content}"
|
||||
else:
|
||||
subtitle = html.unescape(story['story_title'])
|
||||
body = replace_with_newlines(soup)
|
||||
# if notification_title_only:
|
||||
subtitle = None
|
||||
body_title = html.unescape(story['story_title']).strip()
|
||||
body_content = replace_with_newlines(soup)
|
||||
if body_content:
|
||||
if (
|
||||
body_title == body_content[: len(body_title)]
|
||||
or body_content[:100] == body_title[:100]
|
||||
):
|
||||
body_content = ""
|
||||
else:
|
||||
body_content = f"\n※ {body_content}"
|
||||
body = f"{body_title}{body_content}"
|
||||
# else:
|
||||
# subtitle = html.unescape(story['story_title'])
|
||||
# body = replace_with_newlines(soup)
|
||||
body = truncate_chars(body.strip(), 600)
|
||||
if not body:
|
||||
body = " "
|
||||
|
||||
|
||||
if not usersub.user.profile.is_premium:
|
||||
body = "Please upgrade to a premium subscription to receive full push notifications."
|
||||
|
||||
|
||||
return title, subtitle, body
|
||||
|
||||
|
||||
def push_story_notification(self, story, classifiers, usersub):
|
||||
story_score = self.story_score(story, classifiers)
|
||||
if self.is_focus and story_score <= 0:
|
||||
|
@ -228,49 +278,72 @@ class MUserFeedNotification(mongo.Document):
|
|||
if settings.DEBUG:
|
||||
logging.debug("Is unread, but story is hidden")
|
||||
return False
|
||||
|
||||
|
||||
user = User.objects.get(pk=self.user_id)
|
||||
logging.user(user, "~FCSending push notification: %s/%s (score: %s)" % (story['story_title'][:40], story['story_hash'], story_score))
|
||||
|
||||
logging.user(
|
||||
user,
|
||||
"~FCSending push notification: %s/%s (score: %s)"
|
||||
% (story['story_title'][:40], story['story_hash'], story_score),
|
||||
)
|
||||
|
||||
self.send_web(story, user)
|
||||
self.send_ios(story, user, usersub)
|
||||
self.send_android(story)
|
||||
self.send_email(story, usersub)
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def send_web(self, story, user):
|
||||
if not self.is_web: return
|
||||
|
||||
if not self.is_web:
|
||||
return
|
||||
|
||||
r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL)
|
||||
r.publish(user.username, 'notification:%s,%s' % (story['story_hash'], story['story_title']))
|
||||
|
||||
|
||||
def send_ios(self, story, user, usersub):
|
||||
if not self.is_ios: return
|
||||
if not self.is_ios:
|
||||
return
|
||||
|
||||
tokens = MUserNotificationTokens.get_tokens_for_user(self.user_id)
|
||||
apns = APNsClient('/srv/newsblur/config/certificates/aps.p12.pem', use_sandbox=tokens.use_sandbox)
|
||||
|
||||
# To update APNS:
|
||||
# 1. Create certificate signing requeswt in Keychain Access
|
||||
# 2. Upload to https://developer.apple.com/account/resources/certificates/list
|
||||
# 3. Download to secrets/certificates/ios/aps.cer
|
||||
# 4. Open in Keychain Access and export as aps.p12
|
||||
# 4. Export private key as aps_key.p12 WITH A PASSPHRASE (removed later)
|
||||
# 5. openssl pkcs12 -in aps.p12 -out aps.pem -nodes -clcerts -nokeys
|
||||
# 6. openssl pkcs12 -clcerts -nokeys -out aps.pem -in aps.p12
|
||||
# 7. cat aps.pem aps_key.noenc.pem > aps.p12.pem
|
||||
# 8. Verify: openssl s_client -connect gateway.push.apple.com:2195 -cert aps.p12.pem
|
||||
# 9. Deploy: aps -l work -t apns,repo,celery
|
||||
apns = APNsClient(
|
||||
'/srv/newsblur/config/certificates/aps.p12.pem', use_sandbox=tokens.use_sandbox
|
||||
)
|
||||
|
||||
notification_title_only = is_true(user.profile.preference_value('notification_title_only'))
|
||||
title, subtitle, body = self.title_and_body(story, usersub, notification_title_only)
|
||||
image_url = None
|
||||
if len(story['image_urls']):
|
||||
image_url = story['image_urls'][0]
|
||||
# print image_url
|
||||
|
||||
|
||||
confirmed_ios_tokens = []
|
||||
for token in tokens.ios_tokens:
|
||||
logging.user(user, '~BMStory notification by iOS: ~FY~SB%s~SN~BM~FY/~SB%s' %
|
||||
(story['story_title'][:50], usersub.feed.feed_title[:50]))
|
||||
payload = Payload(alert={'title': title,
|
||||
'subtitle': subtitle,
|
||||
'body': body},
|
||||
category="STORY_CATEGORY",
|
||||
mutable_content=True,
|
||||
custom={'story_hash': story['story_hash'],
|
||||
'story_feed_id': story['story_feed_id'],
|
||||
'image_url': image_url,
|
||||
})
|
||||
logging.user(
|
||||
user,
|
||||
'~BMStory notification by iOS: ~FY~SB%s~SN~BM~FY/~SB%s'
|
||||
% (story['story_title'][:50], usersub.feed.feed_title[:50]),
|
||||
)
|
||||
payload = Payload(
|
||||
alert={'title': title, 'subtitle': subtitle, 'body': body},
|
||||
category="STORY_CATEGORY",
|
||||
mutable_content=True,
|
||||
custom={
|
||||
'story_hash': story['story_hash'],
|
||||
'story_feed_id': story['story_feed_id'],
|
||||
'image_url': image_url,
|
||||
},
|
||||
)
|
||||
try:
|
||||
apns.send_notification(token, payload, topic="com.newsblur.NewsBlur")
|
||||
except (BadDeviceToken, Unregistered):
|
||||
|
@ -278,64 +351,77 @@ class MUserFeedNotification(mongo.Document):
|
|||
else:
|
||||
confirmed_ios_tokens.append(token)
|
||||
if settings.DEBUG:
|
||||
logging.user(user, '~BMiOS token good: ~FB~SB%s / %s' % (token[:50], len(confirmed_ios_tokens)))
|
||||
logging.user(
|
||||
user,
|
||||
'~BMiOS token good: ~FB~SB%s / %s'
|
||||
% (token[:50], len(confirmed_ios_tokens)),
|
||||
)
|
||||
|
||||
if len(confirmed_ios_tokens) < len(tokens.ios_tokens):
|
||||
tokens.ios_tokens = confirmed_ios_tokens
|
||||
tokens.save()
|
||||
|
||||
|
||||
def send_android(self, story):
|
||||
if not self.is_android: return
|
||||
|
||||
|
||||
if not self.is_android:
|
||||
return
|
||||
|
||||
def send_email(self, story, usersub):
|
||||
if not self.is_email: return
|
||||
if not self.is_email:
|
||||
return
|
||||
feed = usersub.feed
|
||||
story_content = self.sanitize_story(story['story_content'])
|
||||
|
||||
params = {
|
||||
|
||||
params = {
|
||||
"story": story,
|
||||
"story_content": story_content,
|
||||
"feed": feed,
|
||||
"feed_title": usersub.user_title or feed.feed_title,
|
||||
"favicon_border": feed.favicon_color,
|
||||
}
|
||||
from_address = 'share@newsblur.com'
|
||||
from_address = 'notifications@newsblur.com'
|
||||
to_address = '%s <%s>' % (usersub.user.username, usersub.user.email)
|
||||
text = render_to_string('mail/email_story_notification.txt', params)
|
||||
html = render_to_string('mail/email_story_notification.xhtml', params)
|
||||
text = render_to_string('mail/email_story_notification.txt', params)
|
||||
html = render_to_string('mail/email_story_notification.xhtml', params)
|
||||
subject = '%s: %s' % (usersub.user_title or usersub.feed.feed_title, story['story_title'])
|
||||
subject = subject.replace('\n', ' ')
|
||||
msg = EmailMultiAlternatives(subject, text,
|
||||
from_email='NewsBlur <%s>' % from_address,
|
||||
to=[to_address])
|
||||
msg = EmailMultiAlternatives(
|
||||
subject, text, from_email='NewsBlur <%s>' % from_address, to=[to_address]
|
||||
)
|
||||
msg.attach_alternative(html, "text/html")
|
||||
# try:
|
||||
msg.send()
|
||||
# except BotoServerError as e:
|
||||
# logging.user(usersub.user, '~BMStory notification by email error: ~FR%s' % e)
|
||||
# return
|
||||
logging.user(usersub.user, '~BMStory notification by email: ~FY~SB%s~SN~BM~FY/~SB%s' %
|
||||
(story['story_title'][:50], usersub.feed.feed_title[:50]))
|
||||
|
||||
logging.user(
|
||||
usersub.user,
|
||||
'~BMStory notification by email: ~FY~SB%s~SN~BM~FY/~SB%s'
|
||||
% (story['story_title'][:50], usersub.feed.feed_title[:50]),
|
||||
)
|
||||
|
||||
def sanitize_story(self, story_content):
|
||||
soup = BeautifulSoup(story_content.strip(), features="lxml")
|
||||
fqdn = Site.objects.get_current().domain
|
||||
|
||||
|
||||
# Convert videos in newsletters to images
|
||||
for iframe in soup("iframe"):
|
||||
url = dict(iframe.attrs).get('src', "")
|
||||
youtube_id = self.extract_youtube_id(url)
|
||||
if youtube_id:
|
||||
a = soup.new_tag('a', href=url)
|
||||
img = soup.new_tag('img', style="display: block; 'background-image': \"url(https://%s/img/reader/youtube_play.png), url(http://img.youtube.com/vi/%s/0.jpg)\"" % (fqdn, youtube_id), src='http://img.youtube.com/vi/%s/0.jpg' % youtube_id)
|
||||
img = soup.new_tag(
|
||||
'img',
|
||||
style="display: block; 'background-image': \"url(https://%s/img/reader/youtube_play.png), url(http://img.youtube.com/vi/%s/0.jpg)\""
|
||||
% (fqdn, youtube_id),
|
||||
src='http://img.youtube.com/vi/%s/0.jpg' % youtube_id,
|
||||
)
|
||||
a.insert(0, img)
|
||||
iframe.replaceWith(a)
|
||||
else:
|
||||
iframe.extract()
|
||||
|
||||
|
||||
return str(soup)
|
||||
|
||||
|
||||
def extract_youtube_id(self, url):
|
||||
youtube_id = None
|
||||
|
||||
|
@ -343,14 +429,16 @@ class MUserFeedNotification(mongo.Document):
|
|||
youtube_parts = urllib.parse.urlparse(url)
|
||||
if '/embed/' in youtube_parts.path:
|
||||
youtube_id = youtube_parts.path.replace('/embed/', '')
|
||||
|
||||
|
||||
return youtube_id
|
||||
|
||||
|
||||
def story_score(self, story, classifiers):
|
||||
score = compute_story_score(story, classifier_titles=classifiers.get('titles', []),
|
||||
classifier_authors=classifiers.get('authors', []),
|
||||
classifier_tags=classifiers.get('tags', []),
|
||||
classifier_feeds=classifiers.get('feeds', []))
|
||||
|
||||
score = compute_story_score(
|
||||
story,
|
||||
classifier_titles=classifiers.get('titles', []),
|
||||
classifier_authors=classifiers.get('authors', []),
|
||||
classifier_tags=classifiers.get('tags', []),
|
||||
classifier_feeds=classifiers.get('feeds', []),
|
||||
)
|
||||
|
||||
return score
|
||||
|
||||
|
|
|
@ -11,32 +11,41 @@ from django.template import Template, Context
|
|||
from apps.statistics.rstats import round_time
|
||||
from utils import json_functions as json
|
||||
|
||||
|
||||
class LastSeenMiddleware(object):
|
||||
def __init__(self, get_response=None):
|
||||
self.get_response = get_response
|
||||
|
||||
def process_response(self, request, response):
|
||||
if ((request.path == '/' or
|
||||
request.path.startswith('/reader/refresh_feeds') or
|
||||
request.path.startswith('/reader/load_feeds') or
|
||||
request.path.startswith('/reader/feeds'))
|
||||
if (
|
||||
(
|
||||
request.path == '/'
|
||||
or request.path.startswith('/reader/refresh_feeds')
|
||||
or request.path.startswith('/reader/load_feeds')
|
||||
or request.path.startswith('/reader/feeds')
|
||||
)
|
||||
and hasattr(request, 'user')
|
||||
and request.user.is_authenticated):
|
||||
and request.user.is_authenticated
|
||||
):
|
||||
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=60)
|
||||
ip = request.META.get('HTTP_X_FORWARDED_FOR', None) or request.META['REMOTE_ADDR']
|
||||
if request.user.profile.last_seen_on < hour_ago:
|
||||
logging.user(request, "~FG~BBRepeat visitor: ~SB%s (%s)" % (
|
||||
request.user.profile.last_seen_on, ip))
|
||||
logging.user(
|
||||
request, "~FG~BBRepeat visitor: ~SB%s (%s)" % (request.user.profile.last_seen_on, ip)
|
||||
)
|
||||
from apps.profile.tasks import CleanupUser
|
||||
|
||||
CleanupUser.delay(user_id=request.user.pk)
|
||||
elif settings.DEBUG:
|
||||
logging.user(request, "~FG~BBRepeat visitor (ignored): ~SB%s (%s)" % (
|
||||
request.user.profile.last_seen_on, ip))
|
||||
logging.user(
|
||||
request,
|
||||
"~FG~BBRepeat visitor (ignored): ~SB%s (%s)" % (request.user.profile.last_seen_on, ip),
|
||||
)
|
||||
|
||||
request.user.profile.last_seen_on = datetime.datetime.utcnow()
|
||||
request.user.profile.last_seen_ip = ip[-15:]
|
||||
request.user.profile.save()
|
||||
|
||||
|
||||
return response
|
||||
|
||||
def __call__(self, request):
|
||||
|
@ -50,29 +59,31 @@ class LastSeenMiddleware(object):
|
|||
|
||||
return response
|
||||
|
||||
|
||||
class DBProfilerMiddleware:
|
||||
def __init__(self, get_response=None):
|
||||
self.get_response = get_response
|
||||
|
||||
def process_request(self, request):
|
||||
def process_request(self, request):
|
||||
setattr(request, 'activated_segments', [])
|
||||
if ((request.path.startswith('/reader/feed') or
|
||||
request.path.startswith('/reader/river')) and
|
||||
random.random() < .01):
|
||||
if (
|
||||
# request.path.startswith('/reader/feed') or
|
||||
request.path.startswith('/reader/feed/')
|
||||
) and random.random() < 0.01:
|
||||
request.activated_segments.append('db_profiler')
|
||||
connection.use_debug_cursor = True
|
||||
setattr(settings, 'ORIGINAL_DEBUG', settings.DEBUG)
|
||||
settings.DEBUG = True
|
||||
|
||||
def process_celery(self):
|
||||
setattr(self, 'activated_segments', [])
|
||||
if random.random() < .01:
|
||||
def process_celery(self):
|
||||
setattr(self, 'activated_segments', [])
|
||||
if random.random() < 0.01:
|
||||
self.activated_segments.append('db_profiler')
|
||||
connection.use_debug_cursor = True
|
||||
setattr(settings, 'ORIGINAL_DEBUG', settings.DEBUG)
|
||||
settings.DEBUG = True
|
||||
return self
|
||||
|
||||
|
||||
def process_exception(self, request, exception):
|
||||
if hasattr(request, 'sql_times_elapsed'):
|
||||
self._save_times(request.sql_times_elapsed)
|
||||
|
@ -84,25 +95,25 @@ class DBProfilerMiddleware:
|
|||
# logging.debug(" ---> ~FGProfiling~FB app: %s" % request.sql_times_elapsed)
|
||||
self._save_times(request.sql_times_elapsed)
|
||||
return response
|
||||
|
||||
|
||||
def process_celery_finished(self):
|
||||
middleware = SQLLogToConsoleMiddleware()
|
||||
middleware.process_celery(self)
|
||||
if hasattr(self, 'sql_times_elapsed'):
|
||||
logging.debug(" ---> ~FGProfiling~FB task: %s" % self.sql_times_elapsed)
|
||||
self._save_times(self.sql_times_elapsed, 'task_')
|
||||
|
||||
|
||||
def process_request_finished(self):
|
||||
middleware = SQLLogToConsoleMiddleware()
|
||||
middleware.process_celery(self)
|
||||
if hasattr(self, 'sql_times_elapsed'):
|
||||
logging.debug(" ---> ~FGProfiling~FB app: %s" % self.sql_times_elapsed)
|
||||
self._save_times(self.sql_times_elapsed, 'app_')
|
||||
|
||||
|
||||
def _save_times(self, db_times, prefix=""):
|
||||
if not db_times:
|
||||
if not db_times:
|
||||
return
|
||||
|
||||
|
||||
r = redis.Redis(connection_pool=settings.REDIS_STATISTICS_POOL)
|
||||
pipe = r.pipeline()
|
||||
minute = round_time(round_to=60)
|
||||
|
@ -126,17 +137,19 @@ class DBProfilerMiddleware:
|
|||
|
||||
return response
|
||||
|
||||
|
||||
class SQLLogToConsoleMiddleware:
|
||||
def __init__(self, get_response=None):
|
||||
self.get_response = get_response
|
||||
|
||||
def activated(self, request):
|
||||
return (settings.DEBUG_QUERIES or
|
||||
(hasattr(request, 'activated_segments') and
|
||||
'db_profiler' in request.activated_segments))
|
||||
return settings.DEBUG_QUERIES or (
|
||||
hasattr(request, 'activated_segments') and 'db_profiler' in request.activated_segments
|
||||
)
|
||||
|
||||
def process_response(self, request, response):
|
||||
if not self.activated(request): return response
|
||||
def process_response(self, request, response):
|
||||
if not self.activated(request):
|
||||
return response
|
||||
if connection.queries:
|
||||
time_elapsed = sum([float(q['time']) for q in connection.queries])
|
||||
queries = connection.queries
|
||||
|
@ -164,20 +177,37 @@ class SQLLogToConsoleMiddleware:
|
|||
query['sql'] = re.sub(r'INSERT', '~FGINSERT', query['sql'])
|
||||
query['sql'] = re.sub(r'UPDATE', '~FY~SBUPDATE', query['sql'])
|
||||
query['sql'] = re.sub(r'DELETE', '~FR~SBDELETE', query['sql'])
|
||||
if settings.DEBUG and settings.DEBUG_QUERIES and not getattr(settings, 'DEBUG_QUERIES_SUMMARY_ONLY', False):
|
||||
t = Template("{% for sql in sqllog %}{% if not forloop.first %} {% endif %}[{{forloop.counter}}] ~FC{{sql.time}}s~FW: {{sql.sql|safe}}{% if not forloop.last %}\n{% endif %}{% endfor %}")
|
||||
logging.debug(t.render(Context({
|
||||
'sqllog': queries,
|
||||
'count': len(queries),
|
||||
'time': time_elapsed,
|
||||
})))
|
||||
if (
|
||||
settings.DEBUG
|
||||
and settings.DEBUG_QUERIES
|
||||
and not getattr(settings, 'DEBUG_QUERIES_SUMMARY_ONLY', False)
|
||||
):
|
||||
t = Template(
|
||||
"{% for sql in sqllog %}{% if not forloop.first %} {% endif %}[{{forloop.counter}}] ~FC{{sql.time}}s~FW: {{sql.sql|safe}}{% if not forloop.last %}\n{% endif %}{% endfor %}"
|
||||
)
|
||||
logging.debug(
|
||||
t.render(
|
||||
Context(
|
||||
{
|
||||
'sqllog': queries,
|
||||
'count': len(queries),
|
||||
'time': time_elapsed,
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
times_elapsed = {
|
||||
'sql': sum([float(q['time'])
|
||||
for q in queries if not q.get('mongo') and
|
||||
not q.get('redis_user') and
|
||||
not q.get('redis_story') and
|
||||
not q.get('redis_session') and
|
||||
not q.get('redis_pubsub')]),
|
||||
'sql': sum(
|
||||
[
|
||||
float(q['time'])
|
||||
for q in queries
|
||||
if not q.get('mongo')
|
||||
and not q.get('redis_user')
|
||||
and not q.get('redis_story')
|
||||
and not q.get('redis_session')
|
||||
and not q.get('redis_pubsub')
|
||||
]
|
||||
),
|
||||
'mongo': sum([float(q['time']) for q in queries if q.get('mongo')]),
|
||||
'redis_user': sum([float(q['time']) for q in queries if q.get('redis_user')]),
|
||||
'redis_story': sum([float(q['time']) for q in queries if q.get('redis_story')]),
|
||||
|
@ -191,7 +221,7 @@ class SQLLogToConsoleMiddleware:
|
|||
settings.DEBUG = False
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def process_celery(self, profiler):
|
||||
self.process_response(profiler, None)
|
||||
if not getattr(settings, 'ORIGINAL_DEBUG', settings.DEBUG):
|
||||
|
@ -208,110 +238,247 @@ class SQLLogToConsoleMiddleware:
|
|||
|
||||
return response
|
||||
|
||||
|
||||
SIMPSONS_QUOTES = [
|
||||
("Homer", "D'oh."),
|
||||
("Ralph", "Me fail English? That's unpossible."),
|
||||
("Lionel Hutz", "This is the greatest case of false advertising I've seen since I sued the movie \"The Never Ending Story.\""),
|
||||
(
|
||||
"Lionel Hutz",
|
||||
"This is the greatest case of false advertising I've seen since I sued the movie \"The Never Ending Story.\"",
|
||||
),
|
||||
("Sideshow Bob", "No children have ever meddled with the Republican Party and lived to tell about it."),
|
||||
("Troy McClure", "Don't kid yourself, Jimmy. If a cow ever got the chance, he'd eat you and everyone you care about!"),
|
||||
(
|
||||
"Troy McClure",
|
||||
"Don't kid yourself, Jimmy. If a cow ever got the chance, he'd eat you and everyone you care about!",
|
||||
),
|
||||
("Comic Book Guy", "The Internet King? I wonder if he could provide faster nudity..."),
|
||||
("Homer", "Oh, so they have Internet on computers now!"),
|
||||
("Ned Flanders", "I've done everything the Bible says - even the stuff that contradicts the other stuff!"),
|
||||
("Comic Book Guy", "Your questions have become more redundant and annoying than the last three \"Highlander\" movies."),
|
||||
(
|
||||
"Ned Flanders",
|
||||
"I've done everything the Bible says - even the stuff that contradicts the other stuff!",
|
||||
),
|
||||
(
|
||||
"Comic Book Guy",
|
||||
"Your questions have become more redundant and annoying than the last three \"Highlander\" movies.",
|
||||
),
|
||||
("Chief Wiggum", "Uh, no, you got the wrong number. This is 9-1...2."),
|
||||
("Sideshow Bob", "I'll be back. You can't keep the Democrats out of the White House forever, and when they get in, I'm back on the streets, with all my criminal buddies."),
|
||||
("Homer", "When I held that gun in my hand, I felt a surge of power...like God must feel when he's holding a gun."),
|
||||
("Nelson", "Dad didn't leave... When he comes back from the store, he's going to wave those pop-tarts right in your face!"),
|
||||
("Milhouse", "Remember the time he ate my goldfish? And you lied and said I never had goldfish. Then why did I have the bowl, Bart? *Why did I have the bowl?*"),
|
||||
("Lionel Hutz", "Well, he's kind of had it in for me ever since I accidentally ran over his dog. Actually, replace \"accidentally\" with \"repeatedly\" and replace \"dog\" with \"son.\""),
|
||||
("Comic Book Guy", "Last night's \"Itchy and Scratchy Show\" was, without a doubt, the worst episode *ever.* Rest assured, I was on the Internet within minutes, registering my disgust throughout the world."),
|
||||
(
|
||||
"Sideshow Bob",
|
||||
"I'll be back. You can't keep the Democrats out of the White House forever, and when they get in, I'm back on the streets, with all my criminal buddies.",
|
||||
),
|
||||
(
|
||||
"Homer",
|
||||
"When I held that gun in my hand, I felt a surge of power...like God must feel when he's holding a gun.",
|
||||
),
|
||||
(
|
||||
"Nelson",
|
||||
"Dad didn't leave... When he comes back from the store, he's going to wave those pop-tarts right in your face!",
|
||||
),
|
||||
(
|
||||
"Milhouse",
|
||||
"Remember the time he ate my goldfish? And you lied and said I never had goldfish. Then why did I have the bowl, Bart? *Why did I have the bowl?*",
|
||||
),
|
||||
(
|
||||
"Lionel Hutz",
|
||||
"Well, he's kind of had it in for me ever since I accidentally ran over his dog. Actually, replace \"accidentally\" with \"repeatedly\" and replace \"dog\" with \"son.\"",
|
||||
),
|
||||
(
|
||||
"Comic Book Guy",
|
||||
"Last night's \"Itchy and Scratchy Show\" was, without a doubt, the worst episode *ever.* Rest assured, I was on the Internet within minutes, registering my disgust throughout the world.",
|
||||
),
|
||||
("Homer", "I'm normally not a praying man, but if you're up there, please save me, Superman."),
|
||||
("Homer", "Save me, Jeebus."),
|
||||
("Mayor Quimby", "I stand by my racial slur."),
|
||||
("Comic Book Guy", "Oh, loneliness and cheeseburgers are a dangerous mix."),
|
||||
("Homer", "You don't like your job, you don't strike. You go in every day and do it really half-assed. That's the American way."),
|
||||
("Chief Wiggum", "Fat Tony is a cancer on this fair city! He is the cancer and I am the...uh...what cures cancer?"),
|
||||
("Homer", "Bart, with $10,000 we'd be millionaires! We could buy all kinds of useful things like...love!"),
|
||||
(
|
||||
"Homer",
|
||||
"You don't like your job, you don't strike. You go in every day and do it really half-assed. That's the American way.",
|
||||
),
|
||||
(
|
||||
"Chief Wiggum",
|
||||
"Fat Tony is a cancer on this fair city! He is the cancer and I am the...uh...what cures cancer?",
|
||||
),
|
||||
(
|
||||
"Homer",
|
||||
"Bart, with $10,000 we'd be millionaires! We could buy all kinds of useful things like...love!",
|
||||
),
|
||||
("Homer", "Fame was like a drug. But what was even more like a drug were the drugs."),
|
||||
("Homer", "Books are useless! I only ever read one book, \"To Kill A Mockingbird,\" and it gave me absolutely no insight on how to kill mockingbirds! Sure it taught me not to judge a man by the color of his skin...but what good does *that* do me?"),
|
||||
("Chief Wiggum", "Can't you people take the law into your own hands? I mean, we can't be policing the entire city!"),
|
||||
("Homer", "Weaseling out of things is important to learn. It's what separates us from the animals...except the weasel."),
|
||||
("Reverend Lovejoy", "Marge, just about everything's a sin. [holds up a Bible] Y'ever sat down and read this thing? Technically we're not supposed to go to the bathroom."),
|
||||
("Homer", "You know, the one with all the well meaning rules that don't work out in real life, uh, Christianity."),
|
||||
(
|
||||
"Homer",
|
||||
"Books are useless! I only ever read one book, \"To Kill A Mockingbird,\" and it gave me absolutely no insight on how to kill mockingbirds! Sure it taught me not to judge a man by the color of his skin...but what good does *that* do me?",
|
||||
),
|
||||
(
|
||||
"Chief Wiggum",
|
||||
"Can't you people take the law into your own hands? I mean, we can't be policing the entire city!",
|
||||
),
|
||||
(
|
||||
"Homer",
|
||||
"Weaseling out of things is important to learn. It's what separates us from the animals...except the weasel.",
|
||||
),
|
||||
(
|
||||
"Reverend Lovejoy",
|
||||
"Marge, just about everything's a sin. [holds up a Bible] Y'ever sat down and read this thing? Technically we're not supposed to go to the bathroom.",
|
||||
),
|
||||
(
|
||||
"Homer",
|
||||
"You know, the one with all the well meaning rules that don't work out in real life, uh, Christianity.",
|
||||
),
|
||||
("Smithers", "Uh, no, they're saying \"Boo-urns, Boo-urns.\""),
|
||||
("Hans Moleman", "I was saying \"Boo-urns.\""),
|
||||
("Homer", "Kids, you tried your best and you failed miserably. The lesson is, never try."),
|
||||
("Homer", "Here's to alcohol, the cause of - and solution to - all life's problems."),
|
||||
("Homer", "When will I learn? The answers to life's problems aren't at the bottom of a bottle, they're on TV!"),
|
||||
(
|
||||
"Homer",
|
||||
"When will I learn? The answers to life's problems aren't at the bottom of a bottle, they're on TV!",
|
||||
),
|
||||
("Chief Wiggum", "I hope this has taught you kids a lesson: kids never learn."),
|
||||
("Homer", "How is education supposed to make me feel smarter? Besides, every time I learn something new, it pushes some old stuff out of my brain. Remember when I took that home winemaking course, and I forgot how to drive?"),
|
||||
(
|
||||
"Homer",
|
||||
"How is education supposed to make me feel smarter? Besides, every time I learn something new, it pushes some old stuff out of my brain. Remember when I took that home winemaking course, and I forgot how to drive?",
|
||||
),
|
||||
("Homer", "Homer no function beer well without."),
|
||||
("Duffman", "Duffman can't breathe! OH NO!"),
|
||||
("Grandpa Simpson", "Dear Mr. President, There are too many states nowadays. Please, eliminate three. P.S. I am not a crackpot."),
|
||||
("Homer", "Old people don't need companionship. They need to be isolated and studied so it can be determined what nutrients they have that might be extracted for our personal use."),
|
||||
("Troy McClure", "Hi. I'm Troy McClure. You may remember me from such self-help tapes as \"Smoke Yourself Thin\" and \"Get Some Confidence, Stupid!\""),
|
||||
(
|
||||
"Grandpa Simpson",
|
||||
"Dear Mr. President, There are too many states nowadays. Please, eliminate three. P.S. I am not a crackpot.",
|
||||
),
|
||||
(
|
||||
"Homer",
|
||||
"Old people don't need companionship. They need to be isolated and studied so it can be determined what nutrients they have that might be extracted for our personal use.",
|
||||
),
|
||||
(
|
||||
"Troy McClure",
|
||||
"Hi. I'm Troy McClure. You may remember me from such self-help tapes as \"Smoke Yourself Thin\" and \"Get Some Confidence, Stupid!\"",
|
||||
),
|
||||
("Homer", "A woman is a lot like a refrigerator. Six feet tall, 300 pounds...it makes ice."),
|
||||
("Homer", "Son, a woman is like a beer. They smell good, they look good, you'd step over your own mother just to get one! But you can't stop at one. You wanna drink another woman!"),
|
||||
(
|
||||
"Homer",
|
||||
"Son, a woman is like a beer. They smell good, they look good, you'd step over your own mother just to get one! But you can't stop at one. You wanna drink another woman!",
|
||||
),
|
||||
("Homer", "Facts are meaningless. You could use facts to prove anything that's even remotely true!"),
|
||||
("Mr Burns", "I'll keep it short and sweet - Family. Religion. Friendship. These are the three demons you must slay if you wish to succeed in business."),
|
||||
("Kent Brockman", "...And the fluffy kitten played with that ball of string all through the night. On a lighter note, a Kwik-E-Mart clerk was brutally murdered last night."),
|
||||
("Ralph", "Mrs. Krabappel and Principal Skinner were in the closet making babies and I saw one of the babies and then the baby looked at me."),
|
||||
(
|
||||
"Mr Burns",
|
||||
"I'll keep it short and sweet - Family. Religion. Friendship. These are the three demons you must slay if you wish to succeed in business.",
|
||||
),
|
||||
(
|
||||
"Kent Brockman",
|
||||
"...And the fluffy kitten played with that ball of string all through the night. On a lighter note, a Kwik-E-Mart clerk was brutally murdered last night.",
|
||||
),
|
||||
(
|
||||
"Ralph",
|
||||
"Mrs. Krabappel and Principal Skinner were in the closet making babies and I saw one of the babies and then the baby looked at me.",
|
||||
),
|
||||
("Apu", "Please do not offer my god a peanut."),
|
||||
("Homer", "You don't win friends with salad."),
|
||||
("Mr Burns", "I don't like being outdoors, Smithers. For one thing, there's too many fat children."),
|
||||
("Sideshow Bob", "Attempted murder? Now honestly, what is that? Do they give a Nobel Prize for attempted chemistry?"),
|
||||
(
|
||||
"Sideshow Bob",
|
||||
"Attempted murder? Now honestly, what is that? Do they give a Nobel Prize for attempted chemistry?",
|
||||
),
|
||||
("Chief Wiggum", "They only come out in the night. Or in this case, the day."),
|
||||
("Mr Burns", "Whoa, slow down there, maestro. There's a *New* Mexico?"),
|
||||
("Homer", "He didn't give you gay, did he? Did he?!"),
|
||||
("Comic Book Guy", "But, Aquaman, you cannot marry a woman without gills. You're from two different worlds... Oh, I've wasted my life."),
|
||||
(
|
||||
"Comic Book Guy",
|
||||
"But, Aquaman, you cannot marry a woman without gills. You're from two different worlds... Oh, I've wasted my life.",
|
||||
),
|
||||
("Homer", "Marge, it takes two to lie. One to lie and one to listen."),
|
||||
("Superintendent Chalmers", "I've had it with this school, Skinner. Low test scores, class after class of ugly, ugly children..."),
|
||||
(
|
||||
"Superintendent Chalmers",
|
||||
"I've had it with this school, Skinner. Low test scores, class after class of ugly, ugly children...",
|
||||
),
|
||||
("Mr Burns", "What good is money if it can't inspire terror in your fellow man?"),
|
||||
("Homer", "Oh, everything looks bad if you remember it."),
|
||||
("Ralph", "Slow down, Bart! My legs don't know how to be as long as yours."),
|
||||
("Homer", "Donuts. Is there anything they can't do?"),
|
||||
("Frink", "Brace yourselves gentlemen. According to the gas chromatograph, the secret ingredient is... Love!? Who's been screwing with this thing?"),
|
||||
("Apu", "Yes! I am a citizen! Now which way to the welfare office? I'm kidding, I'm kidding. I work, I work."),
|
||||
(
|
||||
"Frink",
|
||||
"Brace yourselves gentlemen. According to the gas chromatograph, the secret ingredient is... Love!? Who's been screwing with this thing?",
|
||||
),
|
||||
(
|
||||
"Apu",
|
||||
"Yes! I am a citizen! Now which way to the welfare office? I'm kidding, I'm kidding. I work, I work.",
|
||||
),
|
||||
("Milhouse", "We started out like Romeo and Juliet, but it ended up in tragedy."),
|
||||
("Mr Burns", "A lifetime of working with nuclear power has left me with a healthy green glow...and left me as impotent as a Nevada boxing commissioner."),
|
||||
(
|
||||
"Mr Burns",
|
||||
"A lifetime of working with nuclear power has left me with a healthy green glow...and left me as impotent as a Nevada boxing commissioner.",
|
||||
),
|
||||
("Homer", "Kids, kids. I'm not going to die. That only happens to bad people."),
|
||||
("Milhouse", "Look out, Itchy! He's Irish!"),
|
||||
("Homer", "I'm going to the back seat of my car, with the woman I love, and I won't be back for ten minutes!"),
|
||||
(
|
||||
"Homer",
|
||||
"I'm going to the back seat of my car, with the woman I love, and I won't be back for ten minutes!",
|
||||
),
|
||||
("Smithers", "I'm allergic to bee stings. They cause me to, uh, die."),
|
||||
("Barney", "Aaah! Natural light! Get it off me! Get it off me!"),
|
||||
("Principal Skinner", "That's why I love elementary school, Edna. The children believe anything you tell them."),
|
||||
("Sideshow Bob", "Your guilty consciences may make you vote Democratic, but secretly you all yearn for a Republican president to lower taxes, brutalize criminals, and rule you like a king!"),
|
||||
(
|
||||
"Principal Skinner",
|
||||
"That's why I love elementary school, Edna. The children believe anything you tell them.",
|
||||
),
|
||||
(
|
||||
"Sideshow Bob",
|
||||
"Your guilty consciences may make you vote Democratic, but secretly you all yearn for a Republican president to lower taxes, brutalize criminals, and rule you like a king!",
|
||||
),
|
||||
("Barney", "Jesus must be spinning in his grave!"),
|
||||
("Superintendent Chalmers", "\"Thank the Lord\"? That sounded like a prayer. A prayer in a public school. God has no place within these walls, just like facts don't have a place within an organized religion."),
|
||||
(
|
||||
"Superintendent Chalmers",
|
||||
"\"Thank the Lord\"? That sounded like a prayer. A prayer in a public school. God has no place within these walls, just like facts don't have a place within an organized religion.",
|
||||
),
|
||||
("Mr Burns", "[answering the phone] Ahoy hoy?"),
|
||||
("Comic Book Guy", "Oh, a *sarcasm* detector. Oh, that's a *really* useful invention!"),
|
||||
("Marge", "Our differences are only skin deep, but our sames go down to the bone."),
|
||||
("Homer", "What's the point of going out? We're just going to wind up back here anyway."),
|
||||
("Marge", "Get ready, skanks! It's time for the truth train!"),
|
||||
("Bill Gates", "I didn't get rich by signing checks."),
|
||||
("Principal Skinner", "Fire can be our friend; whether it's toasting marshmallows or raining down on Charlie."),
|
||||
("Homer", "Oh, I'm in no condition to drive. Wait a minute. I don't have to listen to myself. I'm drunk."),
|
||||
(
|
||||
"Principal Skinner",
|
||||
"Fire can be our friend; whether it's toasting marshmallows or raining down on Charlie.",
|
||||
),
|
||||
(
|
||||
"Homer",
|
||||
"Oh, I'm in no condition to drive. Wait a minute. I don't have to listen to myself. I'm drunk.",
|
||||
),
|
||||
("Homer", "And here I am using my own lungs like a sucker."),
|
||||
("Comic Book Guy", "Human contact: the final frontier."),
|
||||
("Homer", "I hope I didn't brain my damage."),
|
||||
("Krusty the Clown", "And now, in the spirit of the season: start shopping. And for every dollar of Krusty merchandise you buy, I will be nice to a sick kid. For legal purposes, sick kids may include hookers with a cold."),
|
||||
(
|
||||
"Krusty the Clown",
|
||||
"And now, in the spirit of the season: start shopping. And for every dollar of Krusty merchandise you buy, I will be nice to a sick kid. For legal purposes, sick kids may include hookers with a cold.",
|
||||
),
|
||||
("Homer", "I'm a Spalding Gray in a Rick Dees world."),
|
||||
("Dr Nick", "Inflammable means flammable? What a country."),
|
||||
("Homer", "Beer. Now there's a temporary solution."),
|
||||
("Comic Book Guy", "Stan Lee never left. I'm afraid his mind is no longer in mint condition."),
|
||||
("Nelson", "Shoplifting is a victimless crime. Like punching someone in the dark."),
|
||||
("Krusty the Clown", "Kids, we need to talk for a moment about Krusty Brand Chew Goo Gum Like Substance. We all knew it contained spider eggs, but the hantavirus? That came out of left field. So if you're experiencing numbness and/or comas, send five dollars to antidote, PO box..."),
|
||||
(
|
||||
"Krusty the Clown",
|
||||
"Kids, we need to talk for a moment about Krusty Brand Chew Goo Gum Like Substance. We all knew it contained spider eggs, but the hantavirus? That came out of left field. So if you're experiencing numbness and/or comas, send five dollars to antidote, PO box...",
|
||||
),
|
||||
("Milhouse", "I can't go to juvie. They use guys like me as currency."),
|
||||
("Homer", "Son, when you participate in sporting events, it's not whether you win or lose: it's how drunk you get."),
|
||||
(
|
||||
"Homer",
|
||||
"Son, when you participate in sporting events, it's not whether you win or lose: it's how drunk you get.",
|
||||
),
|
||||
("Homer", "I like my beer cold, my TV loud and my homosexuals flaming."),
|
||||
("Apu", "Thank you, steal again."),
|
||||
("Homer", "Marge, you being a cop makes you the man! Which makes me the woman - and I have no interest in that, besides occasionally wearing the underwear, which as we discussed, is strictly a comfort thing."),
|
||||
("Ed Begley Jr", "I prefer a vehicle that doesn't hurt Mother Earth. It's a go-cart, powered by my own sense of self-satisfaction."),
|
||||
(
|
||||
"Homer",
|
||||
"Marge, you being a cop makes you the man! Which makes me the woman - and I have no interest in that, besides occasionally wearing the underwear, which as we discussed, is strictly a comfort thing.",
|
||||
),
|
||||
(
|
||||
"Ed Begley Jr",
|
||||
"I prefer a vehicle that doesn't hurt Mother Earth. It's a go-cart, powered by my own sense of self-satisfaction.",
|
||||
),
|
||||
("Bart", "I didn't think it was physically possible, but this both sucks *and* blows."),
|
||||
("Homer", "How could you?! Haven't you learned anything from that guy who gives those sermons at church? Captain Whatshisname? We live in a society of laws! Why do you think I took you to all those Police Academy movies? For fun? Well, I didn't hear anybody laughing, did you? Except at that guy who made sound effects. Makes sound effects and laughs. Where was I? Oh yeah! Stay out of my booze."),
|
||||
(
|
||||
"Homer",
|
||||
"How could you?! Haven't you learned anything from that guy who gives those sermons at church? Captain Whatshisname? We live in a society of laws! Why do you think I took you to all those Police Academy movies? For fun? Well, I didn't hear anybody laughing, did you? Except at that guy who made sound effects. Makes sound effects and laughs. Where was I? Oh yeah! Stay out of my booze.",
|
||||
),
|
||||
("Homer", "Lisa, vampires are make-believe, like elves, gremlins, and Eskimos."),
|
||||
]
|
||||
|
||||
|
||||
class SimpsonsMiddleware:
|
||||
def __init__(self, get_response=None):
|
||||
self.get_response = get_response
|
||||
|
@ -333,9 +500,9 @@ class SimpsonsMiddleware:
|
|||
response = self.process_response(request, response)
|
||||
|
||||
return response
|
||||
|
||||
class ServerHostnameMiddleware:
|
||||
|
||||
|
||||
class ServerHostnameMiddleware:
|
||||
def __init__(self, get_response=None):
|
||||
self.get_response = get_response
|
||||
|
||||
|
@ -355,8 +522,8 @@ class ServerHostnameMiddleware:
|
|||
|
||||
return response
|
||||
|
||||
class TimingMiddleware:
|
||||
|
||||
class TimingMiddleware:
|
||||
def __init__(self, get_response=None):
|
||||
self.get_response = get_response
|
||||
|
||||
|
@ -369,13 +536,15 @@ class TimingMiddleware:
|
|||
response = self.get_response(request)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
BANNED_USER_AGENTS = (
|
||||
'feed reader-background',
|
||||
'missing',
|
||||
)
|
||||
|
||||
BANNED_USERNAMES = (
|
||||
)
|
||||
BANNED_USERNAMES = ()
|
||||
|
||||
|
||||
class UserAgentBanMiddleware:
|
||||
def __init__(self, get_response=None):
|
||||
|
@ -383,31 +552,38 @@ class UserAgentBanMiddleware:
|
|||
|
||||
def process_request(self, request):
|
||||
user_agent = request.environ.get('HTTP_USER_AGENT', 'missing').lower()
|
||||
|
||||
if 'profile' in request.path: return
|
||||
if 'haproxy' in request.path: return
|
||||
if 'dbcheck' in request.path: return
|
||||
if 'account' in request.path: return
|
||||
if 'push' in request.path: return
|
||||
if getattr(settings, 'TEST_DEBUG'): return
|
||||
|
||||
if 'profile' in request.path:
|
||||
return
|
||||
if 'haproxy' in request.path:
|
||||
return
|
||||
if 'dbcheck' in request.path:
|
||||
return
|
||||
if 'account' in request.path:
|
||||
return
|
||||
if 'push' in request.path:
|
||||
return
|
||||
if getattr(settings, 'TEST_DEBUG'):
|
||||
return
|
||||
if any(ua in user_agent for ua in BANNED_USER_AGENTS):
|
||||
data = {
|
||||
'error': 'User agent banned: %s' % user_agent,
|
||||
'code': -1
|
||||
}
|
||||
logging.user(request, "~FB~SN~BBBanned UA: ~SB%s / %s (%s)" % (user_agent, request.path, request.META))
|
||||
|
||||
data = {'error': 'User agent banned: %s' % user_agent, 'code': -1}
|
||||
logging.user(
|
||||
request, "~FB~SN~BBBanned UA: ~SB%s / %s (%s)" % (user_agent, request.path, request.META)
|
||||
)
|
||||
|
||||
return HttpResponse(json.encode(data), status=403, content_type='text/json')
|
||||
|
||||
if request.user.is_authenticated and any(username == request.user.username for username in BANNED_USERNAMES):
|
||||
data = {
|
||||
'error': 'User banned: %s' % request.user.username,
|
||||
'code': -1
|
||||
}
|
||||
logging.user(request, "~FB~SN~BBBanned Username: ~SB%s / %s (%s)" % (request.user, request.path, request.META))
|
||||
|
||||
if request.user.is_authenticated and any(
|
||||
username == request.user.username for username in BANNED_USERNAMES
|
||||
):
|
||||
data = {'error': 'User banned: %s' % request.user.username, 'code': -1}
|
||||
logging.user(
|
||||
request,
|
||||
"~FB~SN~BBBanned Username: ~SB%s / %s (%s)" % (request.user, request.path, request.META),
|
||||
)
|
||||
|
||||
return HttpResponse(json.encode(data), status=403, content_type='text/json')
|
||||
|
||||
|
||||
def __call__(self, request):
|
||||
response = None
|
||||
if hasattr(self, 'process_request'):
|
||||
|
@ -418,4 +594,3 @@ class UserAgentBanMiddleware:
|
|||
response = self.process_response(request, response)
|
||||
|
||||
return response
|
||||
|
||||
|
|
|
@ -89,8 +89,8 @@ class Profile(models.Model):
|
|||
self.secret_token = generate_secret_token(self.user.username, 12)
|
||||
try:
|
||||
super(Profile, self).save(*args, **kwargs)
|
||||
except DatabaseError:
|
||||
print(" ---> Profile not saved. Table isn't there yet.")
|
||||
except DatabaseError as e:
|
||||
print(f" ---> Profile not saved: {e}")
|
||||
|
||||
def delete_user(self, confirm=False, fast=False):
|
||||
if not confirm:
|
||||
|
|
|
@ -25,7 +25,6 @@ from django.contrib.auth import logout as logout_user
|
|||
from django.contrib.auth.models import User
|
||||
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404, UnreadablePostError
|
||||
from django.conf import settings
|
||||
from django.core.mail import mail_admins
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.core.validators import validate_email
|
||||
from django.contrib.sites.models import Site
|
||||
|
@ -76,7 +75,7 @@ BANNED_URLS = [
|
|||
ALLOWED_SUBDOMAINS = [
|
||||
'dev',
|
||||
'www',
|
||||
'beta',
|
||||
# 'beta', # Comment to redirect beta -> www, uncomment to allow beta -> staging (+ dns changes)
|
||||
'staging',
|
||||
'discovery',
|
||||
'debug',
|
||||
|
@ -1974,42 +1973,57 @@ def mark_story_as_unread(request):
|
|||
@required_params('story_hash')
|
||||
def mark_story_hash_as_unread(request):
|
||||
r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL)
|
||||
story_hash = request.POST.get('story_hash')
|
||||
feed_id, _ = MStory.split_story_hash(story_hash)
|
||||
story, _ = MStory.find_story(feed_id, story_hash)
|
||||
if not story:
|
||||
data = dict(code=-1, message="That story has been removed from the feed, no need to mark it unread.")
|
||||
return data
|
||||
message = RUserStory.story_can_be_marked_read_by_user(story, request.user)
|
||||
if message:
|
||||
data = dict(code=-1, message=message)
|
||||
return data
|
||||
|
||||
# Also count on original subscription
|
||||
usersubs = UserSubscription.objects.filter(user=request.user.pk, feed=feed_id)
|
||||
if usersubs:
|
||||
usersub = usersubs[0]
|
||||
if not usersub.needs_unread_recalc:
|
||||
usersub.needs_unread_recalc = True
|
||||
usersub.save(update_fields=['needs_unread_recalc'])
|
||||
data = usersub.invert_read_stories_after_unread_story(story, request)
|
||||
r.publish(request.user.username, 'feed:%s' % feed_id)
|
||||
story_hashes = request.POST.getlist('story_hash') or request.POST.getlist('story_hash[]')
|
||||
is_list = len(story_hashes) > 1
|
||||
datas = []
|
||||
for story_hash in story_hashes:
|
||||
feed_id, _ = MStory.split_story_hash(story_hash)
|
||||
story, _ = MStory.find_story(feed_id, story_hash)
|
||||
if not story:
|
||||
data = dict(code=-1, message="That story has been removed from the feed, no need to mark it unread.", story_hash=story_hash)
|
||||
if not is_list:
|
||||
return data
|
||||
else:
|
||||
datas.append(data)
|
||||
message = RUserStory.story_can_be_marked_read_by_user(story, request.user)
|
||||
if message:
|
||||
data = dict(code=-1, message=message, story_hash=story_hash)
|
||||
if not is_list:
|
||||
return data
|
||||
else:
|
||||
datas.append(data)
|
||||
|
||||
# Also count on original subscription
|
||||
usersubs = UserSubscription.objects.filter(user=request.user.pk, feed=feed_id)
|
||||
if usersubs:
|
||||
usersub = usersubs[0]
|
||||
if not usersub.needs_unread_recalc:
|
||||
usersub.needs_unread_recalc = True
|
||||
usersub.save(update_fields=['needs_unread_recalc'])
|
||||
data = usersub.invert_read_stories_after_unread_story(story, request)
|
||||
r.publish(request.user.username, 'feed:%s' % feed_id)
|
||||
|
||||
feed_id, friend_ids = RUserStory.mark_story_hash_unread(request.user, story_hash)
|
||||
feed_id, friend_ids = RUserStory.mark_story_hash_unread(request.user, story_hash)
|
||||
|
||||
if friend_ids:
|
||||
socialsubs = MSocialSubscription.objects.filter(
|
||||
user_id=request.user.pk,
|
||||
subscription_user_id__in=friend_ids)
|
||||
for socialsub in socialsubs:
|
||||
if not socialsub.needs_unread_recalc:
|
||||
socialsub.needs_unread_recalc = True
|
||||
socialsub.save()
|
||||
r.publish(request.user.username, 'social:%s' % socialsub.subscription_user_id)
|
||||
if friend_ids:
|
||||
socialsubs = MSocialSubscription.objects.filter(
|
||||
user_id=request.user.pk,
|
||||
subscription_user_id__in=friend_ids)
|
||||
for socialsub in socialsubs:
|
||||
if not socialsub.needs_unread_recalc:
|
||||
socialsub.needs_unread_recalc = True
|
||||
socialsub.save()
|
||||
r.publish(request.user.username, 'social:%s' % socialsub.subscription_user_id)
|
||||
|
||||
logging.user(request, "~FYUnread story in feed/socialsubs: %s/%s" % (feed_id, friend_ids))
|
||||
logging.user(request, "~FYUnread story in feed/socialsubs: %s/%s" % (feed_id, friend_ids))
|
||||
|
||||
return dict(code=1, story_hash=story_hash, feed_id=feed_id, friend_user_ids=friend_ids)
|
||||
data = dict(code=1, story_hash=story_hash, feed_id=feed_id, friend_user_ids=friend_ids)
|
||||
if not is_list:
|
||||
return data
|
||||
else:
|
||||
datas.append(data)
|
||||
|
||||
return datas
|
||||
|
||||
@ajax_login_required
|
||||
@json.json_view
|
||||
|
@ -2494,9 +2508,7 @@ def activate_premium_account(request):
|
|||
sub.feed.count_subscribers()
|
||||
sub.feed.schedule_feed_fetch_immediately()
|
||||
except Exception as e:
|
||||
subject = "Premium activation failed"
|
||||
message = "%s -- %s\n\n%s" % (request.user, usersubs, e)
|
||||
mail_admins(subject, message, fail_silently=True)
|
||||
logging.user(request, "~BR~FWPremium activation failed: {e} {usersubs}")
|
||||
|
||||
request.user.profile.is_premium = True
|
||||
request.user.profile.save()
|
||||
|
@ -2535,97 +2547,113 @@ def _mark_story_as_starred(request):
|
|||
code = 1
|
||||
feed_id = int(request.POST.get('feed_id', 0))
|
||||
story_id = request.POST.get('story_id', None)
|
||||
story_hash = request.POST.get('story_hash', None)
|
||||
user_tags = request.POST.getlist('user_tags') or request.POST.getlist('user_tags[]')
|
||||
user_notes = request.POST.get('user_notes', None)
|
||||
highlights = request.POST.getlist('highlights') or request.POST.getlist('highlights[]') or []
|
||||
message = ""
|
||||
if story_hash:
|
||||
story, _ = MStory.find_story(story_hash=story_hash)
|
||||
feed_id = story and story.story_feed_id
|
||||
else:
|
||||
story_hashes = request.POST.getlist('story_hash') or request.POST.getlist('story_hash[]')
|
||||
is_list = len(story_hashes) > 1
|
||||
datas = []
|
||||
if not len(story_hashes):
|
||||
story, _ = MStory.find_story(story_feed_id=feed_id, story_id=story_id)
|
||||
if story:
|
||||
story_hashes = [story.story_hash]
|
||||
|
||||
if not story:
|
||||
if not len(story_hashes):
|
||||
return {'code': -1, 'message': "Could not find story to save."}
|
||||
|
||||
story_db = dict([(k, v) for k, v in list(story._data.items())
|
||||
if k is not None and v is not None])
|
||||
# Pop all existing user-specific fields because we don't want to reuse them from the found story
|
||||
# in case MStory.find_story uses somebody else's saved/shared story (because the original is deleted)
|
||||
story_db.pop('user_id', None)
|
||||
story_db.pop('starred_date', None)
|
||||
story_db.pop('id', None)
|
||||
story_db.pop('user_tags', None)
|
||||
story_db.pop('highlights', None)
|
||||
story_db.pop('user_notes', None)
|
||||
|
||||
now = datetime.datetime.now()
|
||||
story_values = dict(starred_date=now, user_tags=user_tags, highlights=highlights, user_notes=user_notes, **story_db)
|
||||
params = dict(story_guid=story.story_guid, user_id=request.user.pk)
|
||||
starred_story = MStarredStory.objects(**params).limit(1)
|
||||
created = False
|
||||
changed_user_notes = False
|
||||
removed_user_tags = []
|
||||
removed_highlights = []
|
||||
if not starred_story:
|
||||
params.update(story_values)
|
||||
if 'story_latest_content_z' in params:
|
||||
params.pop('story_latest_content_z')
|
||||
try:
|
||||
starred_story = MStarredStory.objects.create(**params)
|
||||
except OperationError as e:
|
||||
logging.user(request, "~FCStarring ~FRfailed~FC: ~SB%s (~FM~SB%s~FC~SN)" % (story.story_title[:32], e))
|
||||
return {'code': -1, 'message': "Could not save story due to: %s" % e}
|
||||
for story_hash in story_hashes:
|
||||
story, _ = MStory.find_story(story_hash=story_hash)
|
||||
if not story:
|
||||
logging.user(request, "~FCStarring ~FRfailed~FC: %s not found" % (story_hash))
|
||||
datas.append({'code': -1, 'message': "Could not save story, not found", 'story_hash': story_hash})
|
||||
continue
|
||||
|
||||
feed_id = story and story.story_feed_id
|
||||
|
||||
story_db = dict([(k, v) for k, v in list(story._data.items())
|
||||
if k is not None and v is not None])
|
||||
# Pop all existing user-specific fields because we don't want to reuse them from the found story
|
||||
# in case MStory.find_story uses somebody else's saved/shared story (because the original is deleted)
|
||||
story_db.pop('user_id', None)
|
||||
story_db.pop('starred_date', None)
|
||||
story_db.pop('id', None)
|
||||
story_db.pop('user_tags', None)
|
||||
story_db.pop('highlights', None)
|
||||
story_db.pop('user_notes', None)
|
||||
|
||||
now = datetime.datetime.now()
|
||||
story_values = dict(starred_date=now, user_tags=user_tags, highlights=highlights, user_notes=user_notes, **story_db)
|
||||
params = dict(story_guid=story.story_guid, user_id=request.user.pk)
|
||||
starred_story = MStarredStory.objects(**params).limit(1)
|
||||
created = False
|
||||
changed_user_notes = False
|
||||
removed_user_tags = []
|
||||
removed_highlights = []
|
||||
if not starred_story:
|
||||
params.update(story_values)
|
||||
if 'story_latest_content_z' in params:
|
||||
params.pop('story_latest_content_z')
|
||||
try:
|
||||
starred_story = MStarredStory.objects.create(**params)
|
||||
except OperationError as e:
|
||||
logging.user(request, "~FCStarring ~FRfailed~FC: ~SB%s (~FM~SB%s~FC~SN)" % (story.story_title[:32], e))
|
||||
datas.append({'code': -1, 'message': "Could not save story due to: %s" % e, 'story_hash': story_hash})
|
||||
|
||||
created = True
|
||||
MActivity.new_starred_story(user_id=request.user.pk,
|
||||
story_title=story.story_title,
|
||||
story_feed_id=feed_id,
|
||||
story_id=starred_story.story_guid)
|
||||
new_user_tags = user_tags
|
||||
new_highlights = highlights
|
||||
changed_user_notes = bool(user_notes)
|
||||
MStarredStoryCounts.adjust_count(request.user.pk, feed_id=feed_id, amount=1)
|
||||
else:
|
||||
starred_story = starred_story[0]
|
||||
new_user_tags = list(set(user_tags) - set(starred_story.user_tags or []))
|
||||
removed_user_tags = list(set(starred_story.user_tags or []) - set(user_tags))
|
||||
new_highlights = list(set(highlights) - set(starred_story.highlights or []))
|
||||
removed_highlights = list(set(starred_story.highlights or []) - set(highlights))
|
||||
changed_user_notes = bool(user_notes != starred_story.user_notes)
|
||||
starred_story.user_tags = user_tags
|
||||
starred_story.highlights = highlights
|
||||
starred_story.user_notes = user_notes
|
||||
starred_story.save()
|
||||
|
||||
if len(highlights) == 1 and len(new_highlights) == 1:
|
||||
MStarredStoryCounts.adjust_count(request.user.pk, highlights=True, amount=1)
|
||||
elif len(highlights) == 0 and len(removed_highlights):
|
||||
MStarredStoryCounts.adjust_count(request.user.pk, highlights=True, amount=-1)
|
||||
|
||||
created = True
|
||||
MActivity.new_starred_story(user_id=request.user.pk,
|
||||
story_title=story.story_title,
|
||||
story_feed_id=feed_id,
|
||||
story_id=starred_story.story_guid)
|
||||
new_user_tags = user_tags
|
||||
new_highlights = highlights
|
||||
changed_user_notes = bool(user_notes)
|
||||
MStarredStoryCounts.adjust_count(request.user.pk, feed_id=feed_id, amount=1)
|
||||
else:
|
||||
starred_story = starred_story[0]
|
||||
new_user_tags = list(set(user_tags) - set(starred_story.user_tags or []))
|
||||
removed_user_tags = list(set(starred_story.user_tags or []) - set(user_tags))
|
||||
new_highlights = list(set(highlights) - set(starred_story.highlights or []))
|
||||
removed_highlights = list(set(starred_story.highlights or []) - set(highlights))
|
||||
changed_user_notes = bool(user_notes != starred_story.user_notes)
|
||||
starred_story.user_tags = user_tags
|
||||
starred_story.highlights = highlights
|
||||
starred_story.user_notes = user_notes
|
||||
starred_story.save()
|
||||
|
||||
if len(highlights) == 1 and len(new_highlights) == 1:
|
||||
MStarredStoryCounts.adjust_count(request.user.pk, highlights=True, amount=1)
|
||||
elif len(highlights) == 0 and len(removed_highlights):
|
||||
MStarredStoryCounts.adjust_count(request.user.pk, highlights=True, amount=-1)
|
||||
for tag in new_user_tags:
|
||||
MStarredStoryCounts.adjust_count(request.user.pk, tag=tag, amount=1)
|
||||
for tag in removed_user_tags:
|
||||
MStarredStoryCounts.adjust_count(request.user.pk, tag=tag, amount=-1)
|
||||
|
||||
for tag in new_user_tags:
|
||||
MStarredStoryCounts.adjust_count(request.user.pk, tag=tag, amount=1)
|
||||
for tag in removed_user_tags:
|
||||
MStarredStoryCounts.adjust_count(request.user.pk, tag=tag, amount=-1)
|
||||
if random.random() < 0.01:
|
||||
MStarredStoryCounts.schedule_count_tags_for_user(request.user.pk)
|
||||
MStarredStoryCounts.count_for_user(request.user.pk, total_only=True)
|
||||
starred_counts, starred_count = MStarredStoryCounts.user_counts(request.user.pk, include_total=True)
|
||||
if not starred_count and len(starred_counts):
|
||||
starred_count = MStarredStory.objects(user_id=request.user.pk).count()
|
||||
|
||||
if not changed_user_notes:
|
||||
r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL)
|
||||
r.publish(request.user.username, 'story:starred:%s' % story.story_hash)
|
||||
|
||||
if created:
|
||||
logging.user(request, "~FCStarring: ~SB%s (~FM~SB%s~FC~SN)" % (story.story_title[:32], starred_story.user_tags))
|
||||
else:
|
||||
logging.user(request, "~FCUpdating starred:~SN~FC ~SB%s~SN (~FM~SB%s~FC~SN/~FM%s~FC)" % (story.story_title[:32], starred_story.user_tags, starred_story.user_notes))
|
||||
|
||||
datas.append({'code': code, 'message': message, 'starred_count': starred_count, 'starred_counts': starred_counts})
|
||||
|
||||
if random.random() < 0.01:
|
||||
MStarredStoryCounts.schedule_count_tags_for_user(request.user.pk)
|
||||
MStarredStoryCounts.count_for_user(request.user.pk, total_only=True)
|
||||
starred_counts, starred_count = MStarredStoryCounts.user_counts(request.user.pk, include_total=True)
|
||||
if not starred_count and len(starred_counts):
|
||||
starred_count = MStarredStory.objects(user_id=request.user.pk).count()
|
||||
|
||||
if not changed_user_notes:
|
||||
r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL)
|
||||
r.publish(request.user.username, 'story:starred:%s' % story.story_hash)
|
||||
|
||||
if created:
|
||||
logging.user(request, "~FCStarring: ~SB%s (~FM~SB%s~FC~SN)" % (story.story_title[:32], starred_story.user_tags))
|
||||
else:
|
||||
logging.user(request, "~FCUpdating starred:~SN~FC ~SB%s~SN (~FM~SB%s~FC~SN/~FM%s~FC)" % (story.story_title[:32], starred_story.user_tags, starred_story.user_notes))
|
||||
|
||||
return {'code': code, 'message': message, 'starred_count': starred_count, 'starred_counts': starred_counts}
|
||||
if len(datas) >= 2:
|
||||
return datas
|
||||
elif len(datas) == 1:
|
||||
return datas[0]
|
||||
return datas
|
||||
|
||||
@required_params('story_id')
|
||||
@ajax_login_required
|
||||
|
@ -2642,15 +2670,25 @@ def mark_story_hash_as_unstarred(request):
|
|||
def _mark_story_as_unstarred(request):
|
||||
code = 1
|
||||
story_id = request.POST.get('story_id', None)
|
||||
story_hash = request.POST.get('story_hash', None)
|
||||
story_hashes = request.POST.getlist('story_hash') or request.POST.getlist('story_hash[]')
|
||||
starred_counts = None
|
||||
starred_story = None
|
||||
|
||||
if story_id:
|
||||
starred_story = MStarredStory.objects(user_id=request.user.pk, story_guid=story_id)
|
||||
if not story_id or not starred_story:
|
||||
starred_story = MStarredStory.objects(user_id=request.user.pk, story_hash=story_hash or story_id)
|
||||
if starred_story:
|
||||
if starred_story:
|
||||
starred_story = starred_story[0]
|
||||
story_hashes = [starred_story.story_hash]
|
||||
else:
|
||||
story_hashes = [story_id]
|
||||
|
||||
datas = []
|
||||
for story_hash in story_hashes:
|
||||
starred_story = MStarredStory.objects(user_id=request.user.pk, story_hash=story_hash)
|
||||
if not starred_story:
|
||||
logging.user(request, "~FCUnstarring ~FRfailed~FC: %s not found" % (story_hash))
|
||||
datas.append({'code': -1, 'message': "Could not unsave story, not found", 'story_hash': story_hash})
|
||||
continue
|
||||
|
||||
starred_story = starred_story[0]
|
||||
logging.user(request, "~FCUnstarring: ~SB%s" % (starred_story.story_title[:50]))
|
||||
user_tags = starred_story.user_tags
|
||||
|
@ -2677,12 +2715,12 @@ def _mark_story_as_unstarred(request):
|
|||
|
||||
r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL)
|
||||
r.publish(request.user.username, 'story:unstarred:%s' % starred_story.story_hash)
|
||||
else:
|
||||
code = -1
|
||||
|
||||
return {'code': code, 'starred_counts': starred_counts}
|
||||
|
||||
if not story_hashes:
|
||||
datas.append(dict(code=-1, message=f"Failed to find {story_hashes}"))
|
||||
|
||||
return {'code': code, 'starred_counts': starred_counts, 'messages': datas}
|
||||
|
||||
@ajax_login_required
|
||||
@json.json_view
|
||||
def starred_counts(request):
|
||||
|
|
|
@ -2496,6 +2496,17 @@ class MStory(mongo.Document):
|
|||
def decoded_story_title(self):
|
||||
return html.unescape(self.story_title)
|
||||
|
||||
@property
|
||||
def story_content_str(self):
|
||||
story_content = self.story_content
|
||||
if not story_content and self.story_content_z:
|
||||
story_content = smart_str(zlib.decompress(self.story_content_z))
|
||||
else:
|
||||
story_content = smart_str(story_content)
|
||||
|
||||
return story_content
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
story_title_max = MStory._fields['story_title'].max_length
|
||||
story_content_type_max = MStory._fields['story_content_type'].max_length
|
||||
|
@ -2794,12 +2805,10 @@ class MStory(mongo.Document):
|
|||
|
||||
story_content = None
|
||||
if not text:
|
||||
story_content = self.story_content
|
||||
if not story_content and self.story_content_z:
|
||||
story_content = zlib.decompress(self.story_content_z)
|
||||
story_content = self.story_content_str
|
||||
elif text:
|
||||
if self.original_text_z:
|
||||
story_content = zlib.decompress(self.original_text_z)
|
||||
story_content = smart_str(zlib.decompress(self.original_text_z))
|
||||
if not story_content:
|
||||
return
|
||||
|
||||
|
@ -2815,6 +2824,33 @@ class MStory(mongo.Document):
|
|||
return
|
||||
|
||||
images = soup.findAll('img')
|
||||
|
||||
# Add youtube thumbnail and insert appropriately before/after images.
|
||||
# Give the Youtube a bit of an edge.
|
||||
video_thumbnails = soup.findAll('iframe', src=lambda x: x and any(y in x for y in ['youtube.com', 'ytimg.com']))
|
||||
for video_thumbnail in video_thumbnails:
|
||||
video_src = video_thumbnail.get('src')
|
||||
video_id = re.search('.*?youtube.com/embed/([A-Za-z0-9\-_]+)', video_src)
|
||||
if not video_id:
|
||||
video_id = re.search('.*?youtube.com/v/([A-Za-z0-9\-_]+)', video_src)
|
||||
if not video_id:
|
||||
video_id = re.search('.*?ytimg.com/vi/([A-Za-z0-9\-_]+)', video_src)
|
||||
if not video_id:
|
||||
video_id = re.search('.*?youtube.com/watch\?v=([A-Za-z0-9\-_]+)', video_src)
|
||||
if not video_id:
|
||||
logging.debug(f" ***> Couldn't find youtube url in {video_thumbnail}: {video_src}")
|
||||
continue
|
||||
video_img_url = f"https://img.youtube.com/vi/{video_id.groups()[0]}/0.jpg"
|
||||
iframe_index = story_content.index('<iframe')
|
||||
try:
|
||||
img_index = story_content.index('<img')*3
|
||||
except ValueError:
|
||||
img_index = None
|
||||
if not img_index or iframe_index < img_index:
|
||||
images.insert(0, video_img_url)
|
||||
else:
|
||||
images.append(video_img_url)
|
||||
|
||||
if not images:
|
||||
if not text:
|
||||
return self.extract_image_urls(force=force, text=True)
|
||||
|
@ -2826,7 +2862,10 @@ class MStory(mongo.Document):
|
|||
image_urls = []
|
||||
|
||||
for image in images:
|
||||
image_url = image.get('src')
|
||||
if isinstance(image, str):
|
||||
image_url = image
|
||||
else:
|
||||
image_url = image.get('src')
|
||||
if not image_url:
|
||||
continue
|
||||
if image_url and len(image_url) >= 1024:
|
||||
|
|
27
blog/_posts/2022-03-04-magazine-view.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
layout: post
|
||||
title: The Magazine view is a new perspective
|
||||
tags: ['web']
|
||||
---
|
||||
|
||||
Here's a nice new feature that brings a new perspective to your stories. It's called the Magazine view and you can access it next to the other story views:
|
||||
|
||||
<img src="/assets/magazine-views.png" style="width: 750px;">
|
||||
|
||||
|
||||
<img src="/assets/magazine-light.png" style="width: 750px;">
|
||||
<img src="/assets/magazine-dark.png" style="width: 750px;">
|
||||
|
||||
|
||||
|
||||
Loads of new features:
|
||||
|
||||
* The dashboard now has multiple, customizable rivers of news
|
||||
* Image previews are now customizable by size and layout
|
||||
* Story previews are also customizable by length
|
||||
* Images are now full bleed on the web (edge-to-edge)
|
||||
* Controls have been re-styled and made more accessible
|
||||
* Sizes, spaces, and text have all been tweaked for a more legible read
|
||||
* Upgraded backend: Python 2 to Python 3, latest Django and libraries, containerized infrastructure
|
||||
* Both Android and iOS apps have been updated with the new design
|
||||
|
BIN
blog/assets/magazine-dark.png
Normal file
After Width: | Height: | Size: 1.6 MiB |
BIN
blog/assets/magazine-light.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
blog/assets/magazine-views.png
Normal file
After Width: | Height: | Size: 33 KiB |
|
@ -21,16 +21,10 @@ limit_request_fields = 1000
|
|||
worker_tmp_dir = "/dev/shm"
|
||||
reload = True
|
||||
|
||||
if GIGS_OF_MEMORY > NUM_CPUS:
|
||||
workers = NUM_CPUS
|
||||
else:
|
||||
workers = int(NUM_CPUS / 2)
|
||||
workers = max(int(math.floor(GIGS_OF_MEMORY * 2)), 3)
|
||||
|
||||
if workers <= 4:
|
||||
workers = max(int(math.floor(GIGS_OF_MEMORY * 1000 / 512)), 3)
|
||||
|
||||
if workers > 8:
|
||||
workers = 8
|
||||
if workers > 4:
|
||||
workers = 4
|
||||
|
||||
if os.environ.get('DOCKERBUILD', False):
|
||||
workers = 2
|
||||
|
|
|
@ -24,7 +24,7 @@ django-cors-middleware==1.3.1
|
|||
django-extensions==3.1.0
|
||||
django-nose==1.4.7
|
||||
django-oauth-toolkit==1.3.3
|
||||
django-paypal==1.0.0
|
||||
django-paypal==1.1.2
|
||||
django-qurl==0.1.1
|
||||
django-pipeline>=2,<3
|
||||
django-redis-cache==3.0.0
|
||||
|
@ -74,6 +74,7 @@ numpy==1.19.4
|
|||
oauth2==1.9.0.post1
|
||||
oauthlib==3.1.0
|
||||
packaging==20.9
|
||||
paypalrestsdk==1.13.1
|
||||
pbr==5.6.0
|
||||
Pillow==8.0.1
|
||||
pluggy==0.13.1
|
||||
|
|
|
@ -59,6 +59,7 @@ frontend public
|
|||
use_backend nginx if { path_beg /favicon }
|
||||
use_backend nginx if { path_beg /crossdomain/ }
|
||||
use_backend nginx if { path_beg /robots }
|
||||
use_backend nginx if { hdr_sub(host) -i blog.localhost }
|
||||
#use_backend self if { path_beg /munin/ }
|
||||
|
||||
# use_backend gunicorn_counts if is_unread_count
|
||||
|
|
|
@ -62,6 +62,10 @@ server {
|
|||
|
||||
location /static/ {
|
||||
gzip_static on;
|
||||
gzip on;
|
||||
gzip_comp_level 6;
|
||||
gzip_vary on;
|
||||
gzip_types text/plain text/css application/json application/x-javascript application/javascript text/xml application/xml application/rss+xml text/javascript image/svg+xml application/vnd.ms-fontobject application/x-font-ttf font/opentype;
|
||||
expires max;
|
||||
keepalive_timeout 1;
|
||||
root /srv/newsblur;
|
||||
|
|
|
@ -62,7 +62,7 @@ server {
|
|||
keepalive_timeout 1;
|
||||
root /srv/newsblur;
|
||||
}
|
||||
|
||||
|
||||
location /static/ {
|
||||
gzip_static on;
|
||||
expires max;
|
||||
|
@ -114,3 +114,17 @@ server {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
server {
|
||||
listen 81;
|
||||
|
||||
server_name blog.localhost;
|
||||
|
||||
set_real_ip_from 0.0.0.0/0;
|
||||
real_ip_header X-Forwarded-For;
|
||||
real_ip_recursive on;
|
||||
|
||||
root /srv/newsblur/blog/_site;
|
||||
}
|
||||
|
||||
|
|
|
@ -1398,7 +1398,7 @@ hr {
|
|||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;>>>
|
||||
overflow: hidden;
|
||||
}
|
||||
.NB-feedbar .NB-feedbar-options-container {
|
||||
float: right;
|
||||
|
@ -1761,6 +1761,31 @@ hr {
|
|||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
}
|
||||
.NB-theme-feed-size-xs .NB-layout-magazine .NB-story-title {
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.NB-theme-feed-size-s .NB-layout-magazine .NB-story-title {
|
||||
font-size: 15px;
|
||||
line-height: 17px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
.NB-theme-feed-size-m .NB-layout-magazine .NB-story-title {
|
||||
font-size: 16px;
|
||||
line-height: 19px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
.NB-theme-feed-size-l .NB-layout-magazine .NB-story-title {
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
padding-top: 25px;
|
||||
}
|
||||
.NB-theme-feed-size-xl .NB-layout-magazine .NB-story-title {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
.NB-story-pane-west .NB-story-title {
|
||||
padding-right: 4px;
|
||||
|
@ -1772,30 +1797,51 @@ hr {
|
|||
.NB-view-river .NB-story-pane-west .NB-story-title {
|
||||
padding-left: 56px;
|
||||
}
|
||||
.NB-image-preview-small-right:not(.NB-story-layout-grid) .NB-story-title.NB-has-image,
|
||||
.NB-image-preview-large-right:not(.NB-story-layout-grid) .NB-story-title.NB-has-image {
|
||||
.NB-image-preview-small-right .NB-story-title.NB-has-image,
|
||||
.NB-image-preview-large-right .NB-story-title.NB-has-image {
|
||||
padding-right: 242px;
|
||||
}
|
||||
.NB-image-preview-small-right:not(.NB-story-layout-grid) .NB-story-pane-west .NB-story-title.NB-has-image,
|
||||
.NB-image-preview-large-right:not(.NB-story-layout-grid) .NB-story-pane-west .NB-story-title.NB-has-image {
|
||||
.NB-image-preview-small-right .NB-story-pane-west .NB-story-title.NB-has-image,
|
||||
.NB-image-preview-large-right .NB-story-pane-west .NB-story-title.NB-has-image {
|
||||
padding-right: 90px;
|
||||
}
|
||||
/* Ragged right but not ragged left
|
||||
.NB-image-preview-small-left .NB-story-title.NB-has-image,
|
||||
.NB-image-preview-large-left .NB-story-title.NB-has-image */
|
||||
.NB-image-preview-small-left:not(.NB-story-layout-grid) .NB-story-title,
|
||||
.NB-image-preview-large-left:not(.NB-story-layout-grid) .NB-story-title {
|
||||
.NB-image-preview-small-left .NB-story-title,
|
||||
.NB-image-preview-large-left .NB-story-title {
|
||||
padding-left: 116px;
|
||||
}
|
||||
.NB-image-preview-small-left:not(.NB-story-layout-grid) .NB-view-river .NB-story-title,
|
||||
.NB-image-preview-large-left:not(.NB-story-layout-grid) .NB-view-river .NB-story-title {
|
||||
.NB-image-preview-small-left .NB-story-title.NB-story-grid,
|
||||
.NB-image-preview-large-left .NB-story-title.NB-story-grid {
|
||||
padding-left: 0;
|
||||
}
|
||||
.NB-image-preview-small-left .NB-view-river .NB-story-title,
|
||||
.NB-image-preview-large-left .NB-view-river .NB-story-title {
|
||||
padding-left: 242px;
|
||||
}
|
||||
.NB-image-preview-small-left:not(.NB-story-layout-grid) .NB-view-river .NB-story-pane-west .NB-story-title,
|
||||
.NB-image-preview-large-left:not(.NB-story-layout-grid) .NB-view-river .NB-story-pane-west .NB-story-title {
|
||||
.NB-image-preview-small-left .NB-view-river .NB-story-pane-west .NB-story-title,
|
||||
.NB-image-preview-large-left .NB-view-river .NB-story-pane-west .NB-story-title {
|
||||
padding-left: 120px;
|
||||
}
|
||||
|
||||
.NB-image-preview-small-left .NB-view-river .NB-story-title.NB-story-grid,
|
||||
.NB-image-preview-large-left .NB-view-river .NB-story-title.NB-story-grid {
|
||||
padding-left: 0;
|
||||
}
|
||||
.NB-image-preview-small-left .NB-layout-magazine .NB-story-title,
|
||||
.NB-image-preview-large-left .NB-layout-magazine .NB-story-title,
|
||||
.NB-view-river .NB-layout-magazine .NB-story-title {
|
||||
padding-left: 400px;
|
||||
min-height: 200px;
|
||||
}
|
||||
.NB-image-preview-none .NB-layout-magazine .NB-story-title {
|
||||
padding-left: 56px;
|
||||
}
|
||||
.NB-image-preview-large-right .NB-layout-magazine .NB-story-title,
|
||||
.NB-image-preview-small-right .NB-layout-magazine .NB-story-title {
|
||||
padding-left: 48px;
|
||||
padding-right: 400px;
|
||||
}
|
||||
.NB-story-title .NB-storytitles-feed-border-inner,
|
||||
.NB-story-title .NB-storytitles-feed-border-outer {
|
||||
position: absolute;
|
||||
|
@ -1823,13 +1869,17 @@ hr {
|
|||
.NB-story-pane-west .NB-story-title .NB-storytitles-sentiment {
|
||||
left: 4px;
|
||||
}
|
||||
.NB-image-preview-small-left:not(.NB-story-layout-grid) .NB-story-title .NB-storytitles-sentiment,
|
||||
.NB-image-preview-large-left:not(.NB-story-layout-grid) .NB-story-title .NB-storytitles-sentiment {
|
||||
.NB-image-preview-small-left .NB-story-title .NB-storytitles-sentiment,
|
||||
.NB-image-preview-large-left .NB-story-title .NB-storytitles-sentiment {
|
||||
left: 89px;
|
||||
}
|
||||
.NB-layout-magazine .NB-story-title .NB-storytitles-sentiment {
|
||||
left: -28px;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.NB-image-preview-small-left:not(.NB-story-layout-grid) .NB-view-river .NB-story-pane-west .NB-story-title .NB-storytitles-sentiment,
|
||||
.NB-image-preview-large-left:not(.NB-story-layout-grid) .NB-view-river .NB-story-pane-west .NB-story-title .NB-storytitles-sentiment {
|
||||
.NB-image-preview-small-left .NB-view-river .NB-story-pane-west .NB-story-title .NB-storytitles-sentiment,
|
||||
.NB-image-preview-large-left .NB-view-river .NB-story-pane-west .NB-story-title .NB-storytitles-sentiment {
|
||||
top: 24px;
|
||||
}
|
||||
.NB-story-title.NB-story-positive .NB-storytitles-sentiment {
|
||||
|
@ -1862,13 +1912,20 @@ hr {
|
|||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.NB-layout-magazine .NB-story-title .NB-storytitles-story-image {
|
||||
background-size: 40%, contain;
|
||||
}
|
||||
.NB-story-title.read .NB-storytitles-story-image {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.NB-image-preview-large-left:not(.NB-story-layout-grid) .NB-story-title .NB-storytitles-story-image {
|
||||
.NB-image-preview-large-left .NB-story-title .NB-storytitles-story-image {
|
||||
right: inherit;
|
||||
left: 8px;
|
||||
}
|
||||
.NB-image-preview-large-left .NB-layout-magazine .NB-story-title .NB-storytitles-story-image {
|
||||
height: 100%;
|
||||
top: 0;
|
||||
}
|
||||
.NB-story-title.NB-story-starred .NB-storytitles-star,
|
||||
.NB-story-title.read.NB-story-starred .NB-storytitles-star {
|
||||
width: 16px;
|
||||
|
@ -2013,6 +2070,15 @@ hr {
|
|||
.NB-content-preview-large .NB-storytitles-content-preview {
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
.NB-content-preview-small .NB-layout-magazine .NB-storytitles-content-preview {
|
||||
-webkit-line-clamp: 8;
|
||||
}
|
||||
.NB-content-preview-medium .NB-layout-magazine .NB-storytitles-content-preview {
|
||||
-webkit-line-clamp: 12;
|
||||
}
|
||||
.NB-content-preview-large .NB-layout-magazine .NB-storytitles-content-preview {
|
||||
-webkit-line-clamp: 16;
|
||||
}
|
||||
|
||||
.NB-theme-feed-size-xs .NB-storytitles-content-preview {
|
||||
font-size: 11px;
|
||||
|
@ -2046,6 +2112,33 @@ hr {
|
|||
.NB-story-layout-list .NB-selected .NB-storytitles-content-preview {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.NB-theme-feed-size-xs .NB-layout-magazine .NB-storytitles-content-preview {
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.NB-theme-feed-size-s .NB-layout-magazine .NB-storytitles-content-preview {
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.NB-theme-feed-size-m .NB-layout-magazine .NB-storytitles-content-preview {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.NB-theme-feed-size-l .NB-layout-magazine .NB-storytitles-content-preview {
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
margin: 25px 0;
|
||||
}
|
||||
.NB-theme-feed-size-xl .NB-layout-magazine .NB-storytitles-content-preview {
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
margin: 30px 0;
|
||||
}
|
||||
.read .NB-storytitles-content-preview {
|
||||
color: #b3b3b1;
|
||||
}
|
||||
|
@ -2086,6 +2179,13 @@ hr {
|
|||
color: #707070;
|
||||
position: absolute;
|
||||
}
|
||||
.NB-layout-magazine .NB-story-title .NB-story-feed {
|
||||
position: relative;
|
||||
top: auto;
|
||||
left: auto;
|
||||
width: 100%;
|
||||
margin: 0 0 14px 4px;
|
||||
}
|
||||
.NB-story-pane-west .NB-story-title .NB-story-feed {
|
||||
left: -22px;
|
||||
top: 9px;
|
||||
|
@ -2157,12 +2257,20 @@ hr {
|
|||
.NB-story-pane-west .NB-story-title .NB-story-manage-icon {
|
||||
left: 4px;
|
||||
}
|
||||
.NB-image-preview-small-left:not(.NB-story-layout-grid) .NB-story-title .NB-story-manage-icon,
|
||||
.NB-image-preview-large-left:not(.NB-story-layout-grid) .NB-story-title .NB-story-manage-icon {
|
||||
.NB-image-preview-small-left .NB-story-title .NB-story-manage-icon,
|
||||
.NB-image-preview-large-left .NB-story-title .NB-story-manage-icon {
|
||||
left: 90px;
|
||||
}
|
||||
.NB-image-preview-small-left:not(.NB-story-layout-grid) .NB-view-river .NB-story-pane-west .NB-story-title .NB-story-manage-icon,
|
||||
.NB-image-preview-large-left:not(.NB-story-layout-grid) .NB-view-river .NB-story-pane-west .NB-story-title .NB-story-manage-icon {
|
||||
.NB-layout-magazine .NB-story-title .NB-story-manage-icon {
|
||||
left: -28px;
|
||||
top: 4px;
|
||||
}
|
||||
.NB-layout-grid .NB-story-title .NB-story-manage-icon {
|
||||
left: 0;
|
||||
top: 2px;
|
||||
}
|
||||
.NB-image-preview-small-left .NB-view-river .NB-story-pane-west .NB-story-title .NB-story-manage-icon,
|
||||
.NB-image-preview-large-left .NB-view-river .NB-story-pane-west .NB-story-title .NB-story-manage-icon {
|
||||
top: 24px;
|
||||
}
|
||||
.NB-story-title:hover .NB-story-manage-icon {
|
||||
|
@ -2367,20 +2475,20 @@ hr {
|
|||
box-sizing: border-box;
|
||||
}
|
||||
.NB-layout-grid.NB-grid-height-xs .NB-story-grid {
|
||||
height: 220px;
|
||||
}
|
||||
.NB-layout-grid.NB-grid-height-s .NB-story-grid {
|
||||
height: 280px;
|
||||
}
|
||||
.NB-layout-grid.NB-grid-height-m .NB-story-grid {
|
||||
.NB-layout-grid.NB-grid-height-s .NB-story-grid {
|
||||
height: 360px;
|
||||
}
|
||||
.NB-layout-grid.NB-grid-height-l .NB-story-grid {
|
||||
.NB-layout-grid.NB-grid-height-m .NB-story-grid {
|
||||
height: 420px;
|
||||
}
|
||||
.NB-layout-grid.NB-grid-height-xl .NB-story-grid {
|
||||
.NB-layout-grid.NB-grid-height-l .NB-story-grid {
|
||||
height: 480px;
|
||||
}
|
||||
.NB-layout-grid.NB-grid-height-xl .NB-story-grid {
|
||||
height: 540px;
|
||||
}
|
||||
.NB-story-grid.NB-story-title-hide-preview,
|
||||
.NB-content-preview-title .NB-story-grid {
|
||||
height: 296px;
|
||||
|
@ -2412,7 +2520,7 @@ hr {
|
|||
grid-template-columns: none;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
.NB-view-river .NB-story-grid {
|
||||
.NB-story-layout-grid .NB-view-river .NB-story-grid {
|
||||
padding-left: 0;
|
||||
border: none;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||
|
@ -2443,7 +2551,6 @@ hr {
|
|||
line-height: 1.3em;
|
||||
padding: 0;
|
||||
word-break: break-word;
|
||||
word-break: anywhere;
|
||||
}
|
||||
.NB-theme-feed-size-xs .NB-story-grid .NB-storytitles-title {
|
||||
font-size: 14px;
|
||||
|
@ -2465,23 +2572,37 @@ hr {
|
|||
.NB-story-grid .NB-storytitles-story-image-container {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.NB-story-grid .NB-storytitles-story-image {
|
||||
.NB-image-preview-small-left .NB-story-grid .NB-storytitles-story-image,
|
||||
.NB-image-preview-large-left .NB-story-grid .NB-storytitles-story-image,
|
||||
.NB-image-preview-small-right .NB-story-grid .NB-storytitles-story-image,
|
||||
.NB-image-preview-large-right .NB-story-grid .NB-storytitles-story-image {
|
||||
display: none;
|
||||
position: inherit;
|
||||
width: 100%;
|
||||
height: 186px;
|
||||
background-size: 64px 64px, cover;
|
||||
background-position: 50% 60%, 50% 30%;
|
||||
border-radius: 0;
|
||||
background-repeat: no-repeat;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.NB-layout-grid.NB-grid-height-xs .NB-story-grid .NB-storytitles-story-image {
|
||||
height: 86px;
|
||||
.NB-image-preview-small-left .NB-grid-height-xs .NB-story-grid .NB-storytitles-story-image,
|
||||
.NB-image-preview-small-right .NB-grid-height-xs .NB-story-grid .NB-storytitles-story-image {
|
||||
height: 76px;
|
||||
}
|
||||
.NB-layout-grid.NB-grid-height-s .NB-story-grid .NB-storytitles-story-image {
|
||||
height: 112px;
|
||||
.NB-image-preview-large-left .NB-grid-height-xs .NB-story-grid .NB-storytitles-story-image,
|
||||
.NB-image-preview-large-right .NB-grid-height-xs .NB-story-grid .NB-storytitles-story-image {
|
||||
height: 106px;
|
||||
}
|
||||
.NB-image-preview-small-left .NB-grid-height-s .NB-story-grid .NB-storytitles-story-image,
|
||||
.NB-image-preview-small-right .NB-grid-height-s .NB-story-grid .NB-storytitles-story-image {
|
||||
height: 106px;
|
||||
}
|
||||
.NB-image-preview-large-left .NB-grid-height-s .NB-story-grid .NB-storytitles-story-image,
|
||||
.NB-image-preview-large-right .NB-grid-height-s .NB-story-grid .NB-storytitles-story-image {
|
||||
height: 146px;
|
||||
}
|
||||
.NB-image-preview-small-left .NB-grid-height-m .NB-story-grid .NB-storytitles-story-image,
|
||||
.NB-image-preview-small-right .NB-grid-height-m .NB-story-grid .NB-storytitles-story-image {
|
||||
|
@ -2489,23 +2610,23 @@ hr {
|
|||
}
|
||||
.NB-image-preview-large-left .NB-grid-height-m .NB-story-grid .NB-storytitles-story-image,
|
||||
.NB-image-preview-large-right .NB-grid-height-m .NB-story-grid .NB-storytitles-story-image {
|
||||
height: 204px;
|
||||
height: 202px;
|
||||
}
|
||||
.NB-image-preview-small-left .NB-grid-height-l .NB-story-grid .NB-storytitles-story-image,
|
||||
.NB-image-preview-small-right .NB-grid-height-l .NB-story-grid .NB-storytitles-story-image {
|
||||
height: 152px;
|
||||
height: 202px;
|
||||
}
|
||||
.NB-image-preview-large-left .NB-grid-height-l .NB-story-grid .NB-storytitles-story-image,
|
||||
.NB-image-preview-large-right .NB-grid-height-l .NB-story-grid .NB-storytitles-story-image {
|
||||
height: 264px;
|
||||
height: 242px;
|
||||
}
|
||||
.NB-image-preview-small-left .NB-grid-height-xl .NB-story-grid .NB-storytitles-story-image,
|
||||
.NB-image-preview-small-right .NB-grid-height-xl .NB-story-grid .NB-storytitles-story-image {
|
||||
height: 202px;
|
||||
height: 242px;
|
||||
}
|
||||
.NB-image-preview-large-left .NB-grid-height-xl .NB-story-grid .NB-storytitles-story-image,
|
||||
.NB-image-preview-large-right .NB-grid-height-xl .NB-story-grid .NB-storytitles-story-image {
|
||||
height: 324px;
|
||||
height: 272px;
|
||||
}
|
||||
.NB-layout-grid.NB-grid-height-xs.NB-grid-columns-3 .NB-story-grid .NB-storytitles-author,
|
||||
.NB-layout-grid.NB-grid-height-xs.NB-grid-columns-4 .NB-story-grid .NB-storytitles-author,
|
||||
|
@ -2551,7 +2672,7 @@ hr {
|
|||
.NB-story-grid.read .NB-storytitles-content-preview {
|
||||
color: #D6D6DE;
|
||||
}
|
||||
.NB-story-grid.NB-story-title .NB-storytitles-grid-bottom {
|
||||
.NB-story-title .NB-storytitles-grid-bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
top: inherit;
|
||||
|
@ -2567,6 +2688,16 @@ hr {
|
|||
.NB-story-grid:not(.read):hover .NB-storytitles-grid-bottom {
|
||||
background-color: #F7F7F6;
|
||||
}
|
||||
.NB-storytitles-magazine-bottom {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.NB-storytitles-magazine-bottom .story_date {
|
||||
top: 0;
|
||||
}
|
||||
.NB-layout-magazine .NB-story-title .NB-storytitles-author {
|
||||
padding-left: 4px;
|
||||
}
|
||||
.NB-story-grid.NB-story-title .NB-storytitles-author {
|
||||
float: left;
|
||||
display: block;
|
||||
|
@ -2580,10 +2711,11 @@ hr {
|
|||
display: block;
|
||||
padding: 0 4px 0 0;
|
||||
}
|
||||
.NB-story-grid .NB-storytitles-sentiment,
|
||||
.NB-story-grid .NB-story-manage-icon {
|
||||
.NB-story-title.NB-story-grid .NB-storytitles-sentiment,
|
||||
.NB-story-title.NB-story-grid .NB-story-manage-icon {
|
||||
left: -28px;
|
||||
}
|
||||
.NB-story-magazine .NB-story-feed,
|
||||
.NB-story-grid .NB-story-feed {
|
||||
position: static;
|
||||
margin: 10px 0;
|
||||
|
@ -2593,11 +2725,13 @@ hr {
|
|||
.NB-story-grid .story_title {
|
||||
position: relative;
|
||||
}
|
||||
.NB-layout-magazine .NB-story-feed .feed_favicon,
|
||||
.NB-story-grid .NB-story-feed .feed_favicon {
|
||||
margin: 0 4px 0 0;
|
||||
float: left;
|
||||
position: static;
|
||||
}
|
||||
.NB-layout-magazine .NB-story-feed .feed_title,
|
||||
.NB-story-grid .NB-story-feed .feed_title {
|
||||
width: auto;
|
||||
-webkit-line-clamp: 1;
|
||||
|
@ -2851,20 +2985,64 @@ body {
|
|||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
.NB-image-preview-small-left:not(.NB-story-layout-grid) .NB-storytitles-story-image {
|
||||
.NB-image-preview-small-left .NB-storytitles-story-image {
|
||||
width: 62px;
|
||||
height: 70%;
|
||||
left: 22px;
|
||||
top: 7px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.NB-image-preview-small-right:not(.NB-story-layout-grid) .NB-storytitles-story-image {
|
||||
.NB-image-preview-small-right .NB-storytitles-story-image {
|
||||
width: 62px;
|
||||
height: 70%;
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.NB-image-preview-small-left .NB-layout-magazine .NB-storytitles-story-image-container,
|
||||
.NB-image-preview-large-left .NB-layout-magazine .NB-storytitles-story-image-container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 362px;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 8px;
|
||||
}
|
||||
.NB-image-preview-small-left .NB-layout-magazine .NB-storytitles-story-image,
|
||||
.NB-image-preview-small-right .NB-layout-magazine .NB-storytitles-story-image {
|
||||
top: auto;
|
||||
right: auto;
|
||||
left: auto;
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.NB-image-preview-large-left .NB-layout-magazine .NB-storytitles-story-image,
|
||||
.NB-image-preview-large-right .NB-layout-magazine .NB-storytitles-story-image {
|
||||
top: auto;
|
||||
right: auto;
|
||||
left: auto;
|
||||
width: 96%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border-radius: 0;
|
||||
}
|
||||
.NB-image-preview-small-right .NB-layout-magazine .NB-storytitles-story-image-container,
|
||||
.NB-image-preview-large-right .NB-layout-magazine .NB-storytitles-story-image-container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 362px;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
right: 8px;
|
||||
}
|
||||
.NB-image-preview-small-left.NB-story-layout-list .NB-storytitles-story-image,
|
||||
.NB-image-preview-small-right.NB-story-layout-list .NB-storytitles-story-image,
|
||||
.NB-image-preview-small-left.NB-story-layout-split .NB-story-pane-south .NB-storytitles-story-image,
|
||||
|
@ -2987,7 +3165,7 @@ body {
|
|||
}
|
||||
|
||||
.NB-feed-story .NB-feed-story-header-info {
|
||||
padding: 0 206px 8px 28px;
|
||||
padding: 8px 236px 8px 28px;
|
||||
background-color: #EEF0E9;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, .2);
|
||||
border-left: 3px solid transparent;
|
||||
|
@ -3019,7 +3197,7 @@ body {
|
|||
rgb(76,76,76) 36%,
|
||||
rgb(55,55,55) 84%
|
||||
);
|
||||
padding: 2px 200px 2px 28px;
|
||||
padding: 2px 236px 2px 28px;
|
||||
position: sticky;
|
||||
top: -1px;
|
||||
border-bottom: 1px solid #000;
|
||||
|
@ -3042,15 +3220,25 @@ body {
|
|||
padding: 2px 0;
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
position: relative;
|
||||
}
|
||||
.NB-inverse .NB-feed-story-feed {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.NB-pref-story-position-center .NB-feed-story-feed {
|
||||
margin: 0 auto;
|
||||
width: 700px;
|
||||
}
|
||||
.NB-pref-story-position-right .NB-feed-story-feed {
|
||||
margin: 0 0 0 auto;
|
||||
width: 700px;
|
||||
}
|
||||
|
||||
.NB-feed-story-feed .feed_favicon {
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 5px;
|
||||
left: -22px;
|
||||
top: 3px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
@ -3203,6 +3391,14 @@ body {
|
|||
position: relative;
|
||||
clear: both;
|
||||
}
|
||||
.NB-pref-story-position-center .NB-feed-story .NB-feed-story-title-container {
|
||||
margin: 0 auto;
|
||||
max-width: 700px;
|
||||
}
|
||||
.NB-pref-story-position-right .NB-feed-story .NB-feed-story-title-container {
|
||||
margin: 0 0 0 auto;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.NB-feed-story a.NB-feed-story-title {
|
||||
display: block;
|
||||
|
@ -3423,6 +3619,14 @@ body {
|
|||
.NB-feed-story .NB-feed-story-header .NB-feed-story-date-line {
|
||||
clear: left;
|
||||
}
|
||||
.NB-pref-story-position-center .NB-feed-story .NB-feed-story-header .NB-feed-story-date-line {
|
||||
margin: 0 auto;
|
||||
max-width: 700px;
|
||||
}
|
||||
.NB-pref-story-position-right .NB-feed-story .NB-feed-story-header .NB-feed-story-date-line {
|
||||
margin: 0 0 0 auto;
|
||||
max-width: 700px;
|
||||
}
|
||||
.NB-feed-story .NB-feed-story-header .NB-feed-story-show-changes {
|
||||
float: left;
|
||||
font-size: 12px;
|
||||
|
@ -3457,14 +3661,21 @@ body {
|
|||
|
||||
.NB-feed-story .NB-feed-story-header .NB-feed-story-starred-date {
|
||||
font-size: 12px;
|
||||
background: transparent url('/media/embed/icons/circular/clock.png') no-repeat 0 center;
|
||||
background-size: 16px;
|
||||
padding-left: 20px;
|
||||
/* text-shadow: 1px 1px 0 rgba(255, 255, 255, .5);*/
|
||||
/* text-shadow: 1px 1px 0 rgba(255, 255, 255, .5);*/
|
||||
color: #808080;
|
||||
display: block;
|
||||
clear: both;
|
||||
margin: 6px 0;
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
.NB-feed-story .NB-feed-story-header .NB-feed-story-starred-date .NB-icon {
|
||||
background: transparent url('/media/embed/icons/circular/clock.png') no-repeat 0 center;
|
||||
background-size: 16px;
|
||||
height: 16px;
|
||||
margin-top: 6px;
|
||||
display: inline-block;
|
||||
vertical-align: text-bottom;
|
||||
color: #9B9D97;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.NB-theme-size-xs .NB-feed-story-header .NB-feed-story-starred-date {
|
||||
font-size: 10px;
|
||||
|
@ -3478,13 +3689,27 @@ body {
|
|||
.NB-theme-size-xl .NB-feed-story-header .NB-feed-story-starred-date {
|
||||
font-size: 14px;
|
||||
}
|
||||
.NB-pref-story-position-center .NB-feed-story-header .NB-feed-story-starred-date {
|
||||
margin: 6px auto 0;
|
||||
max-width: 700px;
|
||||
}
|
||||
.NB-pref-story-position-right .NB-feed-story-header .NB-feed-story-starred-date {
|
||||
margin: 6px 0 0 auto;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.NB-feed-story .NB-feed-story-content {
|
||||
padding: 12px 0 0;
|
||||
max-width: 700px;
|
||||
min-height: 12px;
|
||||
}
|
||||
.NB-pref-full-width-story .NB-feed-story .NB-feed-story-content {
|
||||
.NB-pref-story-position-center .NB-feed-story .NB-feed-story-content {
|
||||
margin: 0 auto;
|
||||
}
|
||||
.NB-pref-story-position-right .NB-feed-story .NB-feed-story-content {
|
||||
margin: 0 0 0 auto;
|
||||
}
|
||||
.NB-pref-story-position-stretch .NB-feed-story .NB-feed-story-content {
|
||||
max-width: none;
|
||||
}
|
||||
.NB-feed-story .NB-narrow-content .NB-feed-story-content {
|
||||
|
@ -3667,15 +3892,20 @@ body {
|
|||
/* ============ */
|
||||
|
||||
.NB-feed-story-comments {
|
||||
margin: 0 236px 32px 28px;
|
||||
padding: 0 236px 32px 28px;
|
||||
max-width: 700px;
|
||||
/* border-bottom: 1px solid #EAECE8;*/
|
||||
clear: both;
|
||||
background-color: white;
|
||||
position: relative;
|
||||
}
|
||||
.NB-narrow-content .NB-feed-story-comments {
|
||||
margin-right: 28px;
|
||||
padding-right: 28px;
|
||||
}
|
||||
.NB-pref-story-position-center .NB-feed-story-comments {
|
||||
margin: 0 auto;
|
||||
}
|
||||
.NB-pref-story-position-right .NB-feed-story-comments {
|
||||
margin: 0 0 0 auto;
|
||||
}
|
||||
.NB-story-comment {
|
||||
border-bottom: 1px solid #EAECE8;
|
||||
|
@ -5126,6 +5356,16 @@ background: transparent;
|
|||
background: transparent url('/media/img/icons/circular/nav_story_grid_active.png') no-repeat center center;
|
||||
background-size: 15px;
|
||||
}
|
||||
.NB-taskbar .NB-task-layout-magazine .NB-task-image {
|
||||
left: 12px;
|
||||
background: transparent url('/media/img/icons/circular/nav_story_magazine.png') no-repeat center center;
|
||||
background-size: 15px;
|
||||
}
|
||||
.NB-taskbar .NB-task-layout-magazine.NB-active .NB-task-image {
|
||||
left: 12px;
|
||||
background: transparent url('/media/img/icons/circular/nav_story_magazine_active.png') no-repeat center center;
|
||||
background-size: 15px;
|
||||
}
|
||||
|
||||
.NB-taskbar .NB-task-return .NB-task-image {
|
||||
background: transparent url('/media/embed/icons/silk/arrow_undo.png') no-repeat center center;
|
||||
|
@ -5372,6 +5612,37 @@ background: transparent;
|
|||
.NB-options-story-titles-pane-south .NB-icon {
|
||||
background: transparent url("/media/embed/reader/layout_bottom.png") no-repeat center center;
|
||||
}
|
||||
.NB-style-popover .NB-story-position-option .NB-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
.NB-options-story-position .NB-story-position-option {
|
||||
width: auto;
|
||||
font-size: 12px;
|
||||
padding: 4px 15px;
|
||||
}
|
||||
.NB-options-story-position-stretch {
|
||||
|
||||
}
|
||||
.NB-options-story-position-left .NB-icon {
|
||||
background: transparent url("/media/embed/reader/position_left.svg") no-repeat center center;
|
||||
background-size: 16px;
|
||||
}
|
||||
.NB-options-story-position-center .NB-icon {
|
||||
background: transparent url("/media/embed/reader/position_center.svg") no-repeat center center;
|
||||
background-size: 16px;
|
||||
}
|
||||
.NB-options-story-position-right .NB-icon {
|
||||
background: transparent url("/media/embed/reader/position_right.svg") no-repeat center center;
|
||||
background-size: 16px;
|
||||
}
|
||||
.NB-options-story-position-stretch .NB-icon {
|
||||
background: transparent url("/media/embed/reader/position_stretch.png") no-repeat center center;
|
||||
background-size: 16px;
|
||||
}
|
||||
|
||||
.NB-style-popover .NB-options-single-story {
|
||||
margin-top: 6px;
|
||||
|
@ -7918,7 +8189,6 @@ form.opml_import_form input {
|
|||
margin: 0 4px 0 0;
|
||||
}
|
||||
|
||||
}
|
||||
.NB-menu-manage .NB-menu-manage-story-share-confirm .NB-sideoption-share {
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
|
|
BIN
media/img/icons/circular/nav_story_magazine.png
Normal file
After Width: | Height: | Size: 265 B |
BIN
media/img/icons/circular/nav_story_magazine_active.png
Normal file
After Width: | Height: | Size: 265 B |
4
media/img/reader/position_center.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m1050 700v250c0 13.262-5.2695 25.98-14.645 35.355s-22.094 14.645-35.355 14.645h-350v100c0 17.863-9.5312 34.371-25 43.301-15.469 8.9336-34.531 8.9336-50 0-15.469-8.9297-25-25.438-25-43.301v-100h-350c-13.262 0-25.98-5.2695-35.355-14.645s-14.645-22.094-14.645-35.355v-250c0-13.262 5.2695-25.98 14.645-35.355s22.094-14.645 35.355-14.645h350v-100h-200c-13.262 0-25.98-5.2695-35.355-14.645s-14.645-22.094-14.645-35.355v-250c0-13.262 5.2695-25.98 14.645-35.355s22.094-14.645 35.355-14.645h200v-100c0-17.863 9.5312-34.371 25-43.301 15.469-8.9336 34.531-8.9336 50 0 15.469 8.9297 25 25.438 25 43.301v100h200c13.262 0 25.98 5.2695 35.355 14.645s14.645 22.094 14.645 35.355v250c0 13.262-5.2695 25.98-14.645 35.355s-22.094 14.645-35.355 14.645h-200v100h350c13.262 0 25.98 5.2695 35.355 14.645s14.645 22.094 14.645 35.355z" fill="#7f7f79"/>
|
||||
</svg>
|
After Width: | Height: | Size: 994 B |
4
media/img/reader/position_left.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m100 1150c-13.262 0-25.98-5.2695-35.355-14.645s-14.645-22.094-14.645-35.355v-1e3c0-17.863 9.5312-34.371 25-43.301 15.469-8.9336 34.531-8.9336 50 0 15.469 8.9297 25 25.438 25 43.301v1e3c0 13.262-5.2695 25.98-14.645 35.355s-22.094 14.645-35.355 14.645zm1e3 -500h-800c-13.262 0-25.98 5.2695-35.355 14.645s-14.645 22.094-14.645 35.355v250c0 13.262 5.2695 25.98 14.645 35.355s22.094 14.645 35.355 14.645h800c13.262 0 25.98-5.2695 35.355-14.645s14.645-22.094 14.645-35.355v-250c0-13.262-5.2695-25.98-14.645-35.355s-22.094-14.645-35.355-14.645zm-800-100h500c13.262 0 25.98-5.2695 35.355-14.645s14.645-22.094 14.645-35.355v-250c0-13.262-5.2695-25.98-14.645-35.355s-22.094-14.645-35.355-14.645h-500c-13.262 0-25.98 5.2695-35.355 14.645s-14.645 22.094-14.645 35.355v250c0 13.262 5.2695 25.98 14.645 35.355s22.094 14.645 35.355 14.645z" fill="#7f7f79"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1,009 B |
4
media/img/reader/position_right.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m950 250v250c0 13.262-5.2695 25.98-14.645 35.355s-22.094 14.645-35.355 14.645h-500c-13.262 0-25.98-5.2695-35.355-14.645s-14.645-22.094-14.645-35.355v-250c0-13.262 5.2695-25.98 14.645-35.355s22.094-14.645 35.355-14.645h500c13.262 0 25.98 5.2695 35.355 14.645s14.645 22.094 14.645 35.355zm150-200c-13.262 0-25.98 5.2695-35.355 14.645s-14.645 22.094-14.645 35.355v1e3c0 17.863 9.5312 34.371 25 43.301 15.469 8.9336 34.531 8.9336 50 0 15.469-8.9297 25-25.438 25-43.301v-1e3c0-13.262-5.2695-25.98-14.645-35.355s-22.094-14.645-35.355-14.645zm-200 600h-800c-13.262 0-25.98 5.2695-35.355 14.645s-14.645 22.094-14.645 35.355v250c0 13.262 5.2695 25.98 14.645 35.355s22.094 14.645 35.355 14.645h800c13.262 0 25.98-5.2695 35.355-14.645s14.645-22.094 14.645-35.355v-250c0-13.262-5.2695-25.98-14.645-35.355s-22.094-14.645-35.355-14.645z" fill="#7f7f79"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1,007 B |
BIN
media/img/reader/position_stretch.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
|
@ -1026,6 +1026,8 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
|
|||
|
||||
refresh_feed: function(feed_id, callback) {
|
||||
var self = this;
|
||||
var feed = this.feeds.get(feed_id);
|
||||
if (!feed) return;
|
||||
|
||||
var pre_callback = function(data) {
|
||||
// NEWSBLUR.log(['refresh_feed pre_callback', data]);
|
||||
|
@ -1037,7 +1039,7 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
|
|||
this.make_request('/reader/feed/'+feed_id,
|
||||
{
|
||||
page: 0,
|
||||
feed_address: this.feeds.get(feed_id).get('feed_address')
|
||||
feed_address: feed.get('feed_address')
|
||||
}, pre_callback,
|
||||
null,
|
||||
{
|
||||
|
@ -1417,7 +1419,9 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
|
|||
if (setting == 'read_filter' && _.string.contains(feed_id, 'river:')) {
|
||||
default_setting = 'unread';
|
||||
}
|
||||
return feed && feed[s] || default_setting;
|
||||
var view_setting = feed && feed[s] || default_setting;
|
||||
// if (view_setting == "magazine") view_setting = "list";
|
||||
return view_setting;
|
||||
}
|
||||
|
||||
var view_settings = _.clone(NEWSBLUR.Preferences.view_settings[feed_id+'']) || {};
|
||||
|
@ -1640,7 +1644,9 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
|
|||
|
||||
save_exception_retry: function(feed_id, callback, error_callback) {
|
||||
var self = this;
|
||||
|
||||
var feed = this.feeds.get(feed_id);
|
||||
if (!feed) return;
|
||||
|
||||
var pre_callback = function(data) {
|
||||
// NEWSBLUR.log(['refresh_feed pre_callback', data]);
|
||||
self.post_refresh_feeds(data, callback);
|
||||
|
@ -1648,8 +1654,8 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
|
|||
|
||||
this.make_request('/rss_feeds/exception_retry', {
|
||||
'feed_id': feed_id,
|
||||
'reset_fetch': !!(this.feeds.get(feed_id).get('has_feed_exception') ||
|
||||
this.feeds.get(feed_id).get('has_page_exception'))
|
||||
'reset_fetch': !!(feed.get('has_feed_exception') ||
|
||||
feed.get('has_page_exception'))
|
||||
}, pre_callback, error_callback);
|
||||
},
|
||||
|
||||
|
|
|
@ -388,7 +388,7 @@ NEWSBLUR.Collections.Folders = Backbone.Collection.extend({
|
|||
}
|
||||
|
||||
var remove_articles = function(str) {
|
||||
words = str.split(" ");
|
||||
var words = str.split(" ");
|
||||
if (words.length <= 1) return str;
|
||||
if (words[0] == 'the') return words.splice(1).join(" ");
|
||||
return str;
|
||||
|
|
|
@ -45,12 +45,14 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({
|
|||
return score_name;
|
||||
},
|
||||
|
||||
content_preview: function(attribute, length) {
|
||||
content_preview: function(attribute, length, preserve_paragraphs) {
|
||||
var content = this.get(attribute);
|
||||
if (!attribute || !content) content = this.story_content();
|
||||
// First do a naive strip, which is faster than rendering which makes network calls
|
||||
content = content && content.replace(/<(?:.|\n)*?>/gm, ' ');
|
||||
content = content && Inflector.stripTags(content);
|
||||
content = content && content.replace(/<p(>| [^>]+>)/ig, '\n\n').replace(/(<([^>]+)>)/ig, ' ')
|
||||
if (preserve_paragraphs) {
|
||||
content = content && _.string.trim(content).replace(/\n{2,}/gm, '<br><br>').replace(/(<br\s*\/?>\s*){3,}/igm, '<br><br>');
|
||||
}
|
||||
content = content && content.replace(/[\u00a0\u200c]/g, ' '); // Invisible space, boo
|
||||
content = content && content.replace(/\s+/gm, ' ');
|
||||
|
||||
|
@ -62,7 +64,6 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({
|
|||
if (this.get('image_urls').length >= index+1) {
|
||||
var url = this.get('image_urls')[index];
|
||||
if (window.location.protocol == 'https:' &&
|
||||
NEWSBLUR.Globals.is_staff &&
|
||||
_.str.startsWith(url, "http://")) {
|
||||
var secure_url = this.get('secure_image_urls')[url];
|
||||
if (secure_url) url = secure_url;
|
||||
|
|
|
@ -205,7 +205,7 @@
|
|||
resize: true
|
||||
});
|
||||
|
||||
if (_.contains(['split', 'list', 'grid'],
|
||||
if (_.contains(['split', 'list', 'grid', 'magazine'],
|
||||
NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
NEWSBLUR.app.story_titles.fill_out();
|
||||
} else {
|
||||
|
@ -397,7 +397,7 @@
|
|||
contentLayoutOptions[story_anchor+'__onclose_start'] = $.rescope(this.toggle_story_titles_pane, this);
|
||||
contentLayoutOptions[story_anchor+'__onopen_start'] = $.rescope(this.toggle_story_titles_pane, this);
|
||||
this.layout.contentLayout = this.$s.$content_pane.layout(contentLayoutOptions);
|
||||
} else if (_.contains(['list', 'grid'],
|
||||
} else if (_.contains(['list', 'grid', 'magazine'],
|
||||
NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
var rightLayoutOptions = {
|
||||
resizeWhileDragging: true,
|
||||
|
@ -529,13 +529,22 @@
|
|||
add_body_classes: function() {
|
||||
this.$s.$body.toggleClass('NB-is-premium', NEWSBLUR.Globals.is_premium);
|
||||
this.$s.$body.toggleClass('NB-is-anonymous', NEWSBLUR.Globals.is_anonymous);
|
||||
this.$s.$body.toggleClass('NB-is-authenticated', NEWSBLUR.Globals.is_authenticated);
|
||||
this.$s.$body.toggleClass('NB-pref-full-width-story', !!this.model.preference('full_width_story'));
|
||||
this.$s.$body.toggleClass('NB-is-authenticated', NEWSBLUR.Globals.is_authenticated);
|
||||
if (!!this.model.preference('full_width_story')) {
|
||||
this.model.preference('story_position', 'stretch');
|
||||
this.model.preference('full_width_story', false); // Turn off to ignore and deprecate
|
||||
}
|
||||
this.$s.$body.removeClass('NB-pref-story-position-stretch')
|
||||
.removeClass('NB-pref-story-position-left')
|
||||
.removeClass('NB-pref-story-position-center')
|
||||
.removeClass('NB-pref-story-position-right')
|
||||
.toggleClass('NB-pref-story-position-' + this.model.preference('story_position'));
|
||||
this.$s.$body.toggleClass('NB-dashboard-columns-single', this.model.preference('dashboard_columns') == 1);
|
||||
this.$s.$body.removeClass('NB-story-layout-full')
|
||||
.removeClass('NB-story-layout-split')
|
||||
.removeClass('NB-story-layout-list')
|
||||
.removeClass('NB-story-layout-grid')
|
||||
.removeClass('NB-story-layout-magazine')
|
||||
.addClass('NB-story-layout-'+NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'));
|
||||
},
|
||||
|
||||
|
@ -590,7 +599,7 @@
|
|||
var self = this;
|
||||
percentage = percentage || 0;
|
||||
seconds = parseFloat(Math.max(1, parseInt(seconds, 10)), 10);
|
||||
|
||||
var time;
|
||||
if (percentage > 90) {
|
||||
time = seconds;
|
||||
} else if (percentage > 80) {
|
||||
|
@ -942,7 +951,7 @@
|
|||
page_in_story: function(amount, direction) {
|
||||
amount = parseInt(amount, 10) / 100.0;
|
||||
var page_height = this.$s.$story_pane.height();
|
||||
if (_.contains(['list', 'grid'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
if (_.contains(['list', 'grid', 'magazine'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
page_height = this.$s.$story_titles.height();
|
||||
}
|
||||
var scroll_height = parseInt(page_height * amount, 10);
|
||||
|
@ -983,7 +992,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if (_.contains(['list', 'grid'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
} else if (_.contains(['list', 'grid', 'magazine'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
var scroll_top = this.$s.$story_titles.scrollTop();
|
||||
var $story = this.active_story.story_title_view.$el;
|
||||
var story_height = $story.height();
|
||||
|
@ -1005,7 +1014,7 @@
|
|||
dir = '-';
|
||||
}
|
||||
// NEWSBLUR.log(['scroll_in_story', this.$s.$story_pane, direction, amount]);
|
||||
if (_.contains(['list', 'grid'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
if (_.contains(['list', 'grid', 'magazine'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
this.$s.$story_titles.stop().scrollTo({
|
||||
top: dir+'='+amount,
|
||||
left:'+=0'
|
||||
|
@ -1623,6 +1632,7 @@
|
|||
var $split = $(".NB-task-layout-split");
|
||||
var $list = $(".NB-task-layout-list");
|
||||
var $grid = $(".NB-task-layout-grid");
|
||||
var $magazine = $(".NB-task-layout-magazine");
|
||||
var $full = $(".NB-task-layout-full");
|
||||
var story_layout = NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout');
|
||||
this.$s.$story_titles[0].className = this.$s.$story_titles[0].className.replace(/ ?NB-layout-\w+/gi, '');
|
||||
|
@ -1637,6 +1647,7 @@
|
|||
$split.removeClass('NB-active');
|
||||
$list.addClass('NB-active');
|
||||
$grid.removeClass('NB-active');
|
||||
$magazine.removeClass('NB-active');
|
||||
} else if (story_layout == 'grid') {
|
||||
$('.NB-taskbar-button.task_view_page').addClass('NB-hidden');
|
||||
$('.NB-taskbar-button.task_view_feed').addClass('NB-first');
|
||||
|
@ -1646,6 +1657,17 @@
|
|||
$split.removeClass('NB-active');
|
||||
$list.removeClass('NB-active');
|
||||
$grid.addClass('NB-active');
|
||||
$magazine.removeClass('NB-active');
|
||||
} else if (story_layout == 'magazine') {
|
||||
$('.NB-taskbar-button.task_view_page').addClass('NB-hidden');
|
||||
$('.NB-taskbar-button.task_view_feed').addClass('NB-first');
|
||||
$('.NB-taskbar-button.task_view_story').addClass('NB-hidden');
|
||||
$('.NB-taskbar-button.task_view_text').addClass('NB-last');
|
||||
$full.removeClass('NB-active');
|
||||
$split.removeClass('NB-active');
|
||||
$list.removeClass('NB-active');
|
||||
$grid.removeClass('NB-active');
|
||||
$magazine.addClass('NB-active');
|
||||
} else if (story_layout == 'split') {
|
||||
if (!this.flags.river_view) {
|
||||
$('.NB-taskbar-button.task_view_page').removeClass('NB-hidden');
|
||||
|
@ -1657,6 +1679,7 @@
|
|||
$split.addClass('NB-active');
|
||||
$list.removeClass('NB-active');
|
||||
$grid.removeClass('NB-active');
|
||||
$magazine.removeClass('NB-active');
|
||||
} else if (story_layout == 'full') {
|
||||
if (!this.flags.river_view) {
|
||||
$('.NB-taskbar-button.task_view_page').removeClass('NB-hidden');
|
||||
|
@ -1668,6 +1691,7 @@
|
|||
$split.removeClass('NB-active');
|
||||
$list.removeClass('NB-active');
|
||||
$grid.removeClass('NB-active');
|
||||
$magazine.removeClass('NB-active');
|
||||
}
|
||||
|
||||
if (_.contains(['starred', 'read'], feed_id)) {
|
||||
|
@ -1700,7 +1724,8 @@
|
|||
this.set_correct_story_view_for_feed();
|
||||
this.apply_resizable_layout({right_side: true});
|
||||
|
||||
if (original_layout == 'grid' || story_layout == 'grid') {
|
||||
if (_.contains(['grid', 'magazine'], original_layout) ||
|
||||
_.contains(['grid', 'magazine'], story_layout)) {
|
||||
NEWSBLUR.app.story_titles.render();
|
||||
}
|
||||
if (story_layout == 'list') {
|
||||
|
@ -1713,6 +1738,11 @@
|
|||
this.active_story.story_title_view.toggle_selected();
|
||||
}
|
||||
NEWSBLUR.app.story_list.clear();
|
||||
} else if (story_layout == 'magazine') {
|
||||
if (this.active_story) {
|
||||
this.active_story.story_title_view.toggle_selected();
|
||||
}
|
||||
NEWSBLUR.app.story_list.clear();
|
||||
} else if (story_layout == 'split') {
|
||||
NEWSBLUR.app.story_list.render();
|
||||
if (this.active_story) {
|
||||
|
@ -1739,7 +1769,7 @@
|
|||
NEWSBLUR.app.story_titles.scroll_to_selected_story();
|
||||
NEWSBLUR.app.story_list.scroll_to_selected_story();
|
||||
NEWSBLUR.app.feed_list.scroll_to_selected();
|
||||
if (_.contains(['split', 'list', 'grid'],
|
||||
if (_.contains(['split', 'list', 'grid', 'magazine'],
|
||||
NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
NEWSBLUR.app.story_titles.fill_out();
|
||||
} else {
|
||||
|
@ -2417,7 +2447,7 @@
|
|||
this.switch_taskbar_view('feed', {skip_save_type: 'page'});
|
||||
NEWSBLUR.app.story_list.show_stories_preference_in_feed_view();
|
||||
}
|
||||
} else if (_.contains(['list', 'grid'],
|
||||
} else if (_.contains(['list', 'grid', 'magazine'],
|
||||
NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout')) &&
|
||||
(this.story_view == 'page' || this.story_view == 'story')) {
|
||||
this.switch_taskbar_view('feed', {skip_save_type: 'layout'});
|
||||
|
@ -2769,7 +2799,7 @@
|
|||
'fade': true
|
||||
};
|
||||
var $content_pane = this.$s.$content_pane;
|
||||
feed_id = this.active_feed;
|
||||
var feed_id = this.active_feed;
|
||||
if (!feed_id) return;
|
||||
if (this.flags['river_view']) {
|
||||
var folder = this.active_folder;
|
||||
|
@ -3114,7 +3144,7 @@
|
|||
} else if ($('.task_view_'+view).hasClass('NB-disabled') ||
|
||||
$('.task_view_'+view).hasClass('NB-hidden')) {
|
||||
return;
|
||||
} else if (_.contains(['list', 'grid'],
|
||||
} else if (_.contains(['list', 'grid', 'magazine'],
|
||||
NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout')) &&
|
||||
_.contains(['page', 'story'], view)) {
|
||||
view = 'feed';
|
||||
|
@ -3193,7 +3223,7 @@
|
|||
|
||||
NEWSBLUR.app.story_list.reset_story_positions();
|
||||
if (!options.resize && this.active_story &&
|
||||
_.contains(['list', 'grid'],
|
||||
_.contains(['list', 'grid', 'magazine'],
|
||||
NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
NEWSBLUR.app.text_tab_view.unload();
|
||||
if (this.active_story.get('selected')) {
|
||||
|
@ -3218,7 +3248,7 @@
|
|||
}
|
||||
} else if (!options.resize && this.active_story &&
|
||||
this.active_story.get('selected') &&
|
||||
_.contains(['list', 'grid'],
|
||||
_.contains(['list', 'grid', 'magazine'],
|
||||
NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
this.active_story.story_title_view.render_inline_story_detail();
|
||||
}
|
||||
|
@ -3244,7 +3274,7 @@
|
|||
var $active = $('.NB-taskbar-view .NB-active');
|
||||
var view;
|
||||
|
||||
if (_.contains(['list', 'grid'],
|
||||
if (_.contains(['list', 'grid', 'magazine'],
|
||||
NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
if (direction == -1) {
|
||||
if ($active.hasClass('task_view_feed')) {
|
||||
|
@ -5110,7 +5140,7 @@
|
|||
NEWSBLUR.app.story_list.show_correct_explainer();
|
||||
|
||||
// NEWSBLUR.log(['Showing correct stories', this.story_view, unread_view_name, $stories_show.length, $stories_hide.length]);
|
||||
if (_.contains(['split', 'list', 'grid'],
|
||||
if (_.contains(['split', 'list', 'grid', 'magazine'],
|
||||
NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
NEWSBLUR.app.story_titles.fill_out();
|
||||
} else {
|
||||
|
@ -5131,6 +5161,7 @@
|
|||
var self = this;
|
||||
feed_id = feed_id || this.active_feed;
|
||||
var feed = this.model.get_feed(feed_id);
|
||||
if (!feed) return;
|
||||
feed.set({
|
||||
fetched_once: false,
|
||||
has_exception: false
|
||||
|
@ -6733,6 +6764,10 @@
|
|||
e.preventDefault();
|
||||
self.switch_story_layout('grid');
|
||||
});
|
||||
$.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-layout-magazine' }, function($t, $p){
|
||||
e.preventDefault();
|
||||
self.switch_story_layout('magazine');
|
||||
});
|
||||
$.targetIs(e, { tagSelector: '.NB-taskbar-options' }, function($t, $p){
|
||||
e.preventDefault();
|
||||
self.open_story_options_popover();
|
||||
|
|
|
@ -470,7 +470,7 @@ var classifier_prototype = {
|
|||
// NEWSBLUR.log(['Make Story', story, feed]);
|
||||
|
||||
// HTML entities decoding.
|
||||
story_title = _.string.trim($('<div/>').html(story.get('story_title')).text());
|
||||
var story_title = _.string.trim($('<div/>').html(story.get('story_title')).text());
|
||||
|
||||
this.$modal = $.make('div', { className: 'NB-modal-classifiers NB-modal' }, [
|
||||
$.make('div', { className: 'NB-modal-loading' }),
|
||||
|
|
|
@ -198,6 +198,13 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
|
|||
$.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_grid_active.png' }),
|
||||
$.make("div", { className: "NB-layout-title" }, "Grid")
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('label', { 'for': 'NB-preference-layout-5' }, [
|
||||
$.make('input', { id: 'NB-preference-layout-5', type: 'radio', name: 'story_layout', value: 'magazine' }),
|
||||
$.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_magazine_active.png' }),
|
||||
$.make("div", { className: "NB-layout-title" }, "Magazine")
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
|
|
|
@ -382,6 +382,13 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
|
|||
$.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_grid_active.png' }),
|
||||
$.make("div", { className: "NB-layout-title" }, "Grid")
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('label', { 'for': 'NB-preference-layout-5' }, [
|
||||
$.make('input', { id: 'NB-preference-layout-5', type: 'radio', name: 'story_layout', value: 'magazine' }),
|
||||
$.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_magazine_active.png' }),
|
||||
$.make("div", { className: "NB-layout-title" }, "Magazine")
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
|
@ -759,25 +766,6 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
|
|||
'Open links'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-fullwidthdstory' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-fullwidthdstory-1', type: 'radio', name: 'full_width_story', value: "false" }),
|
||||
$.make('label', { 'for': 'NB-preference-fullwidthdstory-1' }, [
|
||||
'Wrap story content at 700px to ease reading'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-fullwidthdstory-2', type: 'radio', name: 'full_width_story', value: "true" }),
|
||||
$.make('label', { 'for': 'NB-preference-fullwidthdstory-2' }, [
|
||||
'Wrap story content at the edge of the screen'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Story width'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-truncatestory' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
|
@ -1131,12 +1119,6 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
|
|||
return false;
|
||||
}
|
||||
});
|
||||
$('input[name=full_width_story]', $modal).each(function() {
|
||||
if ($(this).val() == ""+NEWSBLUR.Preferences.full_width_story) {
|
||||
$(this).prop('checked', true);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
$('input[name=truncate_story]', $modal).each(function() {
|
||||
if ($(this).val() == NEWSBLUR.Preferences.truncate_story) {
|
||||
$(this).prop('checked', true);
|
||||
|
|
|
@ -43,7 +43,7 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({
|
|||
find_story_in_feed_iframe: function(story) {
|
||||
if (!story) return $([]);
|
||||
|
||||
$iframe = this.$el.contents();
|
||||
var $iframe = this.$el.contents();
|
||||
var $stories = $([]);
|
||||
|
||||
if (this.flags['iframe_story_locations_fetched'] || story.id in this.cache.iframe_stories) {
|
||||
|
@ -240,7 +240,7 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({
|
|||
var last_story = this.cache.iframe_story_positions[last_story_position];
|
||||
var $last_story;
|
||||
if (last_story) {
|
||||
$last_story = this.find_story_in_feed_iframe(last_story, $iframe);
|
||||
$last_story = this.find_story_in_feed_iframe(last_story);
|
||||
}
|
||||
// NEWSBLUR.log(['last_story', last_story_index, last_story_position, last_story, $last_story]);
|
||||
var last_story_same_position;
|
||||
|
@ -258,7 +258,7 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({
|
|||
NEWSBLUR.assets.stories.any(_.bind(function(story, i) {
|
||||
if (last_story_same_position && i < last_story_index) return true;
|
||||
|
||||
var $story = this.find_story_in_feed_iframe(story, $iframe);
|
||||
var $story = this.find_story_in_feed_iframe(story);
|
||||
// NEWSBLUR.log(['Pre-fetching', i, last_story_index, last_story_same_position, $story, story.get('story_title')]);
|
||||
if (!$story ||
|
||||
!$story.length ||
|
||||
|
@ -308,9 +308,9 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({
|
|||
});
|
||||
|
||||
NEWSBLUR.assets.stories.any(_.bind(function(story, i) {
|
||||
if (story.get('story_feed_id') == NEWSBLUR.reader.active_feed ||
|
||||
"social:" + story.get('social_user_id') == NEWSBLUR.reader.active_feed) {
|
||||
var $story = this.find_story_in_feed_iframe(story, $iframe);
|
||||
if ((story.get('story_feed_id') == NEWSBLUR.reader.active_feed ||
|
||||
"social:" + story.get('social_user_id') == NEWSBLUR.reader.active_feed)) {
|
||||
var $story = this.find_story_in_feed_iframe(story);
|
||||
// NEWSBLUR.log(['Fetching story', i, story.get('story_title'), $story]);
|
||||
|
||||
if (self.cache['story_misses'] > 5) {
|
||||
|
@ -661,4 +661,4 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({
|
|||
}
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -250,7 +250,10 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({
|
|||
<% } %>\
|
||||
</div>\
|
||||
<% if (story.get("starred_date")) { %>\
|
||||
<span class="NB-feed-story-starred-date"><%= story.get("starred_date") %></span>\
|
||||
<div class="NB-feed-story-starred-date">\
|
||||
<span class="NB-icon">Saved: </span>\
|
||||
<%= story.get("starred_date") %>\
|
||||
</div >\
|
||||
<% } %>\
|
||||
</div>\
|
||||
</div>\
|
||||
|
@ -690,7 +693,7 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({
|
|||
href = footnote_href;
|
||||
var offset = $(href).offset().top;
|
||||
var $scroll;
|
||||
if (_.contains(['list', 'grid'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
if (_.contains(['list', 'grid', 'magazine'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
$scroll = NEWSBLUR.reader.$s.$story_titles;
|
||||
} else if (NEWSBLUR.reader.flags['temporary_story_view'] ||
|
||||
NEWSBLUR.reader.story_view == 'text') {
|
||||
|
@ -998,7 +1001,7 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
scroll_to_comments: function() {
|
||||
if (_.contains(['list', 'grid'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
if (_.contains(['list', 'grid', 'magazine'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
NEWSBLUR.app.story_titles.scroll_to_selected_story(this.model, {
|
||||
scroll_to_comments: true,
|
||||
scroll_offset: -50
|
||||
|
|
|
@ -23,7 +23,9 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({
|
|||
"click .NB-view-setting-option": "change_view_setting",
|
||||
"click .NB-line-spacing-option": "change_line_spacing",
|
||||
"click .NB-story-titles-pane-option": "change_story_titles_pane",
|
||||
"click .NB-story-position-option": "change_story_position",
|
||||
"click .NB-single-story-option": "change_single_story",
|
||||
"click .NB-story-": "change_story_position",
|
||||
"click .NB-grid-columns-option": "change_grid_columns",
|
||||
"click .NB-grid-height-option": "change_grid_height",
|
||||
"click .NB-premium-link": "open_premium_modal"
|
||||
|
@ -74,8 +76,21 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({
|
|||
$.make('div', { className: 'NB-icon' }),
|
||||
'Single Story'
|
||||
])
|
||||
]),
|
||||
$.make('ul', { className: 'segmented-control NB-options-story-position' }, [
|
||||
$.make('li', { className: 'NB-story-position-option NB-options-story-position-stretch', role: "button" }, [
|
||||
'Full width'
|
||||
]),
|
||||
$.make('li', { className: 'NB-story-position-option NB-options-story-position-left', role: "button" }, [
|
||||
$.make('div', { className: 'NB-icon' })
|
||||
]),
|
||||
$.make('li', { className: 'NB-story-position-option NB-options-story-position-center NB-active', role: "button" }, [
|
||||
$.make('div', { className: 'NB-icon' })
|
||||
]),
|
||||
$.make('li', { className: 'NB-story-position-option NB-options-story-position-right NB-active', role: "button" }, [
|
||||
$.make('div', { className: 'NB-icon' })
|
||||
])
|
||||
])
|
||||
|
||||
]),
|
||||
$.make('div', { className: 'NB-popover-section' }, [
|
||||
$.make('div', { className: 'NB-popover-section-title' }, 'Story Layout - Grid Columns'),
|
||||
|
@ -138,8 +153,8 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({
|
|||
])
|
||||
]),
|
||||
(!NEWSBLUR.Globals.is_premium && $.make('div', { className: 'NB-premium-explainer' }, [
|
||||
'Premium fonts require a ',
|
||||
$.make('spam', { className: 'NB-splash-link NB-premium-link' }, 'premium account')
|
||||
'Premium fonts require a ',
|
||||
$.make('span', { className: 'NB-splash-link NB-premium-link' }, 'premium account')
|
||||
]))
|
||||
]),
|
||||
$.make('div', { className: 'NB-popover-section' }, [
|
||||
|
@ -204,6 +219,7 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({
|
|||
var grid_height = NEWSBLUR.assets.preference('grid_height');
|
||||
var image_preview = NEWSBLUR.assets.preference('image_preview');
|
||||
var content_preview = NEWSBLUR.assets.preference('show_content_preview');
|
||||
var story_position = NEWSBLUR.assets.preference('story_position');
|
||||
|
||||
this.$('.NB-font-family-option').removeClass('NB-active');
|
||||
this.$('.NB-options-font-family-'+font_family).addClass('NB-active');
|
||||
|
@ -216,6 +232,8 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({
|
|||
this.$('.NB-line-spacing-option').removeClass('NB-active');
|
||||
this.$('.NB-options-line-spacing-'+line_spacing).addClass('NB-active');
|
||||
|
||||
this.$('.NB-story-position-option').removeClass('NB-active');
|
||||
this.$('.NB-options-story-position-'+story_position).addClass('NB-active');
|
||||
this.$('.NB-story-titles-pane-option').removeClass('NB-active');
|
||||
this.$('.NB-options-story-titles-pane-'+titles_layout_pane).addClass('NB-active');
|
||||
this.$('.NB-single-story-option').removeClass('NB-active');
|
||||
|
@ -373,6 +391,27 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({
|
|||
NEWSBLUR.assets.preference('story_titles_pane_size', pane_size);
|
||||
NEWSBLUR.reader.apply_resizable_layout({right_side: true});
|
||||
},
|
||||
|
||||
change_story_position: function(e) {
|
||||
var $target = $(e.currentTarget);
|
||||
|
||||
if ($target.hasClass("NB-options-story-position-stretch")) {
|
||||
this.update_story_position('stretch');
|
||||
} else if ($target.hasClass("NB-options-story-position-left")) {
|
||||
this.update_story_position('left');
|
||||
} else if ($target.hasClass("NB-options-story-position-center")) {
|
||||
this.update_story_position('center');
|
||||
} else if ($target.hasClass("NB-options-story-position-right")) {
|
||||
this.update_story_position('right');
|
||||
}
|
||||
|
||||
this.show_correct_options();
|
||||
},
|
||||
|
||||
update_story_position: function(setting) {
|
||||
NEWSBLUR.assets.preference('story_position', setting);
|
||||
NEWSBLUR.reader.add_body_classes();
|
||||
},
|
||||
|
||||
change_single_story: function(e) {
|
||||
var $target = $(e.currentTarget);
|
||||
|
|
|
@ -27,6 +27,7 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({
|
|||
render: function() {
|
||||
var template_name = !this.model.get('selected') && this.options.is_grid ?
|
||||
'grid_template' : 'template';
|
||||
if (this.options.is_magazine) template_name = "magazine_template";
|
||||
// console.log(['render story title', template_name, this.$el[0], this.options.is_grid, this.show_image_preview(), this.options.override_layout, NEWSBLUR.assets.get_feed(this.model.get('story_feed_id'))]);
|
||||
this.$el.html(this[template_name]({
|
||||
story : this.model,
|
||||
|
@ -35,7 +36,7 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({
|
|||
NEWSBLUR.reader.flags.social_view) &&
|
||||
NEWSBLUR.assets.get_feed(this.model.get('story_feed_id')),
|
||||
options : this.options,
|
||||
show_content_preview : this.show_content_preview(),
|
||||
show_content_preview : this.show_content_preview(template_name),
|
||||
show_image_preview : this.show_image_preview()
|
||||
}));
|
||||
this.$st = this.$(".NB-story-title");
|
||||
|
@ -45,7 +46,7 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({
|
|||
this.load_youtube_embeds();
|
||||
var story_layout = this.options.override_layout || NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout');
|
||||
if (this.options.is_grid) this.watch_grid_image();
|
||||
if (_.contains(['list'], story_layout) && this.show_image_preview()) this.watch_grid_image();
|
||||
if (_.contains(['list', 'magazine'], story_layout) && this.show_image_preview()) this.watch_grid_image();
|
||||
if (_.contains(['split'], story_layout) && this.show_image_preview() && NEWSBLUR.assets.preference('feed_view_single_story')) this.watch_grid_image();
|
||||
|
||||
return this;
|
||||
|
@ -97,7 +98,7 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({
|
|||
<div class="NB-storytitles-feed-border-outer"></div>\
|
||||
<% if (story.image_url()) { %>\
|
||||
<div class="NB-storytitles-story-image-container">\
|
||||
<div class="NB-storytitles-story-image"></div>\
|
||||
<div class="NB-storytitles-story-image" <% if (story.image_url()) { %>style="background-image: none, url(\'<%= story.image_url() %>\'); display: block;"<% } %>></div>\
|
||||
</div>\
|
||||
<% } %>\
|
||||
<div class="NB-storytitles-content">\
|
||||
|
@ -133,6 +134,48 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({
|
|||
<div class="NB-story-detail"></div>\
|
||||
'),
|
||||
|
||||
magazine_template: _.template('\
|
||||
<div class="NB-story-title <% if (!show_content_preview) { %>NB-story-title-hide-preview<% } %>">\
|
||||
<div class="NB-storytitles-feed-border-inner"></div>\
|
||||
<div class="NB-storytitles-feed-border-outer"></div>\
|
||||
<% if (story.image_url()) { %>\
|
||||
<div class="NB-storytitles-story-image-container">\
|
||||
<div class="NB-storytitles-story-image" <% if (story.image_url()) { %>style="background-image: none, url(\'<%= story.image_url() %>\');"<% } %>></div>\
|
||||
</div>\
|
||||
<% } %>\
|
||||
<div class="NB-storytitles-content">\
|
||||
<% if (feed) { %>\
|
||||
<div class="NB-story-feed">\
|
||||
<img class="feed_favicon" src="<%= $.favicon(feed) %>">\
|
||||
<span class="feed_title"><%= feed.get("feed_title") %></span>\
|
||||
</div>\
|
||||
<% } %>\
|
||||
<a href="<%= story.get("story_permalink") %>" class="story_title NB-hidden-fade">\
|
||||
<div class="NB-storytitles-star"></div>\
|
||||
<div class="NB-storytitles-share"></div>\
|
||||
<div class="NB-storytitles-sentiment"></div>\
|
||||
<div class="NB-story-manage-icon" role="button"></div>\
|
||||
<span class="NB-storytitles-title"><%= story.get("story_title") %></span>\
|
||||
<% if (show_content_preview) { %>\
|
||||
<div class="NB-storytitles-content-preview"><%= show_content_preview %></div>\
|
||||
<% } %>\
|
||||
</a>\
|
||||
</div>\
|
||||
<div class="NB-storytitles-magazine-bottom">\
|
||||
<span class="NB-storytitles-author"><%= story.story_authors() %></span>\
|
||||
<span class="story_date NB-hidden-fade"><%= story.formatted_short_date() %></span>\
|
||||
</div>\
|
||||
<% if (story.get("comment_count_friends")) { %>\
|
||||
<div class="NB-storytitles-shares">\
|
||||
<% _.each(story.get("commented_by_friends"), function(user_id) { %>\
|
||||
<img class="NB-user-avatar" src="<%= NEWSBLUR.assets.user_profiles.find(user_id).get("photo_url") %>">\
|
||||
<% }) %>\
|
||||
</div>\
|
||||
<% } %>\
|
||||
</div>\
|
||||
<div class="NB-story-detail"></div>\
|
||||
'),
|
||||
|
||||
render_inline_story_detail: function(temporary_text) {
|
||||
// console.log(['render_inline_story_detail', this.model.get('story_title')]);
|
||||
if (NEWSBLUR.reader.story_view == 'text' || temporary_text) {
|
||||
|
@ -159,6 +202,10 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({
|
|||
}
|
||||
},
|
||||
|
||||
render_magazine_story_detail: function () {
|
||||
this.render_inline_story_detail();
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
// console.log(["destroy story title", this.model.get('story_title')]);
|
||||
if (this.text_view) {
|
||||
|
@ -204,15 +251,15 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({
|
|||
}
|
||||
},
|
||||
|
||||
show_content_preview: function() {
|
||||
show_content_preview: function(template_name) {
|
||||
var preference = NEWSBLUR.assets.preference('show_content_preview');
|
||||
if (!preference) return preference;
|
||||
var max_length = preference == 'small' ? 300 : preference == 'medium' ? 600 : 1000;
|
||||
|
||||
if (this.options.override_layout == 'grid' ||
|
||||
NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout') == 'grid') {
|
||||
if (_.contains(['grid_template', 'magazine_template'], template_name)) {
|
||||
max_length = preference == 'small' ? 500 : preference == 'medium' ? 1000 : 1500;
|
||||
return this.model.content_preview('story_content', max_length) || " ";
|
||||
var preserve_paragraphs = true;
|
||||
return this.model.content_preview('story_content', max_length, preserve_paragraphs) || " ";
|
||||
}
|
||||
var pruned_description = this.model.content_preview('story_content', max_length) || " ";
|
||||
var pruned_title = this.model.content_preview('story_title');
|
||||
|
@ -232,7 +279,7 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({
|
|||
var story_layout = this.options.override_layout ||
|
||||
NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout');
|
||||
var pane_anchor = this.options.override_layout ? "west" : NEWSBLUR.assets.preference('story_pane_anchor');
|
||||
if (_.contains(['list', 'grid'], story_layout)) return true;
|
||||
if (_.contains(['list', 'grid', 'magazine'], story_layout)) return true;
|
||||
if (story_layout == 'split' && _.contains(['north', 'south'], pane_anchor)) return true;
|
||||
|
||||
return !!this.model.image_url();
|
||||
|
@ -315,10 +362,10 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({
|
|||
this.select_regex(e, text) ||
|
||||
this.select_regex(d, text);
|
||||
if (i) {
|
||||
this.$(".NB-storytitles-story-image").css({
|
||||
'display': 'block',
|
||||
'background-image': "url("+NEWSBLUR.Globals.MEDIA_URL+"img/reader/youtube_play.png), url(" + "https://img.youtube.com/vi/" + i + "/0.jpg" + ")"
|
||||
});
|
||||
// this.$(".NB-storytitles-story-image").css({
|
||||
// 'display': 'block',
|
||||
// 'background-image': "url("+NEWSBLUR.Globals.MEDIA_URL+"img/reader/youtube_play.png), url(" + "https://img.youtube.com/vi/" + i + "/0.jpg" + ")"
|
||||
// });
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
@ -381,16 +428,19 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({
|
|||
}
|
||||
},
|
||||
|
||||
toggle_selected: function(model, selected, options) {
|
||||
toggle_selected: function (model, selected, options) {
|
||||
var story_layout = this.options.override_layout ||
|
||||
NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout');
|
||||
if (this.options.is_grid) this.render();
|
||||
|
||||
this.$st.toggleClass('NB-selected', !!this.model.get('selected'));
|
||||
this.$el.toggleClass('NB-selected', !!this.model.get('selected'));
|
||||
|
||||
if (!!this.model.get('selected')) {
|
||||
if (_.contains(['list', 'grid'], this.options.override_layout ||
|
||||
NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) {
|
||||
if (_.contains(['list', 'grid'], story_layout)) {
|
||||
this.render_inline_story_detail();
|
||||
} else if (_.contains(['magazine'], story_layout)) {
|
||||
this.render_magazine_story_detail();
|
||||
} else {
|
||||
this.destroy_inline_story_detail();
|
||||
}
|
||||
|
@ -479,7 +529,7 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({
|
|||
return;
|
||||
}
|
||||
|
||||
if (_.contains(['list', 'grid'], this.options.override_layout ||
|
||||
if (_.contains(['list', 'grid', 'magazine'], this.options.override_layout ||
|
||||
NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout')) &&
|
||||
this.model.get('selected')) {
|
||||
this.collapse_story();
|
||||
|
|
|
@ -40,6 +40,7 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({
|
|||
model: story,
|
||||
collection: collection,
|
||||
is_grid: story_layout == 'grid',
|
||||
is_magazine: story_layout == 'magazine',
|
||||
override_layout: override_layout,
|
||||
on_dashboard: on_dashboard
|
||||
}).render();
|
||||
|
@ -71,6 +72,7 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({
|
|||
model: story,
|
||||
collection: collection,
|
||||
is_grid: story_layout == 'grid',
|
||||
is_magazine: story_layout == 'magazine',
|
||||
override_layout: override_layout,
|
||||
on_dashboard: on_dashboard
|
||||
}).render();
|
||||
|
@ -256,7 +258,7 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({
|
|||
]);
|
||||
var story_layout = this.options.override_layout ||
|
||||
NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout');
|
||||
if (_.contains(['list', 'grid'], story_layout) || NEWSBLUR.assets.preference('mark_read_on_scroll_titles')) {
|
||||
if (_.contains(['list', 'grid', 'magazine'], story_layout) || NEWSBLUR.assets.preference('mark_read_on_scroll_titles')) {
|
||||
var pane_height = this.$story_titles.height();
|
||||
var endbar_height = 20;
|
||||
var last_story_height = 80;
|
||||
|
@ -308,7 +310,7 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({
|
|||
// console.log(["scroll_to_selected_story 1", story, options]);
|
||||
var story_title_visisble = this.$story_titles.isScrollVisible(story_title_view.$el);
|
||||
if (!story_title_visisble || options.force ||
|
||||
_.contains(['list', 'grid'], story_layout)) {
|
||||
_.contains(['list', 'grid', 'magazine'], story_layout)) {
|
||||
var container_offset = this.$story_titles.position().top;
|
||||
var scroll = story_title_view.$el.find('.NB-story-title').position().top;
|
||||
if (options.scroll_to_comments) {
|
||||
|
@ -318,7 +320,7 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({
|
|||
var height = this.$story_titles.outerHeight();
|
||||
var position = scroll+container-height/5;
|
||||
// console.log(["scroll_to_selected_story 2", container_offset, scroll, container, height, position]);
|
||||
if (_.contains(['list', 'grid'], story_layout)) {
|
||||
if (_.contains(['list', 'grid', 'magazine'], story_layout)) {
|
||||
position = scroll+container;
|
||||
}
|
||||
if (story_layout == 'grid') {
|
||||
|
|
6
media/js/vendor/inflector.js
vendored
|
@ -138,8 +138,8 @@ window.Inflector = {
|
|||
var numberMatcher = /(\d+)(\d{3})/;
|
||||
number += '';
|
||||
var fragments = number.split('.');
|
||||
whole = fragments[0];
|
||||
decimal = fragments.length > 1 ? '.' + fragments[1] : '';
|
||||
var whole = fragments[0];
|
||||
var decimal = fragments.length > 1 ? '.' + fragments[1] : '';
|
||||
while (numberMatcher.test(whole)) {
|
||||
whole = whole.replace(numberMatcher, '$1,$2');
|
||||
}
|
||||
|
@ -175,4 +175,4 @@ window.Inflector = {
|
|||
return s.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1');
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
|
2
media/js/vendor/jquery.layout.js
vendored
2
media/js/vendor/jquery.newsblur.js
vendored
|
@ -352,7 +352,7 @@ NEWSBLUR.log = function(msg) {
|
|||
if(opts.cancelBubbling){
|
||||
fails = true;
|
||||
}else{
|
||||
$tp = $t.closest(ts);
|
||||
var $tp = $t.closest(ts);
|
||||
if(!$tp.length){
|
||||
fails = true;
|
||||
}else{
|
||||
|
|
|
@ -26,7 +26,7 @@ DEBUG = True
|
|||
# DEBUG_ASSETS controls JS/CSS asset packaging. Turning this off requires you to run
|
||||
# `./manage.py collectstatic` first. Turn this on for development so you can see
|
||||
# changes in your JS/CSS.
|
||||
DEBUG_ASSETS = False # Make sure to run `./manage.py collectstatic` first
|
||||
DEBUG_ASSETS = False # Make sure to run `./manage.py collectstatic` first
|
||||
DEBUG_ASSETS = True
|
||||
|
||||
# DEBUG_QUERIES controls the output of the database query logs. Can be rather verbose
|
||||
|
|
|
@ -812,7 +812,7 @@ PIPELINE = {
|
|||
'JS_COMPRESSOR': 'pipeline.compressors.closure.ClosureCompressor',
|
||||
# 'CSS_COMPRESSOR': 'pipeline.compressors.NoopCompressor',
|
||||
# 'JS_COMPRESSOR': 'pipeline.compressors.NoopCompressor',
|
||||
'CLOSURE_BINARY': '/usr/bin/java -jar node_modules/google-closure-compiler-java/compiler.jar',
|
||||
'CLOSURE_BINARY': '/usr/bin/java -jar /usr/local/bin/compiler.jar',
|
||||
'CLOSURE_ARGUMENTS': '--language_in ECMASCRIPT_2016 --language_out ECMASCRIPT_2016 --warning_level DEFAULT',
|
||||
'JAVASCRIPT': {
|
||||
'common': {
|
||||
|
|
|
@ -86,6 +86,7 @@
|
|||
'show_global_shared_stories': true,
|
||||
'show_infrequent_site_stories': true,
|
||||
'full_width_story' : false,
|
||||
'story_position' : 'center',
|
||||
'truncate_story' : 'social',
|
||||
'autoopen_folder' : false,
|
||||
'show_content_preview' : true,
|
||||
|
|
|
@ -266,6 +266,10 @@
|
|||
<div class="NB-task-image"></div>
|
||||
<span class="NB-task-title">List</span>
|
||||
</li>
|
||||
<li class="NB-taskbar-button NB-task-layout-magazine NB-tipsy" tipsy-title="Magazine">
|
||||
<div class="NB-task-image"></div>
|
||||
<span class="NB-task-title">Magazine</span>
|
||||
</li>
|
||||
<li class="NB-taskbar-button NB-task-layout-grid NB-tipsy" tipsy-title="Grid">
|
||||
<div class="NB-task-image"></div>
|
||||
<span class="NB-task-title">Grid</span>
|
||||
|
|
|
@ -48,8 +48,8 @@
|
|||
endpoint to begin the OAuth authentication procedure. Use the <code>/oauth/token</code>
|
||||
endpoint to receive your authorized token, which you can pass in with every request.</p>
|
||||
|
||||
<p>You can not required to use OAuth, in which case you will need to pass the <code>newsblur_sessionid</code>
|
||||
cookie with every request. This cookie is set on login.</p>
|
||||
<p>You are not required to use OAuth, in which case you will need to pass the <code>newsblur_sessionid</code>
|
||||
cookie with every request. This cookie is set on <a href="#/api/login">login</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -463,27 +463,13 @@
|
|||
desc: "JSON serialized dictionary of user_ids to feed_ids to an array of story_ids."
|
||||
required: true
|
||||
example: "{<br>user_id: <br>{feed_id: ['story_id_1', 'story_id_2'],<br>24: ['story_id_3']<br>}<br>}"
|
||||
|
||||
- url: /reader/mark_story_as_unread
|
||||
method: POST
|
||||
short_desc: "Mark a story as unread."
|
||||
long_desc:
|
||||
- "Mark a story as unread."
|
||||
params:
|
||||
- key: story_id
|
||||
desc: "Story id to mark unread."
|
||||
required: true
|
||||
example: "http://www.ofbrooklyn.com/story-title"
|
||||
- key: feed_id
|
||||
desc: "Feed id that the story is from."
|
||||
required: true
|
||||
example: "42"
|
||||
|
||||
- url: /reader/mark_story_hash_as_unread
|
||||
method: POST
|
||||
short_desc: "Mark a story as unread."
|
||||
long_desc:
|
||||
- "Mark a single story as unread using its unique story_hash."
|
||||
- "Mark a single story or multiple stories as unread using its unique story_hash."
|
||||
- "Multiple story hashes can be sent at once."
|
||||
- "Premium users can mark up to 30 days as unread. Free users only have 15 days."
|
||||
- >
|
||||
If a story is too old to mark as read, look for (and display to the user) the
|
||||
|
@ -499,21 +485,6 @@
|
|||
example: >
|
||||
story_hash=123:a1d62b
|
||||
|
||||
- url: /reader/mark_story_as_starred
|
||||
method: POST
|
||||
short_desc: "Mark a story as starred (saved)."
|
||||
long_desc:
|
||||
- "Mark a story as starred (saved)."
|
||||
params:
|
||||
- key: story_id
|
||||
desc: "Story id to save."
|
||||
required: true
|
||||
example: "64"
|
||||
- key: feed_id
|
||||
desc: "Feed id that the story is from."
|
||||
required: true
|
||||
example: "42"
|
||||
|
||||
- url: /reader/mark_story_hash_as_starred
|
||||
method: POST
|
||||
short_desc: "Mark a story as starred (saved)."
|
||||
|
@ -535,26 +506,12 @@
|
|||
tips:
|
||||
- "You can send a list for user_tags and highlights. <code>user_tags[]=blog&user_tags[]=cooking</code>"
|
||||
|
||||
- url: /reader/mark_story_as_unstarred
|
||||
method: POST
|
||||
short_desc: "Mark a story as unstarred (unsaved)."
|
||||
long_desc:
|
||||
- "Mark a story as unstarred (unsaved)."
|
||||
params:
|
||||
- key: story_id
|
||||
desc: "Story id to unsave."
|
||||
required: true
|
||||
example: "64"
|
||||
- key: feed_id
|
||||
desc: "Feed id that the story is from."
|
||||
required: true
|
||||
example: "42"
|
||||
|
||||
- url: /reader/mark_story_hash_as_unstarred
|
||||
method: POST
|
||||
short_desc: "Mark a story as unstarred (unsaved)."
|
||||
long_desc:
|
||||
- "Mark a story as unstarred (unsaved)."
|
||||
- "Multiple story hashes can be sent at once."
|
||||
params:
|
||||
- key: story_hash
|
||||
desc: "Story to unsave, specified by hash."
|
||||
|
|
|
@ -7,7 +7,6 @@ import urllib.request, urllib.parse, urllib.error
|
|||
import urllib.parse
|
||||
import random
|
||||
import warnings
|
||||
from django.core.mail import mail_admins
|
||||
from django.utils.translation import ungettext
|
||||
from django.utils.encoding import smart_str
|
||||
from utils import log as logging
|
||||
|
@ -41,8 +40,8 @@ def timelimit(timeout):
|
|||
raise TimeoutError('took too long')
|
||||
if c.error:
|
||||
tb = ''.join(traceback.format_exception(c.exc_info[0], c.exc_info[1], c.exc_info[2]))
|
||||
logging.debug(tb)
|
||||
mail_admins('Error in timeout: %s' % c.exc_info[0], tb)
|
||||
logging.debug(f" ***> Traceback timeout error: {tb}")
|
||||
# mail_admins('Error in timeout: %s' % c.exc_info[0], tb)
|
||||
raise c.error
|
||||
return c.result
|
||||
return _2
|
||||
|
@ -221,8 +220,7 @@ def mail_feed_error_to_admin(feed, e, local_vars=None, subject=None):
|
|||
pprint.pformat(feed.__dict__),
|
||||
pprint.pformat(local_vars)
|
||||
)
|
||||
# print message
|
||||
mail_admins(subject, message)
|
||||
logging.debug(f" ***> Feed error, {subject}: {message}")
|
||||
|
||||
## {{{ http://code.activestate.com/recipes/576611/ (r11)
|
||||
from operator import itemgetter
|
||||
|
|
|
@ -7,11 +7,11 @@ from decimal import Decimal
|
|||
from django.core import serializers
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse, HttpResponseForbidden, Http404
|
||||
from django.core.mail import mail_admins
|
||||
from django.db.models.query import QuerySet
|
||||
# from django.utils.deprecation import CallableBool
|
||||
from mongoengine.queryset.queryset import QuerySet as MongoQuerySet
|
||||
from bson.objectid import ObjectId
|
||||
from utils import log as logging
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
|
@ -161,7 +161,8 @@ def json_response(request, response=None):
|
|||
'text': str(e)}
|
||||
code = 500
|
||||
if not settings.DEBUG:
|
||||
mail_admins(subject, message, fail_silently=True)
|
||||
logging.debug(f" ***> JSON exception {subject}: {message}")
|
||||
logging.debug('\n'.join(traceback.format_exception(*exc_info)))
|
||||
else:
|
||||
print('\n'.join(traceback.format_exception(*exc_info)))
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ class MongoDumpMiddleware(object):
|
|||
# save old methods
|
||||
setattr(MongoClient, '_logging', True)
|
||||
if hasattr(MongoClient, '_send_message_with_response'):
|
||||
connection.queriesx = []
|
||||
MongoClient._send_message_with_response = \
|
||||
self._instrument(MongoClient._send_message_with_response)
|
||||
MongoReplicaSetClient._send_message_with_response = \
|
||||
|
|
|
@ -19,6 +19,7 @@ class RedisDumpMiddleware(object):
|
|||
if not getattr(Connection, '_logging', False):
|
||||
# save old methods
|
||||
setattr(Connection, '_logging', True)
|
||||
connection.queriesx = []
|
||||
Connection.pack_command = \
|
||||
self._instrument(Connection.pack_command)
|
||||
|
||||
|
|
|
@ -31,8 +31,8 @@ class DumpRequestMiddleware:
|
|||
self.color_db(request.sql_times_elapsed['redis_pubsub'], '~FC'),
|
||||
request.sql_times_elapsed['redis_pubsub'],
|
||||
)
|
||||
logging.user(request, " ---> %s~SN~FCDB times: ~FYsql: %s%.4f~SNs ~SN~FMmongo: %s%.5f~SNs ~SN~FCredis: %s" % (
|
||||
self.elapsed_time(request),
|
||||
logging.user(request, "~SN~FCDB times ~SB~FK%s~SN~FC: ~FYsql: %s%.4f~SNs ~SN~FMmongo: %s%.5f~SNs ~SN~FCredis: %s" % (
|
||||
request.path,
|
||||
self.color_db(request.sql_times_elapsed['sql'], '~FY'),
|
||||
request.sql_times_elapsed['sql'],
|
||||
self.color_db(request.sql_times_elapsed['mongo'], '~FM'),
|
||||
|
@ -63,8 +63,8 @@ class DumpRequestMiddleware:
|
|||
color = '~SB~FR'
|
||||
elif seconds > .1:
|
||||
color = '~FW'
|
||||
elif seconds == 0:
|
||||
color = '~FK'
|
||||
# elif seconds == 0:
|
||||
# color = '~FK~SB'
|
||||
return color
|
||||
|
||||
def __init__(self, get_response=None):
|
||||
|
|