Merge remote-tracking branch 'upstream/master' into defaultview

This commit is contained in:
Mark Anderson 2014-01-14 22:32:05 +00:00
commit 8a53515a09
58 changed files with 3073 additions and 1222 deletions

View file

@ -19,6 +19,7 @@ urlpatterns = patterns('',
url(r'^feed_unread_count', views.feed_unread_count, name='feed-unread-count'),
url(r'^starred_stories', views.load_starred_stories, name='load-starred-stories'),
url(r'^starred_story_hashes', views.starred_story_hashes, name='starred-story-hashes'),
url(r'^starred_rss/(?P<user_id>\d+)/(?P<secret_token>\w+)/(?P<tag_slug>[-\w]+)?/?$', views.starred_stories_rss_feed, name='starred-stories-rss-feed'),
url(r'^unread_story_hashes', views.unread_story_hashes, name='unread-story-hashes'),
url(r'^mark_all_as_read', views.mark_all_as_read, name='mark-all-as-read'),
url(r'^mark_story_as_read', views.mark_story_as_read, name='mark-story-as-read'),

View file

@ -3,6 +3,7 @@ import time
import boto
import redis
import requests
import zlib
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
@ -20,6 +21,7 @@ from django.core.mail import mail_admins
from django.core.validators import email_re
from django.core.mail import EmailMultiAlternatives
from django.contrib.sites.models import Site
from django.utils import feedgenerator
from mongoengine.queryset import OperationError
from apps.recommendations.models import RecommendedFeed
from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag
@ -29,7 +31,7 @@ from apps.analyzer.models import get_classifiers_for_user, sort_classifiers_by_f
from apps.profile.models import Profile
from apps.reader.models import UserSubscription, UserSubscriptionFolders, RUserStory, Feature
from apps.reader.forms import SignupForm, LoginForm, FeatureForm
from apps.rss_feeds.models import MFeedIcon
from apps.rss_feeds.models import MFeedIcon, MStarredStoryCounts
from apps.statistics.models import MStatistics
# from apps.search.models import SearchStarredStory
try:
@ -257,7 +259,9 @@ def load_feeds(request):
len(scheduled_feeds))
ScheduleImmediateFetches.apply_async(kwargs=dict(feed_ids=scheduled_feeds))
starred_count = MStarredStory.objects(user_id=user.pk).count()
starred_counts, starred_count = MStarredStoryCounts.user_counts(user.pk, include_total=True)
if not starred_count and len(starred_counts):
starred_count = MStarredStory.objects(user_id=user.pk).count()
social_params = {
'user_id': user.pk,
@ -283,6 +287,7 @@ def load_feeds(request):
'user_profile': user.profile,
'folders': json.decode(folders.folders),
'starred_count': starred_count,
'starred_counts': starred_counts,
'categories': categories
}
return data
@ -592,13 +597,16 @@ def load_single_feed(request, feed_id):
starred_stories = MStarredStory.objects(user_id=user.pk,
story_feed_id=feed.pk,
story_hash__in=story_hashes)\
.only('story_hash', 'starred_date')
.only('story_hash', 'starred_date', 'user_tags')
shared_stories = MSharedStory.objects(user_id=user.pk,
story_feed_id=feed_id,
story_hash__in=story_hashes)\
.only('story_hash', 'shared_date', 'comments')
starred_stories = dict([(story.story_hash, story.starred_date) for story in starred_stories])
shared_stories = dict([(story.story_hash, dict(shared_date=story.shared_date, comments=story.comments))
starred_stories = dict([(story.story_hash, dict(starred_date=story.starred_date,
user_tags=story.user_tags))
for story in starred_stories])
shared_stories = dict([(story.story_hash, dict(shared_date=story.shared_date,
comments=story.comments))
for story in shared_stories])
checkpoint4 = time.time()
@ -618,9 +626,10 @@ def load_single_feed(request, feed_id):
story['read_status'] = 0
if story['story_hash'] in starred_stories:
story['starred'] = True
starred_date = localtime_for_timezone(starred_stories[story['story_hash']],
starred_date = localtime_for_timezone(starred_stories[story['story_hash']]['starred_date'],
user.profile.timezone)
story['starred_date'] = format_story_link_date__long(starred_date, now)
story['user_tags'] = starred_stories[story['story_hash']]['user_tags']
if story['story_hash'] in shared_stories:
story['shared'] = True
shared_date = localtime_for_timezone(shared_stories[story['story_hash']]['shared_date'],
@ -742,6 +751,7 @@ def load_starred_stories(request):
limit = int(request.REQUEST.get('limit', 10))
page = int(request.REQUEST.get('page', 0))
query = request.REQUEST.get('query')
tag = request.REQUEST.get('tag')
story_hashes = request.REQUEST.getlist('h')[:100]
now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone)
message = None
@ -751,10 +761,20 @@ def load_starred_stories(request):
# results = SearchStarredStory.query(user.pk, query)
# story_ids = [result.db_id for result in results]
if user.profile.is_premium:
stories = MStarredStory.find_stories(query, user.pk, offset=offset, limit=limit)
stories = MStarredStory.find_stories(query, user.pk, tag=tag, offset=offset, limit=limit)
else:
stories = []
message = "You must be a premium subscriber to search."
elif tag:
if user.profile.is_premium:
mstories = MStarredStory.objects(
user_id=user.pk,
user_tags__contains=tag
).order_by('-starred_date')[offset:offset+limit]
stories = Feed.format_stories(mstories)
else:
stories = []
message = "You must be a premium subscriber to read saved stories by tag."
elif story_hashes:
mstories = MStarredStory.objects(
user_id=user.pk,
@ -831,6 +851,60 @@ def starred_story_hashes(request):
return dict(starred_story_hashes=story_hashes)
def starred_stories_rss_feed(request, user_id, secret_token, tag_slug):
try:
user = User.objects.get(pk=user_id)
except User.DoesNotExist:
raise Http404
try:
tag_counts = MStarredStoryCounts.objects.get(user_id=user_id, slug=tag_slug)
except MStarredStoryCounts.DoesNotExist:
raise Http404
data = {}
data['title'] = "Saved Stories - %s" % tag_counts.tag
data['link'] = "%s%s" % (
settings.NEWSBLUR_URL,
reverse('saved-stories-tag', kwargs=dict(tag_name=tag_slug)))
data['description'] = "Stories saved by %s on NewsBlur with the tag \"%s\"." % (user.username,
tag_counts.tag)
data['lastBuildDate'] = datetime.datetime.utcnow()
data['generator'] = 'NewsBlur - %s' % settings.NEWSBLUR_URL
data['docs'] = None
data['author_name'] = user.username
data['feed_url'] = "%s%s" % (
settings.NEWSBLUR_URL,
reverse('starred-stories-rss-feed',
kwargs=dict(user_id=user_id, secret_token=secret_token, tag_slug=tag_slug)),
)
rss = feedgenerator.Atom1Feed(**data)
starred_stories = MStarredStory.objects(
user_id=user.pk,
user_tags__contains=tag_counts.tag
).order_by('-starred_date')[:25]
for starred_story in starred_stories:
story_data = {
'title': starred_story.story_title,
'link': starred_story.story_permalink,
'description': (starred_story.story_content_z and
zlib.decompress(starred_story.story_content_z)),
'author_name': starred_story.story_author_name,
'categories': starred_story.story_tags,
'unique_id': starred_story.story_guid,
'pubdate': starred_story.starred_date,
}
rss.add_item(**story_data)
logging.user(request, "~FBGenerating ~SB%s~SN's saved story RSS feed (%s, %s stories): ~FM%s" % (
user.username,
tag_counts.tag,
tag_counts.count,
request.META.get('HTTP_USER_AGENT', "")[:24]
))
return HttpResponse(rss.writeString('utf-8'), content_type='application/rss+xml')
@json.json_view
def load_river_stories__redis(request):
limit = 12
@ -913,7 +987,8 @@ def load_river_stories__redis(request):
user_id=user.pk,
story_feed_id__in=found_feed_ids
).only('story_hash', 'starred_date')
starred_stories = dict([(story.story_hash, story.starred_date)
starred_stories = dict([(story.story_hash, dict(starred_date=story.starred_date,
user_tags=story.user_tags))
for story in starred_stories])
else:
starred_stories = {}
@ -953,9 +1028,10 @@ def load_river_stories__redis(request):
story['long_parsed_date'] = format_story_link_date__long(story_date, nowtz)
if story['story_hash'] in starred_stories:
story['starred'] = True
starred_date = localtime_for_timezone(starred_stories[story['story_hash']],
starred_date = localtime_for_timezone(starred_stories[story['story_hash']]['starred_date'],
user.profile.timezone)
story['starred_date'] = format_story_link_date__long(starred_date, now)
story['user_tags'] = starred_stories[story['story_hash']]['user_tags']
story['intelligence'] = {
'feed': apply_classifier_feeds(classifier_feeds, story['story_feed_id']),
'author': apply_classifier_authors(classifier_authors, story),
@ -1685,11 +1761,13 @@ def iframe_buster(request):
@ajax_login_required
@json.json_view
def mark_story_as_starred(request):
code = 1
feed_id = int(request.REQUEST['feed_id'])
story_id = request.REQUEST['story_id']
message = ""
story, _ = MStory.find_story(story_feed_id=feed_id, story_id=story_id)
code = 1
feed_id = int(request.REQUEST['feed_id'])
story_id = request.REQUEST['story_id']
user_tags = request.REQUEST.getlist('user_tags')
message = ""
story, _ = MStory.find_story(story_feed_id=feed_id, story_id=story_id)
if not story:
return {'code': -1, 'message': "Could not find story to save."}
@ -1698,24 +1776,31 @@ def mark_story_as_starred(request):
story_db.pop('user_id', None)
story_db.pop('starred_date', None)
story_db.pop('id', None)
story_db.pop('user_tags', None)
now = datetime.datetime.now()
story_values = dict(user_id=request.user.pk, starred_date=now, **story_db)
story_values = dict(user_id=request.user.pk, starred_date=now, user_tags=user_tags, **story_db)
starred_story, created = MStarredStory.objects.get_or_create(
story_hash=story.story_hash,
user_id=story_values.pop('user_id'),
defaults=story_values)
if created:
logging.user(request, "~FCStarring: ~SB%s" % (story.story_title[:50]))
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)
else:
code = -1
message = "Already saved this story."
logging.user(request, "~FC~BRAlready stared:~SN~FC ~SB%s" % (story.story_title[:50]))
starred_story.user_tags = user_tags
starred_story.save()
return {'code': code, 'message': message}
MStarredStoryCounts.count_tags_for_user(request.user.pk)
starred_counts = MStarredStoryCounts.user_counts(request.user.pk)
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)" % (story.story_title[:32], starred_story.user_tags))
return {'code': code, 'message': message, 'starred_counts': starred_counts}
@required_params('story_id')
@ajax_login_required
@ -1723,6 +1808,7 @@ def mark_story_as_starred(request):
def mark_story_as_unstarred(request):
code = 1
story_id = request.POST['story_id']
starred_counts = None
starred_story = MStarredStory.objects(user_id=request.user.pk, story_guid=story_id)
if not starred_story:
@ -1730,10 +1816,12 @@ def mark_story_as_unstarred(request):
if starred_story:
logging.user(request, "~FCUnstarring: ~SB%s" % (starred_story[0].story_title[:50]))
starred_story.delete()
MStarredStoryCounts.count_tags_for_user(request.user.pk)
starred_counts = MStarredStoryCounts.user_counts(request.user.pk)
else:
code = -1
return {'code': code}
return {'code': code, 'starred_counts': starred_counts}
@ajax_login_required
@json.json_view

View file

@ -9,6 +9,7 @@ import zlib
import hashlib
import redis
import pymongo
import HTMLParser
from collections import defaultdict
from operator import itemgetter
from bson.objectid import ObjectId
@ -1215,6 +1216,8 @@ class Feed(models.Model):
story['id'] = story_db.story_guid or story_db.story_date
if hasattr(story_db, 'starred_date'):
story['starred_date'] = story_db.starred_date
if hasattr(story_db, 'user_tags'):
story['user_tags'] = story_db.user_tags
if hasattr(story_db, 'shared_date'):
story['shared_date'] = story_db.shared_date
if include_permalinks and hasattr(story_db, 'blurblog_permalink'):
@ -1643,7 +1646,12 @@ class MStory(mongo.Document):
@property
def feed_guid_hash(self):
return "%s:%s" % (self.story_feed_id, self.guid_hash)
@property
def decoded_story_title(self):
h = HTMLParser.HTMLParser()
return h.unescape(self.story_title)
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
@ -1933,6 +1941,7 @@ class MStarredStory(mongo.Document):
story_guid = mongo.StringField()
story_hash = mongo.StringField()
story_tags = mongo.ListField(mongo.StringField(max_length=250))
user_tags = mongo.ListField(mongo.StringField(max_length=128))
image_urls = mongo.ListField(mongo.StringField(max_length=1024))
meta = {
@ -1967,12 +1976,26 @@ class MStarredStory(mongo.Document):
db_id=str(self.id))
@classmethod
def find_stories(cls, query, user_id, offset=0, limit=25):
def find_stories(cls, query, user_id, tag=None, offset=0, limit=25):
stories_db = cls.objects(
Q(user_id=user_id) &
(Q(story_title__icontains=query) |
Q(story_author_name__icontains=query) |
Q(story_tags__icontains=query))
)
if tag:
stories_db = stories_db.filter(user_tags__contains=tag)
stories_db = stories_db.order_by('-starred_date')[offset:offset+limit]
stories = Feed.format_stories(stories_db)
return stories
@classmethod
def find_stories_by_user_tag(cls, user_tag, user_id, offset=0, limit=25):
stories_db = cls.objects(
Q(user_id=user_id),
Q(user_tags__icontains=user_tag)
).order_by('-starred_date')[offset:offset+limit]
stories = Feed.format_stories(stories_db)
@ -2036,7 +2059,63 @@ class MStarredStory(mongo.Document):
return original_text
class MStarredStoryCounts(mongo.Document):
user_id = mongo.IntField()
tag = mongo.StringField(max_length=128, unique_with=['user_id'])
slug = mongo.StringField(max_length=128)
count = mongo.IntField()
meta = {
'collection': 'starred_stories_counts',
'indexes': ['user_id'],
'ordering': ['tag'],
'allow_inheritance': False,
}
@property
def rss_url(self, secret_token=None):
if not secret_token:
user = User.objects.select_related('profile').get(pk=self.user_id)
secret_token = user.profile.secret_token
return "%s/reader/starred_rss/%s/%s/%s" % (settings.NEWSBLUR_URL, self.user_id,
secret_token, self.slug)
@classmethod
def user_counts(cls, user_id, include_total=False, try_counting=True):
counts = cls.objects.filter(user_id=user_id)
counts = [{'tag': c.tag, 'count': c.count, 'feed_address': c.rss_url} for c in counts]
if counts == [] and try_counting:
cls.count_tags_for_user(user_id)
return cls.user_counts(user_id, include_total=include_total,
try_counting=False)
if include_total:
for c in counts:
if c['tag'] == "":
return counts, c['count']
return counts, 0
return counts
@classmethod
def count_tags_for_user(cls, user_id):
all_tags = MStarredStory.objects(user_id=user_id,
user_tags__exists=True).item_frequencies('user_tags')
user_tags = sorted([(k, v) for k, v in all_tags.items() if int(v) > 0],
key=itemgetter(1),
reverse=True)
cls.objects(user_id=user_id).delete()
for tag, count in dict(user_tags).items():
cls.objects.create(user_id=user_id, tag=tag, slug=slugify(tag), count=count)
total_stories_count = MStarredStory.objects(user_id=user_id).count()
cls.objects.create(user_id=user_id, tag="", count=total_stories_count)
return dict(total=total_stories_count, tags=user_tags)
class MFetchHistory(mongo.Document):
feed_id = mongo.IntField(unique=True)
feed_fetch_history = mongo.DynamicField()

View file

@ -7,6 +7,7 @@ import re
import mongoengine as mongo
import random
import requests
import HTMLParser
from collections import defaultdict
from BeautifulSoup import BeautifulSoup
from mongoengine.queryset import Q
@ -1407,7 +1408,7 @@ class MSharedStory(mongo.Document):
def __unicode__(self):
user = User.objects.get(pk=self.user_id)
return "%s: %s (%s)%s%s" % (user.username,
self.story_title[:20],
self.decoded_story_title[:20],
self.story_feed_id,
': ' if self.has_comments else '',
self.comments[:20])
@ -1420,6 +1421,11 @@ class MSharedStory(mongo.Document):
def feed_guid_hash(self):
return "%s:%s" % (self.story_feed_id or "0", self.guid_hash)
@property
def decoded_story_title(self):
h = HTMLParser.HTMLParser()
return h.unescape(self.story_title)
def canonical(self):
return {
"user_id": self.user_id,
@ -1634,7 +1640,7 @@ class MSharedStory(mongo.Document):
if interactive:
feed = Feed.get_by_id(story.story_feed_id)
accept_story = raw_input("%s / %s [Y/n]: " % (story.story_title, feed.title))
accept_story = raw_input("%s / %s [Y/n]: " % (story.decoded_story_title, feed.title))
if accept_story in ['n', 'N']: continue
story_db = dict([(k, v) for k, v in story._data.items()
@ -1658,7 +1664,7 @@ class MSharedStory(mongo.Document):
shared_feed_ids.append(story.story_feed_id)
publish_new_stories = True
logging.user(popular_user, "~FCSharing: ~SB~FM%s (%s shares, %s min)" % (
story.story_title[:50],
story.decoded_story_title[:50],
story_info['count'],
cutoff))
@ -1914,7 +1920,7 @@ class MSharedStory(mongo.Document):
def generate_post_to_service_message(self, truncate=None, include_url=True):
message = strip_tags(self.comments)
if not message or len(message) < 1:
message = self.story_title
message = self.decoded_story_title
if include_url and truncate:
message = truncate_chars(message, truncate - 18 - 30)
feed = Feed.get_by_id(self.story_feed_id)
@ -2023,7 +2029,7 @@ class MSharedStory(mongo.Document):
'story_feed': story_feed,
'mute_url': mute_url,
}
story_title = self.story_title.replace('\n', ' ')
story_title = self.decoded_story_title.replace('\n', ' ')
text = render_to_string('mail/email_reply.txt', data)
html = pynliner.fromString(render_to_string('mail/email_reply.xhtml', data))
@ -2038,7 +2044,7 @@ class MSharedStory(mongo.Document):
logging.user(reply_user, "~BB~FM~SBSending %s/%s email%s for new reply: %s" % (
sent_emails, len(notify_user_ids),
'' if len(notify_user_ids) == 1 else 's',
self.story_title[:30]))
self.decoded_story_title[:30]))
self.emailed_replies.append(reply.reply_id)
self.save()
@ -2087,7 +2093,7 @@ class MSharedStory(mongo.Document):
'story_feed': story_feed,
'mute_url': mute_url,
}
story_title = self.story_title.replace('\n', ' ')
story_title = self.decoded_story_title.replace('\n', ' ')
text = render_to_string('mail/email_reshare.txt', data)
html = pynliner.fromString(render_to_string('mail/email_reshare.xhtml', data))
@ -2103,7 +2109,7 @@ class MSharedStory(mongo.Document):
logging.user(reshare_user, "~BB~FM~SBSending %s email for story re-share: %s" % (
original_user.username,
self.story_title[:30]))
self.decoded_story_title[:30]))
def calculate_image_sizes(self, force=False):
if not self.story_content_z:
@ -2286,7 +2292,7 @@ class MSocialServices(mongo.Document):
return
twitter_user = api.me()
self.twitter_picture_url = twitter_user.profile_image_url
self.twitter_picture_url = twitter_user.profile_image_url_https
self.twitter_username = twitter_user.screen_name
self.twitter_refreshed_date = datetime.datetime.utcnow()
self.syncing_twitter = False
@ -2524,6 +2530,18 @@ class MSocialServices(mongo.Document):
profile.save()
return profile
def sync_twitter_photo(self):
profile = MSocialProfile.get_user(self.user_id)
if profile.photo_service != "twitter":
return
api = self.twitter_api()
me = api.me()
self.twitter_picture_url = me.profile_image_url_https
self.save()
self.set_photo('twitter')
def post_to_twitter(self, shared_story):
message = shared_story.generate_post_to_service_message(truncate=140)
@ -2547,7 +2565,7 @@ class MSocialServices(mongo.Document):
api.put_object('me', '%s:share' % settings.FACEBOOK_NAMESPACE,
link=shared_story.blurblog_permalink(),
type="link",
name=shared_story.story_title,
name=shared_story.decoded_story_title,
description=content,
website=shared_story.blurblog_permalink(),
message=message,
@ -2564,7 +2582,7 @@ class MSocialServices(mongo.Document):
try:
api = self.appdotnet_api()
api.createPost(text=message, links=[{
'text': shared_story.story_title,
'text': shared_story.decoded_story_title,
'url': shared_story.blurblog_permalink()
}])
except Exception, e:

View file

@ -110,11 +110,13 @@ def load_social_stories(request, user_id, username=None):
starred_stories = MStarredStory.objects(user_id=user.pk,
story_hash__in=story_hashes)\
.only('story_hash', 'starred_date')
.only('story_hash', 'starred_date', 'user_tags')
shared_stories = MSharedStory.objects(user_id=user.pk,
story_hash__in=story_hashes)\
.only('story_hash', 'shared_date', 'comments')
starred_stories = dict([(story.story_hash, story.starred_date) for story in starred_stories])
starred_stories = dict([(story.story_hash, dict(starred_date=story.starred_date,
user_tags=story.user_tags))
for story in starred_stories])
shared_stories = dict([(story.story_hash, dict(shared_date=story.shared_date,
comments=story.comments))
for story in shared_stories])
@ -135,9 +137,10 @@ def load_social_stories(request, user_id, username=None):
if story['story_hash'] in starred_stories:
story['starred'] = True
starred_date = localtime_for_timezone(starred_stories[story['story_hash']],
starred_date = localtime_for_timezone(starred_stories[story['story_hash']]['starred_date'],
user.profile.timezone)
story['starred_date'] = format_story_link_date__long(starred_date, now)
story['user_tags'] = starred_stories[story['story_hash']]['user_tags']
if story['story_hash'] in shared_stories:
story['shared'] = True
story['shared_comments'] = strip_tags(shared_stories[story['story_hash']]['comments'])
@ -239,8 +242,9 @@ def load_river_blurblog(request):
starred_stories = MStarredStory.objects(
user_id=user.pk,
story_hash__in=story_hashes
).only('story_hash', 'starred_date')
starred_stories = dict([(story.story_hash, story.starred_date)
).only('story_hash', 'starred_date', 'user_tags')
starred_stories = dict([(story.story_hash, dict(starred_date=story.starred_date,
user_tags=story.user_tags))
for story in starred_stories])
shared_stories = MSharedStory.objects(user_id=user.pk,
story_hash__in=story_hashes)\
@ -281,8 +285,9 @@ def load_river_blurblog(request):
story['long_parsed_date'] = format_story_link_date__long(story_date, nowtz)
if story['story_hash'] in starred_stories:
story['starred'] = True
starred_date = localtime_for_timezone(starred_stories[story['story_hash']], user.profile.timezone)
starred_date = localtime_for_timezone(starred_stories[story['story_hash']]['starred_date'], user.profile.timezone)
story['starred_date'] = format_story_link_date__long(starred_date, now)
story['user_tags'] = starred_stories[story['story_hash']]['user_tags']
story['intelligence'] = {
'feed': apply_classifier_feeds(classifier_feeds, story['story_feed_id'],
social_user_ids=story['friend_user_ids']),
@ -1233,7 +1238,7 @@ def shared_stories_rss_feed(request, user_id, username):
data['link'] = social_profile.blurblog_url
data['description'] = "Stories shared by %s on NewsBlur." % user.username
data['lastBuildDate'] = datetime.datetime.utcnow()
data['generator'] = 'NewsBlur - http://www.newsblur.com'
data['generator'] = 'NewsBlur - %s' % settings.NEWSBLUR_URL
data['docs'] = None
data['author_name'] = user.username
data['feed_url'] = "http://%s%s" % (

View file

@ -53,6 +53,7 @@ javascripts:
- media/js/vendor/jquery.chosen.js
- media/js/vendor/jquery.effects.core.js
- media/js/vendor/jquery.effects.slideOffscreen.js
- media/js/vendor/tag-it.js
- media/js/vendor/chart.js
- media/js/vendor/audio.js
- media/js/vendor/socket.io-client.*.js
@ -119,6 +120,7 @@ stylesheets:
- media/css/jquery-ui/jquery.theme.css
- media/css/jquery.tipsy.css
- media/css/vendor/bootstrap-progressbar.css
- media/css/vendor/jquery.tagit.css
- media/css/vendor/highlight.css
- media/css/*.css
mobile:

View file

@ -24,19 +24,19 @@ public class AllSharedStoriesReading extends Reading {
setTitle(getResources().getString(R.string.all_shared_stories));
Cursor folderCursor = contentResolver.query(FeedProvider.SOCIALCOUNT_URI, null, DatabaseConstants.getBlogSelectionFromState(currentState), null, null);
int unreadCount = FeedUtils.getCursorUnreadCount(folderCursor, currentState);
folderCursor.close();
this.startingUnreadCount = unreadCount;
this.currentUnreadCount = unreadCount;
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver());
addStoryToMarkAsRead(readingAdapter.getStory(passedPosition));
getSupportLoaderManager().initLoader(0, null, this);
}
@Override
protected int getUnreadCount() {
Cursor folderCursor = contentResolver.query(FeedProvider.SOCIALCOUNT_URI, null, DatabaseConstants.getBlogSelectionFromState(currentState), null, null);
int c = FeedUtils.getCursorUnreadCount(folderCursor, currentState);
folderCursor.close();
return c;
}
@Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
StoryOrder storyOrder = PrefsUtils.getStoryOrderForFolder(this, PrefConstants.ALL_SHARED_STORIES_FOLDER_NAME);

View file

@ -26,19 +26,19 @@ public class AllStoriesReading extends Reading {
stories = contentResolver.query(FeedProvider.ALL_STORIES_URI, null, DatabaseConstants.getStorySelectionFromState(currentState), null, DatabaseConstants.getStorySortOrder(storyOrder));
setTitle(getResources().getString(R.string.all_stories_row_title));
Cursor folderCursor = contentResolver.query(FeedProvider.FEED_COUNT_URI, null, DatabaseConstants.getBlogSelectionFromState(currentState), null, null);
int unreadCount = FeedUtils.getCursorUnreadCount(folderCursor, currentState);
folderCursor.close();
this.startingUnreadCount = unreadCount;
this.currentUnreadCount = unreadCount;
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver());
addStoryToMarkAsRead(readingAdapter.getStory(passedPosition));
getSupportLoaderManager().initLoader(0, null, this);
}
@Override
protected int getUnreadCount() {
Cursor folderCursor = contentResolver.query(FeedProvider.FEED_COUNT_URI, null, DatabaseConstants.getBlogSelectionFromState(currentState), null, null);
int c = FeedUtils.getCursorUnreadCount(folderCursor, currentState);
folderCursor.close();
return c;
}
@Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
StoryOrder storyOrder = PrefsUtils.getStoryOrderForFolder(this, PrefConstants.ALL_STORIES_FOLDER_NAME);

View file

@ -21,7 +21,6 @@ import com.newsblur.util.StoryOrder;
public class FeedReading extends Reading {
String feedId;
private Feed feed;
@Override
protected void onCreate(Bundle savedInstanceBundle) {
@ -33,25 +32,26 @@ public class FeedReading extends Reading {
Cursor feedClassifierCursor = contentResolver.query(classifierUri, null, null, null, null);
Classifier classifier = Classifier.fromCursor(feedClassifierCursor);
final Uri feedUri = FeedProvider.FEEDS_URI.buildUpon().appendPath(feedId).build();
Uri feedUri = FeedProvider.FEEDS_URI.buildUpon().appendPath(feedId).build();
Cursor feedCursor = contentResolver.query(feedUri, null, null, null, null);
feedCursor.moveToFirst();
feed = Feed.fromCursor(feedCursor);
Feed feed = Feed.fromCursor(feedCursor);
feedCursor.close();
setTitle(feed.title);
int unreadCount = FeedUtils.getFeedUnreadCount(this.feed, this.currentState);
this.startingUnreadCount = unreadCount;
this.currentUnreadCount = unreadCount;
readingAdapter = new FeedReadingAdapter(getSupportFragmentManager(), feed, classifier);
addStoryToMarkAsRead(readingAdapter.getStory(passedPosition));
getSupportLoaderManager().initLoader(0, null, this);
}
@Override
protected int getUnreadCount() {
Uri feedUri = FeedProvider.FEEDS_URI.buildUpon().appendPath(feedId).build();
Cursor feedCursor = contentResolver.query(feedUri, null, null, null, null);
Feed feed = Feed.fromCursor(feedCursor);
feedCursor.close();
return FeedUtils.getFeedUnreadCount(feed, this.currentState);
}
@Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
Uri storiesURI = FeedProvider.FEED_STORIES_URI.buildUpon().appendPath(feedId).build();

View file

@ -27,19 +27,19 @@ public class FolderReading extends Reading {
folderName = getIntent().getStringExtra(Reading.EXTRA_FOLDERNAME);
setTitle(folderName);
Cursor folderCursor = contentResolver.query(FeedProvider.FOLDERS_URI.buildUpon().appendPath(folderName).build(), null, null, new String[] { DatabaseConstants.getFolderSelectionFromState(currentState) }, null);
int unreadCount = FeedUtils.getCursorUnreadCount(folderCursor, currentState);
folderCursor.close();
this.startingUnreadCount = unreadCount;
this.currentUnreadCount = unreadCount;
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver());
addStoryToMarkAsRead(readingAdapter.getStory(passedPosition));
getSupportLoaderManager().initLoader(0, null, this);
}
@Override
protected int getUnreadCount() {
Cursor folderCursor = contentResolver.query(FeedProvider.FOLDERS_URI.buildUpon().appendPath(folderName).build(), null, null, new String[] { DatabaseConstants.getFolderSelectionFromState(currentState) }, null);
int c = FeedUtils.getCursorUnreadCount(folderCursor, currentState);
folderCursor.close();
return c;
}
@Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
return new CursorLoader(this, FeedProvider.MULTIFEED_STORIES_URI, null, DatabaseConstants.getStorySelectionFromState(currentState), feedIds, DatabaseConstants.getStorySortOrder(PrefsUtils.getStoryOrderForFolder(this, folderName)));

View file

@ -71,6 +71,9 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
protected int passedPosition;
protected int currentState;
protected final Object STORIES_MUTEX = new Object();
protected Cursor stories;
protected ViewPager pager;
protected Button overlayLeft, overlayRight;
protected ProgressBar overlayProgress, overlayProgressRight, overlayProgressLeft;
@ -79,20 +82,13 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
protected ReadingAdapter readingAdapter;
protected ContentResolver contentResolver;
private APIManager apiManager;
protected Cursor stories;
private boolean noMoreApiPages;
private boolean stopLoading;
protected volatile boolean requestedPage; // set high iff a syncservice request for stories is already in flight
private int currentApiPage = 0;
private Set<Story> storiesToMarkAsRead;
// unread counts for the circular progress overlay. set to nonzero to activate the progress indicator overlay
// unread count for the circular progress overlay. set to nonzero to activate the progress indicator overlay
protected int startingUnreadCount = 0;
protected int currentUnreadCount = 0;
// A list of stories we have marked as read during this reading session. Needed to help keep track of unread
// counts since it would be too costly to query and update the DB on every page change.
private Set<Story> storiesAlreadySeen;
private float overlayRangeTopPx;
private float overlayRangeBotPx;
@ -116,9 +112,6 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
this.overlaySend = (Button) findViewById(R.id.reading_overlay_send);
fragmentManager = getSupportFragmentManager();
storiesToMarkAsRead = new HashSet<Story>();
storiesAlreadySeen = new HashSet<Story>();
passedPosition = getIntent().getIntExtra(EXTRA_POSITION, 0);
currentState = getIntent().getIntExtra(ItemsList.EXTRA_STATE, 0);
@ -139,6 +132,7 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
// this likes to default to 'on' for some platforms
enableProgressCircle(overlayProgressLeft, false);
enableProgressCircle(overlayProgressRight, false);
}
@ -152,30 +146,34 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (cursor != null) {
readingAdapter.swapCursor(cursor);
stories = cursor;
}
if (this.pager == null) {
// if this is the first time we've found a cursor, set up the pager
setupPager();
}
try {
readingAdapter.notifyDataSetChanged();
checkStoryCount(pager.getCurrentItem());
if (this.unreadSearchLatch != null) {
this.unreadSearchLatch.countDown();
synchronized (STORIES_MUTEX) {
if (cursor != null) {
readingAdapter.swapCursor(cursor);
stories = cursor;
}
ReadingItemFragment fragment = getReadingFragment();
if (fragment != null ) {
fragment.updateStory(readingAdapter.getStory(pager.getCurrentItem()));
fragment.updateSaveButton();
if (this.pager == null) {
// if this is the first time we've found a cursor, we know the onCreate chain is done
this.startingUnreadCount = getUnreadCount();
// set up the pager after the unread count, so the first mark-read doesn't happen too quickly
setupPager();
}
try {
readingAdapter.notifyDataSetChanged();
checkStoryCount(pager.getCurrentItem());
if (this.unreadSearchLatch != null) {
this.unreadSearchLatch.countDown();
}
ReadingItemFragment fragment = getReadingFragment();
if (fragment != null ) {
fragment.updateStory(readingAdapter.getStory(pager.getCurrentItem()));
fragment.updateSaveButton();
}
} catch (IllegalStateException ise) {
// sometimes the pager is already shutting down by the time the callback finishes
finish();
}
} catch (IllegalStateException ise) {
// sometimes the pager is already shutting down by the time the callback finishes
finish();
}
}
@ -186,7 +184,7 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
pager.setOnPageChangeListener(this);
pager.setAdapter(readingAdapter);
pager.setCurrentItem(passedPosition);
pager.setCurrentItem(passedPosition, false);
// setCurrentItem sometimes fails to pass the first page to the callback, so call it manually
// for the first one.
this.onPageSelected(passedPosition);
@ -194,6 +192,11 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
this.enableOverlays();
}
/**
* Query the DB for the current unreadcount for this view.
*/
protected abstract int getUnreadCount();
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
@ -264,6 +267,10 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
if (noMoreData) {
this.noMoreApiPages = true;
}
updateCursor();
}
private void updateCursor() {
getSupportLoaderManager().restartLoader(0, null, this);
}
@ -287,10 +294,9 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
this.pageHistory.add(story);
}
}
addStoryToMarkAsRead(story);
checkStoryCount(position);
markStoryRead(story);
}
this.enableOverlays();
checkStoryCount(position);
}
// interface ScrollChangeListener
@ -341,17 +347,16 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
private void enableOverlays() {
this.setOverlayAlpha(1.0f);
this.overlayLeft.setEnabled(this.getLastReadPosition(false) != -1);
this.overlayRight.setText((this.currentUnreadCount > 0) ? R.string.overlay_next : R.string.overlay_done);
this.overlayRight.setBackgroundResource((this.currentUnreadCount > 0) ? R.drawable.selector_overlay_bg_right : R.drawable.selector_overlay_bg_right_done);
this.overlayRight.setText((getUnreadCount() > 0) ? R.string.overlay_next : R.string.overlay_done);
this.overlayRight.setBackgroundResource((getUnreadCount() > 0) ? R.drawable.selector_overlay_bg_right : R.drawable.selector_overlay_bg_right_done);
if (this.startingUnreadCount == 0 ) {
// sessions with no unreads just show a full progress bar
this.overlayProgress.setMax(1);
this.overlayProgress.setProgress(1);
} else {
int unreadProgress = this.startingUnreadCount - this.currentUnreadCount;
int unreadProgress = this.startingUnreadCount - getUnreadCount();
this.overlayProgress.setMax(this.startingUnreadCount);
this.overlayProgress.setProgress(unreadProgress);
}
@ -368,7 +373,6 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
}
invalidateOptionsMenu();
}
public void onWindowFocusChanged(boolean hasFocus) {
@ -413,62 +417,26 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
protected abstract void triggerRefresh(int page);
@Override
protected void onPause() {
flushStoriesMarkedRead();
super.onPause();
}
@Override
protected void onStop() {
this.stopLoading = true;
super.onStop();
}
/**
* Log a story as having been read. The local DB and remote server will be updated
* batch-wise when the activity pauses.
*/
protected void addStoryToMarkAsRead(Story story) {
if (story == null) return;
if (story.read) return;
synchronized (this.storiesToMarkAsRead) {
this.storiesToMarkAsRead.add(story);
}
// flush immediately if the batch reaches a sufficient size
if (this.storiesToMarkAsRead.size() >= AppConstants.MAX_MARK_READ_BATCH) {
flushStoriesMarkedRead();
}
if (!this.storiesAlreadySeen.contains(story)) {
// only decrement the cached story count if the story wasn't already read
this.storiesAlreadySeen.add(story);
this.currentUnreadCount--;
}
this.enableOverlays();
}
private void flushStoriesMarkedRead() {
synchronized(this.storiesToMarkAsRead) {
if (this.storiesToMarkAsRead.size() > 0) {
FeedUtils.markStoriesAsRead(this.storiesToMarkAsRead, this);
this.storiesToMarkAsRead.clear();
}
private void markStoryRead(Story story) {
synchronized (STORIES_MUTEX) {
FeedUtils.markStoryAsRead(story, this);
updateCursor();
}
enableOverlays();
}
private void markStoryUnread(Story story) {
// first, ensure the story isn't queued up to be marked as read
this.storiesToMarkAsRead.remove(story);
// next, call the API to un-mark it as read, just in case we missed the batch
// operation, or it was read long before now.
FeedUtils.markStoryUnread(story, Reading.this, this.apiManager);
this.currentUnreadCount++;
this.storiesAlreadySeen.remove(story);
this.enableOverlays();
synchronized (STORIES_MUTEX) {
FeedUtils.markStoryUnread(story, this);
updateCursor();
}
enableOverlays();
}
// NB: this callback is for the text size slider
@ -492,7 +460,7 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
* Click handler for the righthand overlay nav button.
*/
public void overlayRight(View v) {
if (this.currentUnreadCount == 0) {
if (getUnreadCount() == 0) {
// if there are no unread stories, go back to the feed list
Intent i = new Intent(this, Main.class);
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
@ -506,7 +474,6 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
return null;
}
}.execute();
//}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
@ -530,7 +497,7 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
break unreadSearch;
}
} else {
if ((candidate == pager.getCurrentItem()) || (story.read) || (this.storiesAlreadySeen.contains(story))) {
if ((candidate == pager.getCurrentItem()) || (story.read) ) {
candidate++;
continue unreadSearch;
} else {
@ -612,8 +579,8 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
* Click handler for the progress indicator on the righthand overlay nav button.
*/
public void overlayCount(View v) {
String unreadText = getString((this.currentUnreadCount == 1) ? R.string.overlay_count_toast_1 : R.string.overlay_count_toast_N);
Toast.makeText(this, String.format(unreadText, this.currentUnreadCount), Toast.LENGTH_SHORT).show();
String unreadText = getString((getUnreadCount() == 1) ? R.string.overlay_count_toast_1 : R.string.overlay_count_toast_N);
Toast.makeText(this, String.format(unreadText, getUnreadCount()), Toast.LENGTH_SHORT).show();
}
public void overlaySend(View v) {

View file

@ -26,6 +26,12 @@ public class SavedStoriesReading extends Reading {
getSupportLoaderManager().initLoader(0, null, this);
}
@Override
protected int getUnreadCount() {
// effectively disable the notion of unreads for this feed
return 0;
}
@Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
return new CursorLoader(this, FeedProvider.STARRED_STORIES_URI, null, null, null, DatabaseConstants.getStorySortOrder(StoryOrder.NEWEST));

View file

@ -21,7 +21,6 @@ public class SocialFeedReading extends Reading {
private String userId;
private String username;
private SocialFeed socialFeed;
@Override
protected void onCreate(Bundle savedInstanceBundle) {
@ -30,22 +29,22 @@ public class SocialFeedReading extends Reading {
userId = getIntent().getStringExtra(Reading.EXTRA_USERID);
username = getIntent().getStringExtra(Reading.EXTRA_USERNAME);
Uri socialFeedUri = FeedProvider.SOCIAL_FEEDS_URI.buildUpon().appendPath(userId).build();
socialFeed = SocialFeed.fromCursor(contentResolver.query(socialFeedUri, null, null, null, null));
setTitle(getIntent().getStringExtra(EXTRA_USERNAME));
int unreadCount = FeedUtils.getFeedUnreadCount(this.socialFeed, this.currentState);
this.startingUnreadCount = unreadCount;
this.currentUnreadCount = unreadCount;
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver());
addStoryToMarkAsRead(readingAdapter.getStory(passedPosition));
getSupportLoaderManager().initLoader(0, null, this);
}
@Override
protected int getUnreadCount() {
Uri socialFeedUri = FeedProvider.SOCIAL_FEEDS_URI.buildUpon().appendPath(userId).build();
Cursor cursor = contentResolver.query(socialFeedUri, null, null, null, null);
SocialFeed socialFeed = SocialFeed.fromCursor(cursor);
cursor.close();
return FeedUtils.getFeedUnreadCount(socialFeed, this.currentState);
}
@Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
Uri storiesURI = FeedProvider.SOCIALFEED_STORIES_URI.buildUpon().appendPath(userId).build();

View file

@ -347,6 +347,18 @@ public class FeedProvider extends ContentProvider {
public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) {
return mdb.query(table, columns, selection, selectionArgs, groupBy, having, orderBy);
}
public void execSQL(String sql) {
if (AppConstants.VERBOSE_LOG) {
Log.d(LoggingDatabase.class.getName(), "execSQL: " + sql);
}
mdb.execSQL(sql);
}
public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
return mdb.update(table, values, whereClause, whereArgs);
}
public long insertWithOnConflict(String table, String nullColumnHack, ContentValues initialValues, int conflictAlgorithm) {
return mdb.insertWithOnConflict(table, nullColumnHack, initialValues, conflictAlgorithm);
}
}
@Override
@ -431,6 +443,11 @@ public class FeedProvider extends ContentProvider {
selectionArgs = new String[] { uri.getLastPathSegment() };
return db.query(DatabaseConstants.STORY_TABLE, DatabaseConstants.STORY_COLUMNS, selection, selectionArgs, null, null, sortOrder);
case INDIVIDUAL_STORY:
selectionArgs = new String[] { uri.getLastPathSegment() };
selection = DatabaseConstants.STORY_ID + " = ?";
return db.query(DatabaseConstants.STORY_TABLE, DatabaseConstants.STORY_COLUMNS, selection, selectionArgs, null, null, sortOrder);
// Querying for all stories
case ALL_STORIES:
String allStoriesQuery = "SELECT " + TextUtils.join(",", DatabaseConstants.STORY_COLUMNS) + ", " + DatabaseConstants.FEED_TITLE + ", " +
@ -598,7 +615,8 @@ public class FeedProvider extends ContentProvider {
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
final SQLiteDatabase db = databaseHelper.getWritableDatabase();
final SQLiteDatabase rwdb = databaseHelper.getWritableDatabase();
final LoggingDatabase db = new LoggingDatabase(rwdb);
switch (uriMatcher.match(uri)) {
case ALL_FEEDS:
@ -617,10 +635,10 @@ public class FeedProvider extends ContentProvider {
// In order to run a raw SQL query whereby we make decrement the column we need to a dynamic reference - something the usual content provider can't easily handle. Hence this circuitous hack.
case FEED_COUNT:
db.execSQL("UPDATE " + DatabaseConstants.FEED_TABLE + " SET " + selectionArgs[0] + " = " + selectionArgs[0] + " - 1 WHERE " + DatabaseConstants.FEED_ID + " = " + selectionArgs[1]);
return 0;
return 1;
case SOCIALFEED_COUNT:
db.execSQL("UPDATE " + DatabaseConstants.SOCIALFEED_TABLE + " SET " + selectionArgs[0] + " = " + selectionArgs[0] + " - 1 WHERE " + DatabaseConstants.SOCIAL_FEED_ID + " = " + selectionArgs[1]);
return 0;
return 1;
case STARRED_STORIES_COUNT:
int rows = db.update(DatabaseConstants.STARRED_STORY_COUNT_TABLE, values, null, null);
if (rows == 0 ) {

View file

@ -79,24 +79,27 @@ public class Feed {
return values;
}
public static Feed fromCursor(Cursor childCursor) {
public static Feed fromCursor(Cursor cursor) {
if (cursor.isBeforeFirst()) {
cursor.moveToFirst();
}
Feed feed = new Feed();
feed.active = Boolean.parseBoolean(childCursor.getString(childCursor.getColumnIndex(DatabaseConstants.FEED_ACTIVE)));
feed.address = childCursor.getString(childCursor.getColumnIndex(DatabaseConstants.FEED_ADDRESS));
feed.favicon = childCursor.getString(childCursor.getColumnIndex(DatabaseConstants.FEED_FAVICON));
feed.faviconColor = childCursor.getString(childCursor.getColumnIndex(DatabaseConstants.FEED_FAVICON_COLOR));
feed.faviconFade = childCursor.getString(childCursor.getColumnIndex(DatabaseConstants.FEED_FAVICON_FADE));
feed.faviconBorder = childCursor.getString(childCursor.getColumnIndex(DatabaseConstants.FEED_FAVICON_BORDER));
feed.faviconText = childCursor.getString(childCursor.getColumnIndex(DatabaseConstants.FEED_FAVICON_TEXT));
feed.faviconUrl = childCursor.getString(childCursor.getColumnIndex(DatabaseConstants.FEED_FAVICON_URL));
feed.feedId = childCursor.getString(childCursor.getColumnIndex(DatabaseConstants.FEED_ID));
feed.feedLink = childCursor.getString(childCursor.getColumnIndex(DatabaseConstants.FEED_LINK));
feed.lastUpdated = childCursor.getString(childCursor.getColumnIndex(DatabaseConstants.FEED_UPDATED_SECONDS));
feed.negativeCount = childCursor.getInt(childCursor.getColumnIndex(DatabaseConstants.FEED_NEGATIVE_COUNT));
feed.neutralCount = childCursor.getInt(childCursor.getColumnIndex(DatabaseConstants.FEED_NEUTRAL_COUNT));
feed.positiveCount = childCursor.getInt(childCursor.getColumnIndex(DatabaseConstants.FEED_POSITIVE_COUNT));
feed.subscribers = childCursor.getString(childCursor.getColumnIndex(DatabaseConstants.FEED_SUBSCRIBERS));
feed.title = childCursor.getString(childCursor.getColumnIndex(DatabaseConstants.FEED_TITLE));
feed.active = Boolean.parseBoolean(cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_ACTIVE)));
feed.address = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_ADDRESS));
feed.favicon = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_FAVICON));
feed.faviconColor = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_FAVICON_COLOR));
feed.faviconFade = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_FAVICON_FADE));
feed.faviconBorder = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_FAVICON_BORDER));
feed.faviconText = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_FAVICON_TEXT));
feed.faviconUrl = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_FAVICON_URL));
feed.feedId = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_ID));
feed.feedLink = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_LINK));
feed.lastUpdated = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_UPDATED_SECONDS));
feed.negativeCount = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.FEED_NEGATIVE_COUNT));
feed.neutralCount = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.FEED_NEUTRAL_COUNT));
feed.positiveCount = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.FEED_POSITIVE_COUNT));
feed.subscribers = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_SUBSCRIBERS));
feed.title = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_TITLE));
return feed;
}

View file

@ -120,6 +120,9 @@ public class Story implements Serializable {
}
public static Story fromCursor(final Cursor cursor) {
if (cursor.isBeforeFirst()) {
cursor.moveToFirst();
}
Story story = new Story();
story.authors = cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_AUTHORS));
story.content = cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_CONTENT));

View file

@ -141,6 +141,13 @@ public class APIManager {
return response.getResponse(gson, NewsBlurResponse.class);
}
public NewsBlurResponse markStoryAsRead(String storyHash) {
ValueMultimap values = new ValueMultimap();
values.put(APIConstants.PARAMETER_STORY_HASH, storyHash);
APIResponse response = post(APIConstants.URL_MARK_STORIES_READ, values, false);
return response.getResponse(gson, NewsBlurResponse.class);
}
public NewsBlurResponse markStoryAsStarred(final String feedId, final String storyId) {
final ValueMultimap values = new ValueMultimap();
values.put(APIConstants.PARAMETER_FEEDID, feedId);

View file

@ -25,10 +25,6 @@ public class AppConstants {
public static final String LAST_APP_VERSION = "LAST_APP_VERSION";
// the max number of mark-as-read ops to batch up before flushing to the server
// set to 1 to effectively disable batching
public static final int MAX_MARK_READ_BATCH = 1;
// a pref for the time we completed the last full sync of the feed/fodler list
public static final String LAST_SYNC_TIME = "LAST_SYNC_TIME";

View file

@ -198,11 +198,12 @@ public class FeedUtils {
}
public static void markStoryUnread( final Story story, final Context context, final APIManager apiManager ) {
public static void markStoryUnread(final Story story, final Context context) {
// TODO: update DB, too
new AsyncTask<Void, Void, NewsBlurResponse>() {
@Override
protected NewsBlurResponse doInBackground(Void... arg) {
APIManager apiManager = new APIManager(context);
return apiManager.markStoryAsUnread(story.feedId, story.storyHash);
}
@Override
@ -214,7 +215,52 @@ public class FeedUtils {
}
}
}.execute();
}
/**
* Mark a single story as read on both the local DB and on the server.
*/
public static void markStoryAsRead(final Story story, final Context context) {
if (story.read) { return; }
// it is imperative that we are idempotent. query the DB for a fresh copy of the story
// to ensure it isn't already marked as read. if so, do not update feed counts
Uri storyUri = FeedProvider.STORY_URI.buildUpon().appendPath(story.id).build();
Cursor cursor = context.getContentResolver().query(storyUri, null, null, null, null);
if (cursor.getCount() < 1) {
Log.w(FeedUtils.class.getName(), "can't mark story as read, not found in DB: " + story.id);
return;
}
Story freshStory = Story.fromCursor(cursor);
cursor.close();
if (freshStory.read) { return; }
// update the local object to show as read even before requeried
story.read = true;
// first, update unread counts in the local DB
ArrayList<ContentProviderOperation> updateOps = new ArrayList<ContentProviderOperation>();
appendStoryReadOperations(story, updateOps);
try {
context.getContentResolver().applyBatch(FeedProvider.AUTHORITY, updateOps);
} catch (Exception e) {
Log.w(FeedUtils.class.getName(), "Could not update unread counts in local storage.", e);
}
// next, update the server
new AsyncTask<Void, Void, NewsBlurResponse>() {
@Override
protected NewsBlurResponse doInBackground(Void... arg) {
APIManager apiManager = new APIManager(context);
return apiManager.markStoryAsRead(story.storyHash);
}
@Override
protected void onPostExecute(NewsBlurResponse result) {
if (result.isError()) {
Log.e(FeedUtils.class.getName(), "Could not update unread counts via API: " + result.getErrorMessage());
}
}
}.execute();
}
/**
@ -222,7 +268,6 @@ public class FeedUtils {
* the local DB and on the server.
*/
public static void markStoriesAsRead( Collection<Story> stories, final Context context ) {
// the list of story hashes to mark read
final ArrayList<String> storyHashes = new ArrayList<String>();
// a list of local DB ops to perform
@ -261,7 +306,6 @@ public class FeedUtils {
for (Story story : stories) {
story.read = true;
}
}
private static void appendStoryReadOperations(Story story, List<ContentProviderOperation> operations) {
@ -276,20 +320,28 @@ public class FeedUtils {
} else {
selectionArgs = new String[] { DatabaseConstants.FEED_NEGATIVE_COUNT, story.feedId } ;
}
operations.add(ContentProviderOperation.newUpdate(FeedProvider.FEED_COUNT_URI).withValues(emptyValues).withSelection("", selectionArgs).build());
if (!TextUtils.isEmpty(story.socialUserId)) {
String[] socialSelectionArgs;
if (story.getIntelligenceTotal() > 0) {
socialSelectionArgs = new String[] { DatabaseConstants.SOCIAL_FEED_POSITIVE_COUNT, story.socialUserId } ;
} else if (story.getIntelligenceTotal() == 0) {
socialSelectionArgs = new String[] { DatabaseConstants.SOCIAL_FEED_NEUTRAL_COUNT, story.socialUserId } ;
} else {
socialSelectionArgs = new String[] { DatabaseConstants.SOCIAL_FEED_NEGATIVE_COUNT, story.socialUserId } ;
}
operations.add(ContentProviderOperation.newUpdate(FeedProvider.SOCIALCOUNT_URI).withValues(emptyValues).withSelection("", socialSelectionArgs).build());
}
HashSet<String> socialIds = new HashSet<String>();
if (!TextUtils.isEmpty(story.socialUserId)) {
socialIds.add(story.socialUserId);
}
if (story.friendUserIds != null) {
for (String id : story.friendUserIds) {
socialIds.add(id);
}
}
for (String id : socialIds) {
String[] socialSelectionArgs;
if (story.getIntelligenceTotal() > 0) {
socialSelectionArgs = new String[] { DatabaseConstants.SOCIAL_FEED_POSITIVE_COUNT, id } ;
} else if (story.getIntelligenceTotal() == 0) {
socialSelectionArgs = new String[] { DatabaseConstants.SOCIAL_FEED_NEUTRAL_COUNT, id } ;
} else {
socialSelectionArgs = new String[] { DatabaseConstants.SOCIAL_FEED_NEGATIVE_COUNT, id } ;
}
operations.add(ContentProviderOperation.newUpdate(FeedProvider.SOCIALCOUNT_URI).withValues(emptyValues).withSelection("", socialSelectionArgs).build());
}
Uri storyUri = FeedProvider.STORY_URI.buildUpon().appendPath(story.id).build();
ContentValues values = new ContentValues();

View file

@ -444,8 +444,8 @@ body {
color: rgba(0, 0, 0, .4);
opacity: .4;
font-size: 16px;
padding: 78px 24px 0;
position: absolute;
padding: 78px 16px 0;
margin: 48px 0;
text-shadow: 0 1px 0 rgba(255, 255, 255, .4);
top: 40%;
width: 100%;
@ -733,6 +733,10 @@ body {
opacity: 1;
display: block;
}
.NB-feedlist .feed.NB-no-hover .NB-feedlist-manage-icon,
.NB-feedlist .folder_title.NB-no-hover .NB-feedlist-manage-icon {
display: none;
}
.NB-feedlists .folder .folder_title .feed_counts_floater,
.NB-feeds-header .feed_counts_floater {
@ -793,9 +797,15 @@ body {
.NB-feedlist .feed.NB-toplevel:hover .feed_favicon {
display: none;
}
.NB-feedlist .feed.NB-toplevel.NB-no-hover .feed_favicon {
display: block;
}
.NB-feedlist .folder_title.NB-toplevel:hover {
background: none;
}
.NB-feedlist .folder_title.NB-toplevel.NB-no-hover {
background: inherit;
}
.NB-feedlist .feed_counts {
position: absolute;
@ -809,11 +819,11 @@ body {
.NB-feedlist .feed.NB-selected,
.NB-feeds-header.NB-selected,
.NB-feedlist .folder.NB-selected > .folder_title {
background-color: #FDED8D;
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#FFFFD2), to(#FDED8D));
background: -moz-linear-gradient(center top , #FFFFD2 0%, #FDED8D 100%);
border-top: 1px solid #EBE0BE;
border-bottom: 1px solid #E3D0AE;
background-color: #FFFFD2;
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#FFFFD2), to(#F5F9B4));
background: -moz-linear-gradient(center top , #FFFFD2 0%, #F5F9B4 100%);
border-top: 1px solid #D6D682;
border-bottom: 1px solid #D6D682;
}
.NB-feedlist .folder.NB-selected > .folder_title {
text-shadow: 0 1px 0 rgba(255, 255, 255, .5);
@ -1088,11 +1098,11 @@ body {
.NB-feed-story-header-info ::-moz-selection {
background: transparent;
background: transparent;
}
.NB-feed-story-header-info ::selection {
background: transparent;
background: transparent;
}
#story_titles {
@ -1757,10 +1767,7 @@ background: transparent;
color: #304080;
border-top: 1px solid #EFEEC3;
border-bottom: 1px solid #EFEEC3;
background-color: #FFFDDF;
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#FFFDEF), to(#FFFDDF));
background: -moz-linear-gradient(center top , #FFFDEF 0%, #FFFDDF 100%);
background: linear-gradient(center top , #FFFDEF 0%, #FFFDDF 100%);
background-color: #FFFEE2;
}
#story_titles .NB-story-title:hover:not(.NB-selected) {
@ -2075,11 +2082,11 @@ background: transparent;
font-weight: bold;
}
.NB-feed-story-feed .NB-feed-story-header-title:hover {
color: #DFE2F3;
color: rgba(255, 255, 255, .8);
}
.NB-inverse .NB-feed-story-feed .NB-feed-story-header-title:hover {
color: #274C63;
color: rgba(0, 0, 0, .6);
}
/* =============================== */
@ -2397,6 +2404,7 @@ background: transparent;
.NB-feed-story .NB-feed-story-content {
padding: 12px 0 0;
max-width: 700px;
min-height: 12px;
}
.NB-feed-story .NB-narrow-content .NB-feed-story-content {
margin-right: 28px;
@ -3070,6 +3078,92 @@ background: transparent;
color: #90928B;
opacity: 1;
}
/* ===================== */
/* = Sideoption - Save = */
/* ===================== */
.NB-sideoption-save-wrapper {
height: 0px;
overflow: hidden;
display: none;
}
.NB-sideoption-save-wrapper ::-moz-selection {
background: transparent;
}
.NB-sideoption-save-wrapper ::selection {
background: transparent;
}
.NB-sideoption-save-wrapper.NB-active {
display: block;
height: auto;
}
.NB-narrow-content .NB-sideoption-save-wrapper {
clear: both;
width: 100%;
margin: 0;
}
.NB-sideoption-save {
padding: 4px 12px 6px;
border: 1px solid #DBE6EA;
text-align: left;
color: #606060;
}
.NB-sideoption-save .NB-sideoption-save-icon {
float: left;
margin: 2px 2px 0 0;
width: 16px;
height: 16px;
background: transparent url("/media/embed/reader/tag.png") no-repeat 0 0;
background-size: 14px;
}
.NB-sideoption-save .NB-sideoption-save-title {
text-transform: uppercase;
font-size: 10px;
text-align: left;
text-shadow: 0 1px 0 #F6F6F6;
color: #202020;
}
.NB-sideoption-save .NB-sideoption-save-tag {
width: 100%;
margin: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.NB-sideoption-save .NB-sideoption-save-populate {
float: right;
font-size: 9px;
line-height: 11px;
text-transform: uppercase;
padding: 2px 4px;
margin: 0 0 2px 0;
border-radius: 2px;
background-color: rgba(205, 205, 205, .5);
color: white;
font-weight: bold;
cursor: pointer;
}
.NB-sideoption-save .NB-sideoption-save-populate:hover {
background-color: rgba(205, 205, 205, .8);
}
.NB-sideoption-save .NB-sideoption-save-populate:active {
background-color: rgba(205, 205, 205, 1);
}
.NB-tagging-autocomplete.ui-autocomplete {
font-size: 11px;
width: 150px !important;
}
/* ====================== */
/* = Sideoption - Share = */
/* ====================== */
.NB-sideoption-share-wrapper {
height: 0;
overflow: hidden;
@ -3153,7 +3247,9 @@ background: transparent;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
height: 52px;
height: 52px;
border-color: #C6C6C6;
border-radius: 4px;
}
.NB-sideoption-share .NB-sideoption-share-save {
font-size: 10px;
@ -3495,9 +3591,12 @@ background: transparent;
.NB-feeds-header-river-container .NB-feeds-header {
border-bottom: 1px solid #B7BBAA;
}
.NB-feeds-header-starred.NB-feeds-header {
.NB-feeds-header-starred-container {
border-top: 1px solid #B7BBAA;
}
.NB-feeds-header-starred.NB-feeds-header {
border-bottom: 1px solid #B7BBAA;
}
.NB-feeds-header-river-container .NB-feeds-header.NB-empty .NB-feeds-header-count {
display: none;
}
@ -3512,13 +3611,23 @@ background: transparent;
}
.NB-feeds-header-starred .NB-feeds-header-count {
background-color: #11448B;
background-color: #506B9A;
display: block;
padding: 3px 4px 1px;
margin: 3px 3px 0 0;
padding: 2px 3px 2px;
margin: 3px 3px 0 1px;
border-bottom: 1px solid rgba(0, 0, 0, .2);
}
.NB-starred-feeds .unread_count_positive {
background-color: #506B9A;
border-bottom: 1px solid rgba(0, 0, 0, .2);
}
.NB-starred-feeds .feed {
border-top: 1px solid #E9EBEE;
border-bottom: 1px solid #E9EBEE;
background-color: #E9EBEE;
}
.NB-feeds-header-starred.NB-empty .NB-feeds-header-count {
display: none;
}
@ -7466,6 +7575,13 @@ form.opml_import_form input {
.NB-modal-exception .NB-fieldset-fields .NB-error {
padding: 6px 0 6px 4px;
}
.NB-modal-feed-settings .NB-exception-option-status {
color: #3945C0;
font-weight: bold;
text-transform: uppercase;
opacity: 0;
}
.NB-modal-feed-settings .NB-preference-label {
float: left;
margin: 6px 0;
@ -7475,30 +7591,7 @@ form.opml_import_form input {
float: left;
overflow: hidden;
}
.NB-modal-feed-settings .NB-preference-options div {
float: left;
margin: 0 12px;
}
.NB-modal-feed-settings .NB-preference-options input[type=radio] {
float: left;
margin: 10px 4px;
}
.NB-modal-feed-settings .NB-preference-options label {
padding-left: 4px;
margin: 0 0 4px 0;
float: left;
cursor: pointer;
}
.NB-modal-feed-settings .NB-preference-options img {
height: 31px;
}
.NB-modal-feed-settings .NB-exception-option-status {
color: #3945C0;
font-weight: bold;
text-transform: uppercase;
opacity: 0;
}
/* ===================== */
/* = Feedchooser Modal = */
@ -8215,16 +8308,6 @@ form.opml_import_form input {
border-radius: 3px;
border-bottom: 1px solid rgba(0, 0, 0, .1);
}
.NB-modal-preferences .NB-preference-view {
padding-bottom: 4px;
}
.NB-modal-preferences .NB-preference-view img {
height: 31px;
min-width: 50px;
}
.NB-modal-preferences .NB-preference-view .NB-preference-options input[type="radio"] {
margin-top: 10px;
}
.NB-modal-preferences .NB-preference-story-pane-position input {
margin-top: 4px;
}
@ -8402,6 +8485,52 @@ form.opml_import_form input {
font-size: 12px;
width: 100px;
}
.NB-modal-preferences .NB-preference-autoopenfolder .NB-folders {
max-width: 240px;
}
.NB-modal-preferences .NB-preference .NB-preference-options.NB-view-settings input[type=radio] {
margin: 3px 6px 0 2px;
}
.NB-view-settings.NB-preference-options div {
float: left;
margin: 0 6px 0 0;
}
.NB-view-settings.NB-preference-options input[type=radio] {
float: left;
margin: 2px 6px 0 0px;
}
.NB-view-settings.NB-preference-options label {
margin: 0 0 4px 0;
float: left;
cursor: pointer;
text-transform: uppercase;
font-size: 12px;
color: #303030;
display: block;
padding: 4px 6px;
border: 1px solid rgba(0,0,0,.2);
border-radius: 3px;
}
.NB-preference-options.NB-view-settings img {
float: left;
width: 18px;
height: 15px;
padding: 1px 0 0 0;
margin: 0 4px 0 0;
}
.NB-preference-options.NB-view-settings .NB-view-title {
margin: 0;
padding: 1px 0 0 0;
float: left;
}
.NB-preference-options.NB-view-settings div {
float: left;
margin: 0 6px 0 0;
}
/* ================== */
/* = Account Dialog = */

110
media/css/vendor/jquery.tagit.css vendored Executable file
View file

@ -0,0 +1,110 @@
ul.tagit {
border-style: solid;
border-width: 1px;
border-color: #C6C6C6;
background: inherit;
margin-left: inherit; /* usually we don't want the regular ul margins. */
margin-right: inherit;
padding: 5px 5px 0;
overflow: hidden;
}
ul.tagit li.tagit-choice {
display: block;
margin: 2px 5px 2px 0;
cursor: pointer;
position: relative;
float: left;
font-weight: normal;
font-size: 9px;
border-radius: 4px;
line-height: 14px;
padding: 1px 16px 2px 5px;
margin: 0 4px 4px 0;
background: none;
background-color: rgba(0, 0, 0, .1);
color: #959B8B;
/* text-shadow: 0 1px 0 rgba(255, 255, 255, .5);*/
border: 1px solid transparent;
border-color: rgba(255, 255, 255, .3) transparent rgba(0, 0, 0, .1);
}
ul.tagit li.tagit-new {
padding: 0;
margin: 0;
list-style: none;
}
ul.tagit li.tagit-choice a.tagit-label {
text-decoration: none;
}
ul.tagit li.tagit-choice .tagit-close {
cursor: pointer;
position: absolute;
right: .1em;
top: 50%;
margin-top: -8px;
line-height: 17px;
}
/* used for some custom themes that don't need image icons */
ul.tagit li.tagit-choice .tagit-close .text-icon {
display: none;
}
ul.tagit li.tagit-choice a.tagit-close {
text-decoration: none;
}
ul.tagit li.tagit-choice .tagit-close {
right: .4em;
}
ul.tagit li.tagit-choice .ui-icon {
display: none;
}
ul.tagit li.tagit-choice .tagit-close .text-icon {
display: inline;
font-family: arial, sans-serif;
font-size: 16px;
line-height: 16px;
color: #777;
}
ul.tagit li.tagit-choice:hover, ul.tagit li.tagit-choice.remove {
background-color: #EDADAF;
border-color: #D6565B;
color: white;
}
ul.tagit li.tagit-choice:active {
background-color: #E6888D;
border-color: #CA404A;
color: white;
}
ul.tagit li:hover a.tagit-close .text-icon {
color: #722;
}
ul.tagit input[type="text"] {
color: #333333;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
font-size: 11px;
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
border: none;
padding: 0 1px 5px 1px;
width: inherit;
background-color: inherit;
outline: none;
}
ul.tagit li.tagit-choice input {
display: block;
float: left;
margin: 2px 5px 2px 0;
}

BIN
media/img/reader/tag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

View file

@ -14,6 +14,7 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
this.folders = new NEWSBLUR.Collections.Folders([]);
this.favicons = {};
this.stories = new NEWSBLUR.Collections.Stories();
this.starred_feeds = new NEWSBLUR.Collections.StarredFeeds();
this.queued_read_stories = {};
this.classifiers = {};
this.friends = {};
@ -226,23 +227,49 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
mark_story_as_starred: function(story_id, callback) {
var self = this;
this.starred_count += 1;
var story = this.get_story(story_id);
story.set('starred', true);
var selected = this.starred_feeds.selected();
var pre_callback = function(data) {
self.starred_feeds.reset(data.starred_counts, {parse: true});
if (selected) {
self.starred_feeds.get(selected).set('selected', true);
}
if (callback) callback(data);
};
this.make_request('/reader/mark_story_as_starred', {
story_id: story_id,
feed_id: story.get('story_feed_id')
}, callback);
story_id: story_id,
feed_id: story.get('story_feed_id'),
user_tags: story.get('user_tags')
}, pre_callback);
},
mark_story_as_unstarred: function(story_id, callback) {
var self = this;
this.starred_count -= 1;
var story = this.get_story(story_id);
story.set('starred', false);
var selected = this.starred_feeds.selected();
var pre_callback = function(data) {
self.starred_feeds.reset(data.starred_counts, {parse: true});
if (selected && self.starred_feeds.get(selected)) {
self.starred_feeds.get(selected).set('selected', true);
}
if (callback) callback(data);
};
var pre_callback = function(data) {
self.starred_feeds.reset(data.starred_counts, {parse: true, update: true});
if (callback) callback(data);
};
this.make_request('/reader/mark_story_as_unstarred', {
story_id: story_id
}, callback);
}, pre_callback);
},
mark_feed_as_read: function(feed_id, cutoff_timestamp, direction, mark_active, callback) {
@ -397,7 +424,8 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
self.folders.reset(_.compact(subscriptions.folders), {parse: true});
self.starred_count = subscriptions.starred_count;
self.social_feeds.reset(subscriptions.social_feeds);
self.starred_feeds.reset(subscriptions.starred_counts, {parse: true});
self.social_feeds.reset(subscriptions.social_feeds, {parse: true});
self.user_profile.set(subscriptions.social_profile);
self.social_services = subscriptions.social_services;
@ -514,10 +542,7 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
feed_title: data.feed_title || this.active_feed.get('feed_title'),
updated: data.updated || this.active_feed.get('updated'),
feed_address: data.feed_address || this.active_feed.get('feed_address')
}, {silent: true});
if (this.active_feed.hasChanged()) {
this.active_feed.change();
}
});
}
this.feed_id = feed_id;
this.starred_stories = data.starred_stories;
@ -555,7 +580,7 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
});
},
fetch_starred_stories: function(page, callback, error_callback, first_load) {
fetch_starred_stories: function(page, tag, callback, error_callback, first_load) {
var self = this;
var pre_callback = function(data) {
@ -566,7 +591,8 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
this.make_request('/reader/starred_stories', {
page: page,
query: NEWSBLUR.reader.flags.search
query: NEWSBLUR.reader.flags.search,
tag: tag
}, pre_callback, error_callback, {
'ajax_group': (page ? 'feed_page' : 'feed'),
'request_type': 'GET'
@ -848,8 +874,10 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
get_feed: function(feed_id) {
var self = this;
if (_.string.include(feed_id, 'social:')) {
if (_.string.startsWith(feed_id, 'social:')) {
return this.social_feeds.get(feed_id);
} else if (_.string.startsWith(feed_id, 'starred:')) {
return this.starred_feeds.get(feed_id);
} else {
return this.feeds.get(feed_id);
}
@ -870,6 +898,18 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
return this.feeds;
},
get_social_feeds: function() {
var self = this;
return this.social_feeds;
},
get_starred_feeds: function() {
var self = this;
return this.starred_feeds;
},
get_folders: function() {
var self = this;

View file

@ -69,21 +69,52 @@ NEWSBLUR.Modal.prototype = {
$.modal.close(callback);
},
make_feed_chooser: function() {
make_feed_chooser: function(options) {
options = options || {};
var $chooser = $.make('select', { name: 'feed', className: 'NB-modal-feed-chooser' });
var $feeds_optgroup = $.make('optgroup', { label: "Sites" });
var $social_feeds_optgroup = $.make('optgroup', { label: "Blurblogs" });
var $starred_feeds_optgroup = $.make('optgroup', { label: "Saved Tags" });
var current_feed_id = this.feed_id;
this.feeds = this.model.get_feeds();
this.feeds.each(function(feed) {
var make_feed_option = function(feed) {
if (!feed.get('feed_title')) return;
var $option = $.make('option', { value: feed.id }, feed.get('feed_title'));
$option.appendTo($chooser);
$option.appendTo(feed.is_starred() ? $starred_feeds_optgroup :
feed.is_social() ? $social_feeds_optgroup :
$feeds_optgroup);
if (feed.id == current_feed_id) {
$option.attr('selected', true);
}
});
};
this.feeds = this.model.get_feeds();
this.feeds.each(make_feed_option);
if (!options.skip_social) {
this.social_feeds = this.model.get_social_feeds();
this.social_feeds.each(make_feed_option);
}
if (!options.skip_starred) {
this.starred_feeds = this.model.get_starred_feeds();
this.starred_feeds.each(make_feed_option);
}
$('option', $feeds_optgroup).tsort();
$('option', $social_feeds_optgroup).tsort();
$('option', $starred_feeds_optgroup).tsort();
$chooser.append($feeds_optgroup);
if (!options.skip_social) {
$chooser.append($social_feeds_optgroup);
}
if (!options.skip_starred) {
$chooser.append($starred_feeds_optgroup);
}
$('option', $chooser).tsort();
return $chooser;
},

View file

@ -6,6 +6,10 @@ NEWSBLUR.Router = Backbone.Router.extend({
"site/:site_id/:slug": "site",
"site/:site_id/": "site",
"site/:site_id": "site",
"saved": "starred",
"saved/:tag": "starred",
"folder/saved": "starred",
"folder/saved/:tag": "starred",
"folder/:folder_name": "folder",
"folder/:folder_name/": "folder",
"social/:user_id/:slug": "social",
@ -24,7 +28,7 @@ NEWSBLUR.Router = Backbone.Router.extend({
},
site: function(site_id, slug) {
NEWSBLUR.log(["site", site_id, slug]);
// NEWSBLUR.log(["site", site_id, slug]);
site_id = parseInt(site_id, 10);
var feed = NEWSBLUR.assets.get_feed(site_id);
if (feed) {
@ -40,9 +44,18 @@ NEWSBLUR.Router = Backbone.Router.extend({
}
},
starred: function(tag) {
options = {
router: true,
tag: tag
};
console.log(["starred", options, tag]);
NEWSBLUR.reader.open_starred_stories(options);
},
folder: function(folder_name) {
folder_name = folder_name.replace(/-/g, ' ');
NEWSBLUR.log(["folder", folder_name]);
// NEWSBLUR.log(["folder", folder_name]);
var options = {router: true};
if (folder_name == "everything") {
NEWSBLUR.reader.open_river_stories(null, null, options);

View file

@ -78,6 +78,14 @@ NEWSBLUR.Models.Feed = Backbone.Model.extend({
return true;
},
parent_folder_names: function() {
var names = _.compact(_.flatten(_.map(this.folders, function(folder) {
return folder.parent_folder_names();
})));
return names;
},
rename: function(new_title) {
this.set('feed_title', new_title);
NEWSBLUR.assets.rename_feed(this.id, new_title);
@ -107,6 +115,10 @@ NEWSBLUR.Models.Feed = Backbone.Model.extend({
return true;
},
is_starred: function() {
return false;
},
is_light: function() {
var is_light = this._is_light;
if (!_.isUndefined(is_light)) {

View file

@ -204,6 +204,16 @@ NEWSBLUR.Collections.Folders = Backbone.Collection.extend({
return names;
},
parent_folder_names: function() {
var names = [this.options.title];
if (this.parent_folder) {
var parents = _.compact(_.flatten(this.parent_folder.parent_folder_names()));
names = names.concat(parents);
}
return names;
},
feed_ids_in_folder: function() {
return _.compact(_.flatten(this.map(function(item) {
return item.feed_ids_in_folder();
@ -242,15 +252,17 @@ NEWSBLUR.Collections.Folders = Backbone.Collection.extend({
});
},
unread_counts: function(sum_total) {
unread_counts: function(sum_total, seen_feeds) {
if (!seen_feeds) seen_feeds = [];
var counts = this.reduce(function(counts, item) {
if (item.is_feed()) {
if (item.is_feed() && !_.contains(seen_feeds, item.feed.id)) {
var feed_counts = item.feed.unread_counts();
counts['ps'] += feed_counts['ps'];
counts['nt'] += feed_counts['nt'];
counts['ng'] += feed_counts['ng'];
seen_feeds.push(item.feed.id);
} else if (item.is_folder()) {
var folder_counts = item.folders.unread_counts();
var folder_counts = item.folders.unread_counts(false, seen_feeds);
counts['ps'] += folder_counts['ps'];
counts['nt'] += folder_counts['nt'];
counts['ng'] += folder_counts['ng'];

View file

@ -37,6 +37,10 @@ NEWSBLUR.Models.SocialSubscription = Backbone.Model.extend({
return false;
},
is_starred: function() {
return false;
},
unread_counts: function() {
return {
ps: this.get('ps') || 0,

View file

@ -0,0 +1,83 @@
NEWSBLUR.Models.StarredFeed = Backbone.Model.extend({
initialize: function() {
this.set('feed_title', this.get('tag'));
this.views = [];
},
is_social: function() {
return false;
},
is_feed: function() {
return false;
},
is_starred: function() {
return true;
},
unread_counts: function() {
return {
ps: this.get('count') || 0,
nt: 0,
ng: 0
};
},
tag_slug: function() {
return Inflector.sluggify(this.get('tag'));
}
});
NEWSBLUR.Collections.StarredFeeds = Backbone.Collection.extend({
model: NEWSBLUR.Models.StarredFeed,
parse: function(models) {
_.each(models, function(feed) {
feed.id = 'starred:' + feed.tag;
// feed.selected = false;
feed.ps = feed.count;
});
return models;
},
comparator: function(a, b) {
var sort_order = NEWSBLUR.reader.model.preference('feed_order');
var title_a = a.get('feed_title') || '';
var title_b = b.get('feed_title') || '';
title_a = title_a.toLowerCase();
title_b = title_b.toLowerCase();
if (sort_order == 'MOSTUSED') {
var opens_a = a.get('count');
var opens_b = b.get('count');
if (opens_a > opens_b) return -1;
if (opens_a < opens_b) return 1;
}
// if (!sort_order || sort_order == 'ALPHABETICAL')
if (title_a > title_b) return 1;
else if (title_a < title_b) return -1;
return 0;
},
selected: function() {
return this.detect(function(feed) { return feed.get('selected'); });
},
deselect: function() {
this.chain().select(function(feed) {
return feed.get('selected');
}).each(function(feed){
feed.set('selected', false);
});
},
all_tags: function() {
return this.pluck('tag');
}
});

View file

@ -5,6 +5,8 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({
this.bind('change:shared_comments', this.populate_comments);
this.bind('change:comments', this.populate_comments);
this.bind('change:comment_count', this.populate_comments);
this.bind('change:starred', this.change_starred);
this.bind('change:user_tags', this.change_user_tags);
this.populate_comments();
this.story_permalink = this.get('story_permalink');
this.story_title = this.get('story_title');
@ -16,7 +18,7 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({
},
score: function() {
if (NEWSBLUR.reader.active_feed == 'starred') {
if (NEWSBLUR.reader.flags['starred_view']) {
return 1;
} else {
return NEWSBLUR.utils.compute_story_score(this);
@ -43,16 +45,6 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({
return NEWSBLUR.assets.stories.mark_read(this, options);
},
star_story: function() {
this.set('starred', !this.get('starred'));
if (this.get('starred')) {
NEWSBLUR.assets.mark_story_as_starred(this.id);
} else {
NEWSBLUR.assets.mark_story_as_unstarred(this.id);
}
NEWSBLUR.reader.update_starred_count();
},
open_story_in_new_tab: function(background) {
this.mark_read({skip_delay: true});
@ -88,6 +80,77 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({
if (model.collection) {
model.collection.detect_selected_story(model, selected);
}
},
// =================
// = Saved Stories =
// =================
toggle_starred: function() {
this.set('user_tags', this.existing_tags(), {silent: true});
if (!this.get('starred')) {
NEWSBLUR.assets.starred_count += 1;
this.set('starred', true);
} else {
NEWSBLUR.assets.starred_count -= 1;
this.set('starred', false);
}
NEWSBLUR.reader.update_starred_count();
},
change_starred: function() {
if (this.get('starred')) {
NEWSBLUR.assets.mark_story_as_starred(this.id);
} else {
NEWSBLUR.assets.mark_story_as_unstarred(this.id);
}
},
change_user_tags: function(tags, options, etc) {
NEWSBLUR.assets.mark_story_as_starred(this.id);
},
existing_tags: function() {
var tags = this.get('user_tags');
if (!tags) {
tags = this.folder_tags();
}
return tags || [];
},
unused_story_tags: function() {
var tags = _.reduce(this.get('user_tags') || [], function(m, t) {
return _.without(m, t);
}, this.get('story_tags'));
return tags;
},
folder_tags: function() {
var folder_tags = [];
var feed_id = this.get('story_feed_id');
var feed = NEWSBLUR.assets.get_feed(feed_id);
if (feed) {
folder_tags = feed.parent_folder_names();
}
return folder_tags;
},
all_tags: function() {
var tags = [];
var story_tags = this.get('story_tags') || [];
var user_tags = this.get('user_tags') || [];
var folder_tags = this.folder_tags();
var existing_tags = NEWSBLUR.assets.starred_feeds.all_tags();
var all_tags = _.unique(_.compact(_.reduce([
story_tags, user_tags, folder_tags, existing_tags
], function(x, m) {
return m.concat(x);
}, [])));
return all_tags;
}
});

View file

@ -21,6 +21,7 @@
$feed_lists: $('.NB-feedlists'),
$feed_list: $('#feed_list'),
$social_feeds: $('.NB-socialfeeds-folder'),
$starred_feeds: $('.NB-starred-folder'),
$story_titles: $('#story_titles'),
$story_titles_header: $('.NB-story-titles-header'),
$content_pane: $('.content-pane'),
@ -224,8 +225,10 @@
if ((north && north.width() < 640) ||
(content_width && content_width < 780)) {
$windows.addClass('NB-narrow');
this.flags.narrow_content = true;
} else {
$windows.removeClass('NB-narrow');
this.flags.narrow_content = false;
}
this.apply_tipsy_titles();
@ -1101,6 +1104,7 @@
'unread_threshold_temporarily': null,
'river_view': false,
'social_view': false,
'starred_view': false,
'select_story_in_feed': null,
'global_blurblogs': false,
'reloading_feeds': false
@ -1129,7 +1133,7 @@
});
if (_.isUndefined(options.search)) {
delete this.flags.search;
this.flags.search = "";
this.flags.searching = false;
}
this.model.flags['no_more_stories'] = false;
@ -1140,6 +1144,7 @@
this.$s.$river_global_header.removeClass('NB-selected');
this.$s.$tryfeed_header.removeClass('NB-selected');
this.model.feeds.deselect();
this.model.starred_feeds.deselect();
if (_.string.contains(this.active_feed, 'social:')) {
this.model.social_feeds.deselect();
}
@ -1178,7 +1183,10 @@
reload_feed: function(options) {
options = options || {};
if (this.active_feed == 'starred') {
if (this.flags['starred_view'] && this.flags['starred_tag']) {
options['tag'] = this.flags['starred_tag'];
this.open_starred_stories(options);
} else if (this.flags['starred_view']) {
this.open_starred_stories(options);
} else if (this.flags['social_view'] &&
this.active_feed == 'river:blurblogs') {
@ -1407,7 +1415,7 @@
$list.removeClass('NB-active');
}
if (feed_id == 'starred') {
if (this.flags['starred_view']) {
$page_tab.addClass('NB-disabled');
}
@ -1487,13 +1495,30 @@
this.reset_feed(options);
this.hide_splash_page();
this.active_feed = 'starred';
if (options.story_id) {
this.flags['select_story_in_feed'] = options.story_id;
}
this.iframe_scroll = null;
this.$s.$starred_header.addClass('NB-selected');
if (options.tag && !options.model) {
var model = NEWSBLUR.assets.starred_feeds.detect(function(feed) {
return feed.tag_slug() == options.tag || feed.get('tag') == options.tag;
});
if (model) {
options.model = model;
options.tag = model.get('tag');
}
}
if (options.tag) {
this.active_feed = options.model.id;
this.flags['starred_tag'] = options.model.get('tag');
options.model.set('selected', true);
} else {
this.active_feed = 'starred';
this.$s.$starred_header.addClass('NB-selected');
this.flags['starred_tag'] = null;
}
this.flags['starred_view'] = true;
this.$s.$body.addClass('NB-view-river');
this.flags.river_view = true;
$('.task_view_page', this.$s.$taskbar).addClass('NB-disabled');
@ -1512,12 +1537,24 @@
}
NEWSBLUR.app.taskbar_info.hide_stories_error();
this.model.fetch_starred_stories(1, _.bind(this.post_open_starred_stories, this),
this.model.fetch_starred_stories(1, this.flags['starred_tag'], _.bind(this.post_open_starred_stories, this),
NEWSBLUR.app.taskbar_info.show_stories_error, true);
if (!options.silent) {
var url = "/saved";
if (options.model) {
url += "/" + options.model.tag_slug();
}
if (window.location.pathname != url) {
NEWSBLUR.log(["Navigating to url", url]);
NEWSBLUR.router.navigate(url);
}
}
},
post_open_starred_stories: function(data, first_load) {
if (this.active_feed == 'starred') {
if (this.flags['starred_view']) {
// NEWSBLUR.log(['post_open_starred_stories', data.stories.length, first_load]);
this.flags['opening_feed'] = false;
if (this.counts['select_story_in_feed'] || this.flags['select_story_in_feed']) {
@ -2279,8 +2316,8 @@
NEWSBLUR.app.story_titles.show_loading(options);
}
if (this.active_feed == 'starred') {
this.model.fetch_starred_stories(this.counts['page'], _.bind(this.post_open_starred_stories, this),
if (this.flags['starred_view']) {
this.model.fetch_starred_stories(this.counts['page'], this.flags['starred_tag'], _.bind(this.post_open_starred_stories, this),
NEWSBLUR.app.taskbar_info.show_stories_error, false);
} else if (this.flags['social_view'] && _.contains(['river:blurblogs', 'river:global'], this.active_feed)) {
this.model.fetch_river_blurblogs_stories(this.active_feed,
@ -2936,6 +2973,16 @@
if (feed_id && unread_count == 0) {
$('.NB-menu-manage-feed-mark-read', $manage_menu).addClass('NB-disabled');
}
} else if (type == 'starred') {
$manage_menu = $.make('ul', { className: 'NB-menu-manage NB-menu-manage-feed' }, [
$.make('li', { className: 'NB-menu-separator-inverse' }),
$.make('li', { className: 'NB-menu-item NB-menu-manage-feed-settings' }, [
$.make('div', { className: 'NB-menu-manage-image' }),
$.make('div', { className: 'NB-menu-manage-title' }, 'Tag settings')
])
]);
$manage_menu.data('feed_id', feed_id);
$manage_menu.data('$feed', $item);
} else if (type == 'folder') {
$manage_menu = $.make('ul', { className: 'NB-menu-manage NB-menu-manage-folder' }, [
$.make('li', { className: 'NB-menu-separator-inverse' }),
@ -3198,6 +3245,9 @@
} else if (type == 'socialfeed') {
feed_id = options.feed_id;
inverse = options.inverse || $item.hasClass("NB-hover-inverse");
} else if (type == 'starred') {
feed_id = options.feed_id;
inverse = options.inverse || $item.hasClass("NB-hover-inverse");
} else if (type == 'story') {
story_id = options.story_id;
if ($item.hasClass('NB-hover-inverse')) inverse = true;
@ -3244,15 +3294,16 @@
$manage_menu_container.css('z-index', $("#simplemodal-container").css('z-index'));
}
$('.NB-task-manage').addClass('NB-hover');
} else if (type == 'feed' || type == 'folder' || type == 'story' || type == 'socialfeed') {
} else if (type == 'feed' || type == 'folder' || type == 'story' ||
type == 'socialfeed' || type == 'starred') {
var left, top;
NEWSBLUR.log(['menu open', $item, inverse, toplevel, type]);
// NEWSBLUR.log(['menu open', $item, inverse, toplevel, type]);
if (inverse) {
var $align = $item;
if (type == 'feed') {
left = toplevel ? 2 : -22;
top = toplevel ? 1 : 3;
} else if (type == 'socialfeed') {
} else if (type == 'socialfeed' || type == 'starred') {
left = 2;
top = 2;
} else if (type == 'folder') {
@ -3282,7 +3333,7 @@
left = toplevel ? 0 : -2;
top = toplevel ? 20 : 19;
$align = $('.NB-feedlist-manage-icon', $item);
} else if (type == 'socialfeed') {
} else if (type == 'socialfeed' || type == 'starred') {
left = toplevel ? 0 : -18;
top = toplevel ? 20 : 21;
$align = $('.NB-feedlist-manage-icon', $item);
@ -3307,7 +3358,8 @@
$manage_menu_container.stop().css({'display': 'block', 'opacity': 1});
// Create and position the arrow tab
if (type == 'feed' || type == 'folder' || type == 'story' || type == 'socialfeed') {
if (type == 'feed' || type == 'folder' || type == 'story' ||
type == 'socialfeed' || type == 'starred') {
var $arrow = $.make('div', { className: 'NB-menu-manage-arrow' }, [
$.make('div', { className: 'NB-icon' })
]);
@ -3371,7 +3423,7 @@
// Hide menu on scroll.
var $scroll;
this.flags['feed_list_showing_manage_menu'] = true;
if (type == 'feed' || type == 'socialfeed') {
if (type == 'feed' || type == 'socialfeed' || type == 'starred') {
$scroll = this.$s.$feed_list.parent();
} else if (type == 'story') {
$scroll = this.$s.$story_titles.add(this.$s.$feed_scroll);
@ -3761,7 +3813,7 @@
$.make('br'),
unread_view >= 1 ? 'Switch to All or Unread.' : ""
]);
$(".NB-sidebar.NB-feedlists").prepend($empty);
this.$s.$feed_list.after($empty);
}
// $focus.css('display', show_focus ? 'block' : 'none');
// if (!show_focus) {
@ -3806,8 +3858,7 @@
NEWSBLUR.app.sidebar_header.toggle_hide_read_preference();
NEWSBLUR.app.sidebar_header.count();
NEWSBLUR.assets.folders.update_all_folder_visibility();
NEWSBLUR.app.feed_list.scroll_to_show_selected_feed();
NEWSBLUR.app.feed_list.scroll_to_show_selected_folder();
NEWSBLUR.app.feed_list.scroll_to_selected();
$('.NB-active', $slider).removeClass('NB-active');
if (real_value < 0) {
@ -3890,7 +3941,7 @@
feed_id = feed_id || this.active_feed;
var feed = this.model.get_feed(feed_id);
if (feed_id == 'starred') {
if (this.flags['starred_view']) {
// Umm, no. Not yet.
} else if (feed) {
return feed.unread_counts();
@ -5133,7 +5184,7 @@
e.preventDefault();
var story_id = $t.closest('.NB-menu-manage-story').data('story_id');
var story = NEWSBLUR.assets.get_story(story_id);
story.star_story();
story.toggle_starred();
});
$.targetIs(e, { tagSelector: '.NB-menu-manage-feed-exception' }, function($t, $p){
e.preventDefault();
@ -5760,7 +5811,7 @@
if (self.active_story) {
var story_id = self.active_story.id;
var story = NEWSBLUR.assets.get_story(story_id);
story.star_story();
story.toggle_starred();
}
});
$document.bind('keypress', '+', function(e) {

View file

@ -45,7 +45,9 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
return false;
}
});
$(".NB-exception-option-page", this.$modal).toggle(this.feed.is_feed() || this.feed.is_social());
$(".NB-view-setting-original", this.$modal).toggle(this.feed.is_feed() || this.feed.is_social());
if (this.feed.get('exception_type')) {
this.$modal.removeClass('NB-modal-feed-settings');
} else {
@ -56,10 +58,13 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
},
get_feed_settings: function() {
if (this.feed.is_starred()) return;
var $loading = $('.NB-modal-loading', this.$modal);
$loading.addClass('NB-active');
var settings_fn = this.options.social_feed ? this.model.get_social_settings : this.model.get_feed_settings;
var settings_fn = this.options.social_feed ? this.model.get_social_settings :
this.model.get_feed_settings;
settings_fn.call(this.model, this.feed_id, _.bind(this.populate_settings, this));
},
@ -90,7 +95,7 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
$.make('img', { className: 'NB-modal-feed-image feed_favicon', src: $.favicon(this.feed) }),
$.make('div', { className: 'NB-modal-feed-heading' }, [
$.make('span', { className: 'NB-modal-feed-title' }, this.feed.get('feed_title')),
$.make('span', { className: 'NB-modal-feed-subscribers' },Inflector.pluralize(' subscriber', this.feed.get('num_subscribers'), true))
(this.feed.get('num_subscribers') && $.make('span', { className: 'NB-modal-feed-subscribers' },Inflector.pluralize(' subscriber', this.feed.get('num_subscribers'), true)))
])
]),
$.make('div', { className: 'NB-fieldset NB-exception-option NB-exception-option-view NB-modal-submit NB-settings-only' }, [
@ -104,23 +109,33 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
$.make('div', { className: 'NB-preference-label'}, [
'Reading view'
]),
$.make('div', { className: 'NB-preference-options' }, [
$.make('div', [
$.make('input', { id: 'NB-preference-view-1', type: 'radio', name: 'view_settings', value: 'page' }),
$.make('div', { className: 'NB-preference-options NB-view-settings' }, [
$.make('div', { className: "NB-view-setting-original" }, [
$.make('label', { 'for': 'NB-preference-view-1' }, [
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/reader/preferences_view_original.png' })
$.make('input', { id: 'NB-preference-view-1', type: 'radio', name: 'view_settings', value: 'page' }),
$.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_original_active.png' }),
$.make("div", { className: "NB-view-title" }, "Original")
])
]),
$.make('div', [
$.make('input', { id: 'NB-preference-view-2', type: 'radio', name: 'view_settings', value: 'feed' }),
$.make('label', { 'for': 'NB-preference-view-2' }, [
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/reader/preferences_view_feed.png' })
$.make('input', { id: 'NB-preference-view-2', type: 'radio', name: 'view_settings', value: 'feed' }),
$.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_feed_active.png' }),
$.make("div", { className: "NB-view-title" }, "Feed")
])
]),
$.make('div', [
$.make('input', { id: 'NB-preference-view-3', type: 'radio', name: 'view_settings', value: 'story' }),
$.make('label', { 'for': 'NB-preference-view-3' }, [
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/reader/preferences_view_story.png' })
$.make('input', { id: 'NB-preference-view-3', type: 'radio', name: 'view_settings', value: 'text' }),
$.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_text_active.png' }),
$.make("div", { className: "NB-view-title" }, "Text")
])
]),
$.make('div', [
$.make('label', { 'for': 'NB-preference-view-4' }, [
$.make('input', { id: 'NB-preference-view-4', type: 'radio', name: 'view_settings', value: 'story' }),
$.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_story_active.png' }),
$.make("div", { className: "NB-view-title" }, "Story")
])
])
])
@ -156,14 +171,14 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
]),
$.make('input', { type: 'text', id: 'NB-exception-input-address', className: 'NB-exception-input-address NB-input', name: 'feed_address', value: this.feed.get('feed_address') })
]),
(!this.options.social_feed && $.make('div', { className: 'NB-exception-submit-wrapper' }, [
(this.feed.is_feed() && $.make('div', { className: 'NB-exception-submit-wrapper' }, [
$.make('div', { className: 'NB-modal-submit-button NB-modal-submit-green NB-modal-submit-address' }, 'Parse this RSS/XML Feed'),
$.make('div', { className: 'NB-error' }),
$.make('div', { className: 'NB-exception-feed-history' })
]))
])
]),
$.make('div', { className: 'NB-fieldset NB-exception-option NB-exception-option-page NB-modal-submit' }, [
($.make('div', { className: 'NB-fieldset NB-exception-option NB-exception-option-page NB-modal-submit' }, [
$.make('h5', [
$.make('div', { className: 'NB-exception-option-meta' }),
$.make('span', { className: 'NB-exception-option-option NB-exception-only' }, 'Option 3:'),
@ -178,13 +193,13 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
]),
$.make('input', { type: 'text', id: 'NB-exception-input-link', className: 'NB-exception-input-link NB-input', name: 'feed_link', value: this.feed.get('feed_link') })
]),
(!this.options.social_feed && $.make('div', { className: 'NB-exception-submit-wrapper' }, [
(this.feed.is_feed() && $.make('div', { className: 'NB-exception-submit-wrapper' }, [
$.make('div', { className: 'NB-modal-submit-button NB-modal-submit-green NB-modal-submit-link' }, 'Fetch Feed From Website'),
$.make('div', { className: 'NB-error' }),
$.make('div', { className: 'NB-exception-page-history' })
]))
])
]),
])),
$.make('div', { className: 'NB-fieldset NB-exception-option NB-exception-option-delete NB-exception-block-only NB-modal-submit' }, [
$.make('h5', [
$.make('span', { className: 'NB-exception-option-option NB-exception-only' }, 'Option 4:'),

View file

@ -321,23 +321,33 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
]),
$.make('div', { className: 'NB-tab NB-tab-feeds' }, [
$.make('div', { className: 'NB-preference NB-preference-view' }, [
$.make('div', { className: 'NB-preference-options' }, [
$.make('div', [
$.make('input', { id: 'NB-preference-view-1', type: 'radio', name: 'default_view', value: 'page' }),
$.make('div', { className: 'NB-preference-options NB-view-settings' }, [
$.make('div', { className: "NB-view-setting-original" }, [
$.make('label', { 'for': 'NB-preference-view-1' }, [
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/reader/preferences_view_original.png' })
$.make('input', { id: 'NB-preference-view-1', type: 'radio', name: 'default_view', value: 'page' }),
$.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_original_active.png' }),
$.make("div", { className: "NB-view-title" }, "Original")
])
]),
$.make('div', [
$.make('input', { id: 'NB-preference-view-2', type: 'radio', name: 'default_view', value: 'feed' }),
$.make('label', { 'for': 'NB-preference-view-2' }, [
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/reader/preferences_view_feed.png' })
$.make('input', { id: 'NB-preference-view-2', type: 'radio', name: 'default_view', value: 'feed' }),
$.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_feed_active.png' }),
$.make("div", { className: "NB-view-title" }, "Feed")
])
]),
$.make('div', [
$.make('input', { id: 'NB-preference-view-3', type: 'radio', name: 'default_view', value: 'story' }),
$.make('label', { 'for': 'NB-preference-view-3' }, [
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/reader/preferences_view_story.png' })
$.make('input', { id: 'NB-preference-view-3', type: 'radio', name: 'default_view', value: 'text' }),
$.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_text_active.png' }),
$.make("div", { className: "NB-view-title" }, "Text")
])
]),
$.make('div', [
$.make('label', { 'for': 'NB-preference-view-4' }, [
$.make('input', { id: 'NB-preference-view-4', type: 'radio', name: 'default_view', value: 'story' }),
$.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_story_active.png' }),
$.make("div", { className: "NB-view-title" }, "Story")
])
])
]),

View file

@ -31,7 +31,7 @@ _.extend(NEWSBLUR.ReaderRecommendFeed.prototype, {
this.$modal = $.make('div', { className: 'NB-modal-recommend NB-modal' }, [
$.make('div', { className: 'NB-modal-feed-chooser-container'}, [
this.make_feed_chooser()
this.make_feed_chooser({skip_starred: true, skip_social: true})
]),
$.make('div', { className: 'NB-modal-loading' }),
$.make('h2', { className: 'NB-modal-title' }, [

View file

@ -34,7 +34,7 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, {
this.$modal = $.make('div', { className: 'NB-modal-statistics NB-modal' }, [
$.make('div', { className: 'NB-modal-feed-chooser-container'}, [
this.make_feed_chooser()
this.make_feed_chooser({skip_starred: true})
]),
$.make('div', { className: 'NB-modal-loading' }),
$.make('h2', { className: 'NB-modal-title' }, 'Statistics &amp; History'),

View file

@ -71,11 +71,13 @@ NEWSBLUR.Views.SocialPageComments = Backbone.View.extend({
format: "html"
}, _.bind(function(template) {
var $template = $($.trim(template));
var $header = this.make('div', {
"class": 'NB-story-comments-public-header-wrapper'
}, this.make('div', {
"class": 'NB-story-comments-public-header'
}, Inflector.pluralize(' public comment', $('.NB-story-comment', $template).length, true)));
var $header = $.make('div', {
className: 'NB-story-comments-public-header-wrapper'
}, [
$.make('div', {
className: 'NB-story-comments-public-header'
}, Inflector.pluralize(' public comment', $('.NB-story-comment', $template).length, true))
]);
this.$(".NB-story-comments-public-teaser-wrapper").replaceWith($template);
$template.before($header);

View file

@ -32,8 +32,12 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({
NEWSBLUR.assets.social_feeds.bind('reset', _.bind(function() {
this.make_social_feeds();
}, this));
NEWSBLUR.assets.social_feeds.bind('change:selected', this.selected, this);
NEWSBLUR.assets.feeds.bind('change:selected', this.selected, this);
NEWSBLUR.assets.starred_feeds.bind('reset', _.bind(function(models, options) {
this.make_starred_tags(options);
}, this));
NEWSBLUR.assets.social_feeds.bind('change:selected', this.scroll_to_selected, this);
NEWSBLUR.assets.feeds.bind('change:selected', this.scroll_to_selected, this);
NEWSBLUR.assets.starred_feeds.bind('change:selected', this.scroll_to_selected, this);
if (!NEWSBLUR.assets.folders.size()) {
NEWSBLUR.assets.load_feeds();
}
@ -106,7 +110,7 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({
_.defer(_.bind(function() {
NEWSBLUR.reader.open_dialog_after_feeds_loaded();
NEWSBLUR.reader.toggle_focus_in_slider();
this.selected();
this.scroll_to_selected();
if (NEWSBLUR.reader.socket) {
NEWSBLUR.reader.send_socket_active_feeds();
} else {
@ -156,6 +160,37 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({
$('.NB-module-stats-count-following .NB-module-stats-count-number').text(profile.get('following_count'));
},
make_starred_tags: function(options) {
options = options || {};
var $starred_feeds = $('.NB-starred-feeds', this.$s.$starred_feeds);
var $feeds = _.compact(NEWSBLUR.assets.starred_feeds.map(function(feed) {
if (feed.get('tag') == "") return;
var feed_view = new NEWSBLUR.Views.FeedTitleView({
model: feed,
type: 'feed',
depth: 0,
starred_tag: true
}).render();
feed.views.push(feed_view);
return feed_view.el;
}));
$starred_feeds.empty().css({
'display': 'block',
'opacity': options.update ? 1 : 0
});
$starred_feeds.html($feeds);
if (NEWSBLUR.assets.starred_feeds.length) {
$('.NB-feeds-header-starred-container').css({
'display': 'block',
'opacity': 0
}).animate({'opacity': 1}, {'duration': options.update ? 0 : 700});
}
var collapsed = NEWSBLUR.app.sidebar.check_starred_collapsed({skip_animation: true});
$starred_feeds.animate({'opacity': 1}, {'duration': (collapsed || options.update) ? 0 : 700});
},
load_router: function() {
if (!NEWSBLUR.router) {
NEWSBLUR.router = new NEWSBLUR.Router;
@ -218,51 +253,25 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({
// = Actions =
// ===========
selected: function(model, value, options) {
var feed_view;
options = options || {};
if (!model) {
model = NEWSBLUR.assets.feeds.selected() || NEWSBLUR.assets.social_feeds.selected();
}
if (!model || !model.get('selected')) return;
if (options.$feed) {
feed_view = _.detect(model.views, function(view) {
return view.el == options.$feed[0];
});
}
if (!feed_view) {
feed_view = _.detect(model.views, _.bind(function(view) {
return !!view.$el.closest(this.$s.$feed_lists).length;
}, this));
}
if (feed_view) {
_.defer(_.bind(function() {
this.scroll_to_show_selected_feed(feed_view);
}, this));
}
},
scroll_to_show_selected_feed: function(feed_view) {
scroll_to_show_selected_feed: function() {
var $feed_lists = this.$s.$feed_lists;
if (!feed_view) {
var model = NEWSBLUR.assets.feeds.selected() || NEWSBLUR.assets.social_feeds.selected();
if (!model || !model.get('selected')) return;
var feed_view = _.detect(model.views, _.bind(function(view) {
return !!view.$el.closest(this.$s.$feed_lists).length;
}, this));
if (!feed_view) return;
}
var is_feed_visible = $feed_lists.isScrollVisible(feed_view.$el);
if (!is_feed_visible) {
var model = NEWSBLUR.assets.feeds.selected() ||
NEWSBLUR.assets.social_feeds.selected() ||
NEWSBLUR.assets.starred_feeds.selected();
if (!model) return;
var feed_view = _.detect(model.views, _.bind(function(view) {
return !!view.$el.closest(this.$s.$feed_lists).length;
}, this));
if (!feed_view) return;
if (!$feed_lists.isScrollVisible(feed_view.$el)) {
var scroll = feed_view.$el.position().top;
var container = $feed_lists.scrollTop();
var height = $feed_lists.outerHeight();
$feed_lists.scrollTop(scroll+container-height/5);
}
}
return true;
},
scroll_to_show_highlighted_feed: function() {
@ -281,31 +290,40 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({
}
},
scroll_to_show_selected_folder: function(folder_view) {
scroll_to_show_selected_folder: function() {
var $feed_lists = this.$s.$feed_lists;
var $selected_view;
if (!folder_view) {
var folder = NEWSBLUR.assets.folders.selected();
if (!folder || !folder.get('selected')) return;
folder_view = folder.folder_view;
if (!folder_view) return;
var folder = NEWSBLUR.assets.folders.selected();
if (folder) {
$selected_view = folder.folder_view.$el;
$selected_view = $selected_view.find('.folder_title').eq(0);
}
var $folder_title = folder_view.$el.find('.folder_title').eq(0);
var is_folder_visible = $feed_lists.isScrollVisible($folder_title);
// NEWSBLUR.log(["scroll_to_show_selected_folder", folder_view, folder_view.$el, $feed_lists, is_folder_visible]);
if (!$selected_view && NEWSBLUR.reader.active_feed == 'river:') {
$selected_view = NEWSBLUR.reader.$s.$river_sites_header.closest(".NB-feeds-header-container");
} else if (!$selected_view && NEWSBLUR.reader.active_feed == 'starred') {
$selected_view = NEWSBLUR.reader.$s.$starred_header.closest(".NB-feeds-header-container");
}
if (!$selected_view) return;
var is_folder_visible = $feed_lists.isScrollVisible($selected_view);
if (!is_folder_visible) {
var scroll = folder_view.$el.position().top;
var scroll = $selected_view.position().top;
var container = $feed_lists.scrollTop();
var height = $feed_lists.outerHeight();
$feed_lists.scrollTop(scroll+container-height/5);
}
}
return true;
},
scroll_to_selected: function() {
this.scroll_to_show_selected_feed();
this.scroll_to_show_selected_folder();
var found = this.scroll_to_show_selected_feed();
if (!found) {
this.scroll_to_show_selected_folder();
}
},
start_sorting: function() {

View file

@ -81,7 +81,7 @@ NEWSBLUR.Views.FeedSelector = Backbone.View.extend({
});
var feeds = NEWSBLUR.assets.feeds.filter(function(feed){
return _.string.contains(feed.get('feed_title').toLowerCase(), input) || feed.id == input;
return _.string.contains(feed.get('feed_title') && feed.get('feed_title').toLowerCase(), input) || feed.id == input;
});
var socialsubs = NEWSBLUR.assets.social_feeds.filter(function(feed){
return _.string.contains(feed.get('feed_title').toLowerCase(), input) ||

View file

@ -64,7 +64,7 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({
render: function() {
var feed = this.model;
var extra_classes = this.extra_classes();
var $feed = $(_.template('<<%= list_type %> class="feed <% if (selected) { %>selected<% } %> <%= extra_classes %> <% if (toplevel) { %>NB-toplevel<% } %>" data-id="<%= feed.id %>">\
var $feed = $(_.template('<<%= list_type %> class="feed <% if (selected) { %>selected<% } %> <%= extra_classes %> <% if (toplevel) { %>NB-toplevel<% } %> <% if (disable_hover) { %>NB-no-hover<% } %>" data-id="<%= feed.id %>">\
<div class="feed_counts">\
</div>\
<% if (type == "story") { %>\
@ -102,10 +102,11 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({
', {
feed : feed,
type : this.options.type,
disable_hover : this.options.disable_hover,
extra_classes : extra_classes,
toplevel : this.options.depth == 0,
list_type : this.options.type == 'feed' ? 'li' : 'div',
selected : this.model.get('selected') || NEWSBLUR.reader.active_feed == this.model.id
selected : this.model.get('selected')
}));
if (this.options.type == 'story') {
@ -263,6 +264,11 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({
NEWSBLUR.reader.open_feed_exception_modal(this.model.id);
} else if (this.model.is_social()) {
NEWSBLUR.reader.open_social_stories(this.model.id, {force: true, $feed: this.$el});
} else if (this.model.is_starred()) {
NEWSBLUR.reader.open_starred_stories({
tag: this.model.tag_slug(),
model: this.model
});
} else {
NEWSBLUR.reader.open_feed(this.model.id, {$feed: this.$el});
}
@ -274,18 +280,22 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({
var dblclick_pref = NEWSBLUR.assets.preference('doubleclick_feed');
if (dblclick_pref == "ignore") return;
if (this.options.type == "story") return;
if (this.options.starred_tag) return;
if ($('.NB-modal-feedchooser').is(':visible')) return;
this.flags.double_click = true;
_.delay(_.bind(function() {
this.flags.double_click = false;
}, this), 500);
if (dblclick_pref == "open_and_read") {
NEWSBLUR.reader.mark_feed_as_read(this.model.id);
}
window.open(this.model.get('feed_link'), '_blank');
window.focus();
if (this.model.get('feed_link')) {
window.open(this.model.get('feed_link'), '_blank');
window.focus();
}
return false;
},
@ -297,11 +307,12 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({
},
mark_feed_as_read: function(e, days) {
if (this.options.starred_tag) return;
if (e) {
e.preventDefault();
e.stopPropagation();
}
this.flags.double_click = true;
_.delay(_.bind(function() {
this.flags.double_click = false;
@ -359,11 +370,14 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({
show_manage_menu: function(e) {
if (this.options.feed_chooser) return;
var feed_type = this.model.is_social() ? 'socialfeed' :
this.model.is_starred() ? 'starred' :
'feed';
e.preventDefault();
e.stopPropagation();
NEWSBLUR.log(["showing manage menu", this.model.is_social() ? 'socialfeed' : 'feed', $(this.el), this, e.which, e.button]);
NEWSBLUR.reader.show_manage_menu(this.model.is_social() ? 'socialfeed' : 'feed', this.$el, {
NEWSBLUR.reader.show_manage_menu(feed_type, this.$el, {
feed_id: this.model.id,
toplevel: this.options.depth == 0,
rightclick: e.which >= 2

View file

@ -89,7 +89,7 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({
// console.log(["Not a feed or folder", item]);
}
}));
$feeds.push(this.make('li', { 'class': 'feed NB-empty' }));
$feeds.push($.make('li', { className: 'feed NB-empty' }));
this.$('.folder').append($feeds);
}

View file

@ -3,6 +3,7 @@ NEWSBLUR.Views.Sidebar = Backbone.View.extend({
el: '.NB-sidebar',
events: {
"click .NB-feeds-header-starred .NB-feedlist-collapse-icon": "collapse_starred_stories",
"click .NB-feeds-header-starred": "open_starred_stories",
"click .NB-feeds-header-river-sites": "open_river_stories",
"click .NB-feeds-header-river-blurblogs .NB-feedlist-collapse-icon": "collapse_river_blurblog",
@ -16,6 +17,37 @@ NEWSBLUR.Views.Sidebar = Backbone.View.extend({
// = Actions =
// ===========
check_starred_collapsed: function(options) {
options = options || {};
var collapsed = _.contains(NEWSBLUR.Preferences.collapsed_folders, 'starred');
if (collapsed) {
this.show_collapsed_starred(options);
}
return collapsed;
},
show_collapsed_starred: function(options) {
options = options || {};
var $header = NEWSBLUR.reader.$s.$starred_header;
var $folder = this.$('.NB-starred-folder');
$header.addClass('NB-folder-collapsed');
if (!options.skip_animation) {
$header.addClass('NB-feedlist-folder-title-recently-collapsed');
$header.one('mouseover', function() {
$header.removeClass('NB-feedlist-folder-title-recently-collapsed');
});
} else {
$folder.css({
display: 'none',
opacity: 0
});
}
},
check_river_blurblog_collapsed: function(options) {
options = options || {};
var show_folder_counts = NEWSBLUR.assets.preference('folder_counts');
@ -151,6 +183,48 @@ NEWSBLUR.Views.Sidebar = Backbone.View.extend({
return false;
},
collapse_starred_stories: function(e, options) {
e.stopPropagation();
options = options || {};
var $header = NEWSBLUR.reader.$s.$starred_header;
var $folder = this.$('.NB-starred-folder');
// Hiding / Collapsing
if (options.force_collapse ||
($folder.length &&
$folder.eq(0).is(':visible'))) {
NEWSBLUR.assets.collapsed_folders('starred', true);
$header.addClass('NB-folder-collapsed');
$folder.animate({'opacity': 0}, {
'queue': false,
'duration': options.force_collapse ? 0 : 200,
'complete': _.bind(function() {
this.show_collapsed_starred();
$folder.slideUp({
'duration': 270,
'easing': 'easeOutQuart'
});
}, this)
});
}
// Showing / Expanding
else if ($folder.length &&
(!$folder.eq(0).is(':visible'))) {
NEWSBLUR.assets.collapsed_folders('starred', false);
$header.removeClass('NB-folder-collapsed');
$folder.css({'opacity': 0}).slideDown({
'duration': 240,
'easing': 'easeInOutCubic',
'complete': function() {
$folder.animate({'opacity': 1}, {'queue': false, 'duration': 200});
}
});
}
return false;
},
open_river_blurblogs_stories: function() {
return NEWSBLUR.reader.open_river_blurblogs_stories();
},

View file

@ -129,6 +129,10 @@ NEWSBLUR.Views.StoryComment = Backbone.View.extend({
NEWSBLUR.reader.open_social_profile_modal(this.model.get("user_id"));
},
toggle_feed_story_save_dialog: function() {
this.story.story_save_view.toggle_feed_story_save_dialog();
},
toggle_feed_story_share_dialog: function() {
this.story.story_share_view.toggle_feed_story_share_dialog();
},

View file

@ -23,7 +23,7 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({
"click .NB-feed-story-tag" : "save_classifier",
"click .NB-feed-story-author" : "save_classifier",
"click .NB-feed-story-train" : "open_story_trainer",
"click .NB-feed-story-save" : "star_story",
"click .NB-feed-story-save" : "toggle_starred",
"click .NB-story-comments-label" : "scroll_to_comments",
"click .NB-story-content-expander" : "expand_story"
},
@ -33,7 +33,7 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({
this.model.bind('change', this.toggle_classes, this);
this.model.bind('change:read_status', this.toggle_read_status, this);
this.model.bind('change:selected', this.toggle_selected, this);
this.model.bind('change:starred', this.toggle_starred, this);
this.model.bind('change:starred', this.render_starred, this);
this.model.bind('change:intelligence', this.render_header, this);
this.model.bind('change:intelligence', this.toggle_intelligence, this);
this.model.bind('change:shared', this.render_comments, this);
@ -66,12 +66,15 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({
render: function() {
var params = this.get_render_params();
params['story_header'] = this.story_header_template(params);
this.share_view = new NEWSBLUR.Views.StoryShareView({
this.sideoptions_view = new NEWSBLUR.Views.StorySideoptionsView({
model: this.model,
el: this.el
});
this.save_view = this.sideoptions_view.save_view;
this.share_view = this.sideoptions_view.share_view;
params['story_share_view'] = this.share_view.template({
params['story_save_view'] = this.sideoptions_view.save_view.render();
params['story_share_view'] = this.sideoptions_view.share_view.template({
story: this.model,
social_services: NEWSBLUR.assets.social_services,
profile: NEWSBLUR.assets.user_profile
@ -91,11 +94,24 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({
return this;
},
render_starred_tags: function() {
if (this.model.get('starred')) {
this.save_view.toggle_feed_story_save_dialog();
}
},
resize_starred_tags: function() {
if (this.model.get('starred')) {
this.save_view.reset_height({immediate: true});
}
},
attach_handlers: function() {
this.watch_images_for_story_height();
this.attach_audio_handler();
this.attach_syntax_highlighter_handler();
this.attach_fitvid_handler();
this.render_starred_tags();
},
render_header: function(model, value, options) {
@ -203,6 +219,7 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({
<div class="NB-sideoption-icon">&nbsp;</div>\
<div class="NB-sideoption-title"><%= story.get("starred") ? "Saved" : "Save this story" %></div>\
</div>\
<%= story_save_view %>\
<div class="NB-sideoption NB-feed-story-share">\
<div class="NB-sideoption-icon">&nbsp;</div>\
<div class="NB-sideoption-title"><%= story.get("shared") ? "Shared" : "Share this story" %></div>\
@ -417,6 +434,10 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({
},
watch_images_for_story_height: function() {
this.model.on('change:images_loaded', _.bind(function() {
this.resize_starred_tags();
}, this));
if (!this.is_truncatable()) return;
this.truncate_delay = 100;
@ -498,7 +519,7 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({
});
},
toggle_starred: function() {
render_starred: function() {
var story = this.model;
var $sideoption_title = this.$('.NB-feed-story-save .NB-sideoption-title');
@ -713,8 +734,8 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({
NEWSBLUR.reader.open_story_trainer(this.model.id, feed_id, options);
},
star_story: function() {
this.model.star_story();
toggle_starred: function() {
this.model.toggle_starred();
},
scroll_to_comments: function() {

View file

@ -34,7 +34,6 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({
feed_view_story_positions_keys: []
};
this.flags = {
feed_view_images_loaded: {},
mousemove_timeout: false
};
this.counts = {
@ -224,21 +223,23 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({
if (!NEWSBLUR.assets.stories.visible().length) {
this.show_explainer_no_stories();
return;
// return;
}
var pane_height = NEWSBLUR.reader.$s.$story_pane.height();
var indicator_position = NEWSBLUR.assets.preference('lock_mouse_indicator');
var endbar_height = 20;
if (indicator_position) {
if (indicator_position &&
_.contains(['full', 'split'], NEWSBLUR.assets.preference('story_layout'))) {
var last_visible_story = _.last(NEWSBLUR.assets.stories.visible());
var last_story_height = last_visible_story && last_visible_story.story_view && last_visible_story.story_view.$el.height() || 100;
var last_story_offset = _.last(this.cache.feed_view_story_positions_keys);
var last_story_offset = _.last(this.cache.feed_view_story_positions_keys) || 0;
endbar_height = pane_height - indicator_position - last_story_height;
if (endbar_height <= 20) endbar_height = 20;
var empty_space = pane_height - last_story_offset - last_story_height - endbar_height;
if (empty_space > 0) endbar_height += empty_space + 1;
// console.log(["endbar height full/split", endbar_height, empty_space, pane_height, last_story_offset, last_story_height]);
}
this.$('.NB-end-line').remove();
@ -246,11 +247,13 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({
var last_story = NEWSBLUR.assets.stories.last();
if (!last_story.get('selected')) return;
}
endbar_height /= 2; // Splitting padding between top and bottom
var $end_stories_line = $.make('div', {
className: 'NB-end-line'
}, [
$.make('div', { className: 'NB-fleuron' })
]).css('paddingBottom', endbar_height);
]).css('paddingBottom', endbar_height).css('paddingTop', endbar_height);
this.$el.append($end_stories_line);
},
@ -377,15 +380,24 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({
},
append_river_premium_only_notification: function() {
var $notice = $.make('div', { className: 'NB-feed-story-premium-only' }, [
$.make('div', { className: 'NB-feed-story-premium-only-text'}, [
'The full River of News is a ',
var message = [
'The full River of News is a ',
$.make('a', { href: '#', className: 'NB-splash-link' }, 'premium feature'),
'.'
];
if (NEWSBLUR.reader.flags['starred_view']) {
message = [
'Reading saved stories by tag is a ',
$.make('a', { href: '#', className: 'NB-splash-link' }, 'premium feature'),
'.'
])
];
}
var $notice = $.make('div', { className: 'NB-feed-story-premium-only' }, [
$.make('div', { className: 'NB-feed-story-premium-only-text'}, message)
]);
this.$('.NB-feed-story-premium-only').remove();
this.$(".NB-end-line").append($notice);
console.log(["append_search_premium_only_notification", this.$(".NB-end-line")]);
},
append_search_premium_only_notification: function() {
@ -405,14 +417,17 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({
// =============
is_feed_loaded_for_location_fetch: function() {
var images_begun = _.keys(this.flags.feed_view_images_loaded).length;
var images_begun = NEWSBLUR.assets.stories.any(function(s) {
return !_.isUndefined(s.get('images_loaded'));
});
if (images_begun) {
var images_loaded = _.keys(this.flags.feed_view_images_loaded).length &&
_.all(_.values(this.flags.feed_view_images_loaded), _.identity);
return !!images_loaded;
var images_loaded = NEWSBLUR.assets.stories.all(function(s) {
return s.get('images_loaded');
});
return images_loaded;
}
return !!images_begun;
return images_begun;
},
prefetch_story_locations_in_feed_view: function() {
@ -420,7 +435,7 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({
var stories = NEWSBLUR.assets.stories;
if (!_.contains(['split', 'full'], NEWSBLUR.assets.preference('story_layout'))) return;
// NEWSBLUR.log(['Prefetching Feed', this.flags['feed_view_positions_calculated'], this.flags.feed_view_images_loaded, (_.keys(this.flags.feed_view_images_loaded).length > 0 || this.cache.feed_view_story_positions_keys.length > 0), _.keys(this.flags.feed_view_images_loaded).length, _.values(this.flags.feed_view_images_loaded), this.is_feed_loaded_for_location_fetch()]);
// NEWSBLUR.log(['Prefetching Feed', this.flags['feed_view_positions_calculated'], this.is_feed_loaded_for_location_fetch()]);
if (!NEWSBLUR.assets.stories.size()) return;
@ -451,7 +466,7 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({
if (this.is_feed_loaded_for_location_fetch()) {
this.fetch_story_locations_in_feed_view({'reset_timer': true});
} else {
// NEWSBLUR.log(['Still loading feed view...', _.keys(this.flags.feed_view_images_loaded).length, this.cache.feed_view_story_positions_keys.length, this.flags.feed_view_images_loaded]);
// NEWSBLUR.log(['Still loading feed view...', this.cache.feed_view_story_positions_keys.length]);
}
},
@ -540,17 +555,17 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({
var image_count = story.story_view.$('.NB-feed-story-content img').length;
if (!image_count) {
// NEWSBLUR.log(["No images", story.get('story_title')]);
this.flags.feed_view_images_loaded[story.id] = true;
} else if (!this.flags.feed_view_images_loaded[story.id]) {
story.set('images_loaded', true);
} else if (!story.get('images_loaded')) {
// Progressively load the images in each story, so that when one story
// loads, the position is calculated and the next story can calculate
// its position (after its own images are loaded).
this.flags.feed_view_images_loaded[story.id] = false;
story.set('images_loaded', false);
(function(story, image_count) {
story.story_view.$('.NB-feed-story-content img').load(function() {
// NEWSBLUR.log(['Loaded image', story.get('story_title'), image_count]);
if (image_count <= 1) {
NEWSBLUR.app.story_list.flags.feed_view_images_loaded[story.id] = true;
story.set('images_loaded', true);
} else {
image_count--;
}

View file

@ -0,0 +1,242 @@
NEWSBLUR.Views.StorySaveView = Backbone.View.extend({
events: {
"click .NB-sideoption-save-populate" : "populate_story_tags"
},
initialize: function() {
_.bindAll(this, 'toggle_feed_story_save_dialog');
this.sideoptions_view = this.options.sideoptions_view;
this.model.story_save_view = this;
this.model.bind('change:starred', this.toggle_feed_story_save_dialog);
},
render: function() {
return this.template({
story: this.model,
tags: this.model.existing_tags(),
story_tags: this.model.unused_story_tags(),
social_services: NEWSBLUR.assets.social_services,
profile: NEWSBLUR.assets.user_profile
});
},
template: _.template('\
<div class="NB-sideoption-save-wrapper <% if (story.get("starred")) { %>NB-active<% } %>">\
<div class="NB-sideoption-save">\
<% if (story_tags.length) { %>\
<div class="NB-sideoption-save-populate">\
Add <%= Inflector.pluralize("story tag", story_tags.length, true) %>\
</div>\
<% } %>\
<div class="NB-sideoption-save-icon"></div>\
<div class="NB-sideoption-save-title">\
Tags:\
</div>\
<ul class="NB-sideoption-save-tag">\
<% _.each(tags, function(tag) { %>\
<li><%= tag %></li>\
<% }) %>\
</ul>\
</div>\
</div>\
'),
populate_story_tags: function() {
var $populate = this.$('.NB-sideoption-save-populate');
var $tag_input = this.$('.NB-sideoption-save-tag');
var tags = this.model.get('story_tags');
$populate.fadeOut(500);
_.each(tags, function(tag) {
$tag_input.tagit('createTag', tag, null, true);
});
this.toggle_feed_story_save_dialog({resize_open:true});
this.save_tags();
},
toggle_feed_story_save_dialog: function(options) {
options = options || {};
var self = this;
var feed_id = this.model.get('story_feed_id');
var $sideoption = this.$('.NB-sideoption.NB-feed-story-save');
var $save_wrapper = this.$('.NB-sideoption-save-wrapper');
var $tag_input = this.$('.NB-sideoption-save-tag');
if (options.close || !this.model.get('starred')) {
// Close
this.is_open = false;
this.resize({close: true});
} else {
// Open/resize
this.is_open = true;
if (!options.resize_open) {
this.$('.NB-error').remove();
}
$tag_input.tagit({
fieldName: "tags",
availableTags: this.model.all_tags(),
autocomplete: {delay: 0, minLength: 0},
showAutocompleteOnFocus: false,
createTagOnBlur: false,
removeConfirmation: true,
caseSensitive: false,
allowDuplicates: false,
allowSpaces: true,
readOnly: false,
tagLimit: null,
singleField: false,
singleFieldDelimiter: ',',
singleFieldNode: null,
tabIndex: null,
afterTagAdded: function(event, options) {
options = options || {};
if (!options.duringInitialization) {
self.resize({change_tag: true});
self.save_tags();
}
},
afterTagRemoved: function(event, duringInitialization) {
options = options || {};
if (!options.duringInitialization) {
self.resize({change_tag: true});
self.save_tags();
}
}
});
$tag_input.tagit('addClassAutocomplete', 'NB-tagging-autocomplete');
if (options.animate_scroll) {
var $scroll_container = NEWSBLUR.reader.$s.$story_titles;
if (_.contains(['split', 'full'], NEWSBLUR.assets.preference('story_layout'))) {
$scroll_container = this.model.latest_story_detail_view.$el.parent();
}
$scroll_container.stop().scrollTo(this.$el, {
duration: 600,
queue: false,
easing: 'easeInOutQuint',
offset: this.model.latest_story_detail_view.$el.height() -
$scroll_container.height()
});
}
this.resize(options);
}
},
resize: function(options) {
options = options || {};
var $sideoption_container = this.$('.NB-feed-story-sideoptions-container');
var $save_wrapper = this.$('.NB-sideoption-save-wrapper');
var $save_content = this.$('.NB-sideoption-save');
var $story_content = this.$('.NB-feed-story-content,.NB-story-content');
var $story_comments = this.$('.NB-feed-story-comments');
var $sideoption = this.$('.NB-feed-story-save');
var $tag_input = this.$('.NB-sideoption-save-tag');
var $save_clone = $save_wrapper.clone();
$save_wrapper.after($save_clone.css({
'height': options.close ? 0 : 'auto',
'position': 'absolute',
'visibility': 'hidden',
'display': 'block'
}));
var sideoption_content_height = $save_clone.height();
$save_clone.remove();
var new_sideoptions_height = $sideoption_container.height() - $save_wrapper.height() + sideoption_content_height;
if (!options.close) {
$sideoption.addClass('NB-active');
$save_wrapper.addClass('NB-active');
}
if (!options.resize_open && !options.close && !options.change_tag) {
$save_wrapper.css('height', '0px');
}
$save_wrapper.animate({
'height': sideoption_content_height
}, {
'duration': options.immediate ? 0 : 350,
'easing': 'easeInOutQuint',
'queue': false,
'complete': _.bind(function() {
if ($tag_input.length == 1) {
$tag_input.focus();
}
if (NEWSBLUR.app.story_list) {
NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view();
}
if (options.close) {
$sideoption.removeClass('NB-active');
$save_wrapper.removeClass('NB-active');
}
}, this)
});
var sideoptions_height = $sideoption_container.height();
var content_height = $story_content.height();
var comments_height = $story_comments.height();
var left_height = content_height + comments_height;
var original_height = $story_content.data('original_height') || content_height;
if (!NEWSBLUR.reader.flags.narrow_content &&
!options.close && !options.force && new_sideoptions_height >= original_height) {
// Sideoptions too big, embiggen left side
$story_content.stop(true, true).animate({
'height': new_sideoptions_height
}, {
'duration': 350,
'easing': 'easeInOutQuint',
'queue': false,
'complete': function() {
if (NEWSBLUR.app.story_list) {
NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view();
}
}
});
if (!$story_content.data('original_height')) {
$story_content.data('original_height', content_height);
}
} else if (!NEWSBLUR.reader.flags.narrow_content) {
// Content is bigger, move content back to normal
if ($story_content.data('original_height') && !this.sideoptions_view.share_view.is_open) {
$story_content.stop(true, true).animate({
'height': $story_content.data('original_height')
}, {
'duration': 300,
'easing': 'easeInOutQuint',
'queue': false,
'complete': function() {
if (NEWSBLUR.app.story_list) {
NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view();
}
}
});
} else if (this.sideoptions_view.share_view.is_open) {
this.sideoptions_view.share_view.resize();
}
}
if (NEWSBLUR.app.story_list) {
NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view();
}
},
reset_height: function() {
var $story_content = this.$('.NB-feed-story-content,.NB-story-content');
// Reset story content height to get an accurate height measurement.
$story_content.stop(true, true).css('height', 'auto');
$story_content.removeData('original_height');
this.resize({change_tag: true});
},
save_tags: function() {
var $tag_input = this.$('.NB-sideoption-save-tag');
var user_tags = $tag_input.tagit('assignedTags');
this.model.set('user_tags', user_tags);
}
});

View file

@ -12,6 +12,7 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({
},
initialize: function() {
this.sideoptions_view = this.options.sideoptions_view;
this.model.story_share_view = this;
},
@ -67,38 +68,11 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({
if (options.close ||
($sideoption.hasClass('NB-active') && !options.resize_open)) {
// Close
$share.animate({
'height': 0
}, {
'duration': 300,
'easing': 'easeInOutQuint',
'queue': false,
'complete': _.bind(function() {
this.$('.NB-error').remove();
if (NEWSBLUR.app.story_list) {
NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view();
}
}, this)
});
$comment_input.blur();
$sideoption.removeClass('NB-active');
if ($story_content.data('original_height')) {
$story_content.animate({
'height': $story_content.data('original_height')
}, {
'duration': 300,
'easing': 'easeInOutQuint',
'queue': false,
'complete': function() {
if (NEWSBLUR.app.story_list) {
NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view();
}
}
});
$story_content.removeData('original_height');
}
this.is_open = false;
this.resize({close: true});
} else {
// Open/resize
this.is_open = true;
if (!options.resize_open) {
this.$('.NB-error').remove();
}
@ -108,14 +82,6 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({
$facebook_button.removeClass('NB-active');
$appdotnet_button.removeClass('NB-active');
this.update_share_button_label();
var $share_clone = $share.clone();
var dialog_height = $share_clone.css({
'height': 'auto',
'position': 'absolute',
'visibility': 'hidden'
}).appendTo($share.parent()).height();
$share_clone.remove();
if (options.animate_scroll) {
var $scroll_container = NEWSBLUR.reader.$s.$story_titles;
@ -130,61 +96,9 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({
$scroll_container.height()
});
}
$share.animate({
'height': dialog_height
}, {
'duration': options.immediate ? 0 : 350,
'easing': 'easeInOutQuint',
'queue': false,
'complete': _.bind(function() {
if ($comment_input.length == 1) {
$comment_input.focus();
}
if (NEWSBLUR.app.story_list) {
NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view();
}
}, this)
});
var sideoptions_height = $sideoption_container.outerHeight(true);
var wrapper_height = $story_wrapper.height();
var content_height = $story_content.height();
var content_outerheight = $story_content.outerHeight(true);
var comments_height = $story_comments.outerHeight(true);
var container_offset = $sideoption_container.length &&
($sideoption_container.position().top - 32);
if (content_outerheight + comments_height < sideoptions_height) {
$story_content.css('height', $sideoption_container.height());
$story_content.animate({
'height': sideoptions_height + dialog_height - comments_height
}, {
'duration': 350,
'easing': 'easeInOutQuint',
'queue': false,
'complete': function() {
if (NEWSBLUR.app.story_list) {
NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view();
}
}
}).data('original_height', content_height);
} else if (sideoptions_height + dialog_height > wrapper_height) {
$story_content.animate({
'height': content_height + dialog_height - container_offset
}, {
'duration': 350,
'easing': 'easeInOutQuint',
'queue': false,
'complete': function() {
if (NEWSBLUR.app.story_list) {
NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view();
}
}
}).data('original_height', content_height);
} else if (NEWSBLUR.app.story_list) {
NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view();
}
this.resize(options);
var share = _.bind(function(e) {
e.preventDefault();
this.mark_story_as_shared({'source': 'sideoption'});
@ -196,6 +110,99 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({
}
},
resize: function(options) {
options = options || {};
var $sideoption_container = this.$('.NB-feed-story-sideoptions-container');
var $share_wrapper = this.$('.NB-sideoption-share-wrapper');
var $share_content = this.$('.NB-sideoption-share');
var $story_content = this.$('.NB-feed-story-content,.NB-story-content');
var $story_comments = this.$('.NB-feed-story-comments');
var $sideoption = this.$('.NB-sideoption.NB-feed-story-share');
var $share_clone = $share_wrapper.clone();
$share_wrapper.after($share_clone.css({
'height': options.close ? 0 : 'auto',
'position': 'absolute',
'visibility': 'hidden',
'display': 'block'
}));
var sideoption_content_height = $share_clone.height();
$share_clone.remove();
var new_sideoptions_height = $sideoption_container.height() - $share_wrapper.height() + sideoption_content_height;
if (!options.close) {
$share_wrapper.addClass('NB-active');
$sideoption.addClass('NB-active');
}
$share_wrapper.animate({
'height': sideoption_content_height
}, {
'duration': options.immediate ? 0 : 350,
'easing': 'easeInOutQuint',
'queue': false,
'complete': _.bind(function() {
if (NEWSBLUR.app.story_list) {
NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view();
}
if (options.close) {
$share_wrapper.removeClass('NB-active');
$sideoption.removeClass('NB-active');
}
}, this)
});
var sideoptions_height = $sideoption_container.height();
var content_height = $story_content.height();
var comments_height = $story_comments.height();
var left_height = content_height + comments_height;
var original_height = $story_content.data('original_height') || content_height;
if (!NEWSBLUR.reader.flags.narrow_content &&
!options.close && new_sideoptions_height >= original_height) {
// Sideoptions too big, embiggen left side
$story_content.animate({
'height': new_sideoptions_height
}, {
'duration': 350,
'easing': 'easeInOutQuint',
'queue': false,
'complete': function() {
if (NEWSBLUR.app.story_list) {
NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view();
}
}
});
if (!$story_content.data('original_height')) {
$story_content.data('original_height', content_height);
}
} else if (!NEWSBLUR.reader.flags.narrow_content) {
// Content is bigger, move content back to normal
if ($story_content.data('original_height') && !this.sideoptions_view.save_view.is_open) {
$story_content.animate({
'height': $story_content.data('original_height')
}, {
'duration': 300,
'easing': 'easeInOutQuint',
'queue': false,
'complete': function() {
if (NEWSBLUR.app.story_list) {
NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view();
}
}
});
} else if (this.sideoptions_view &&
this.sideoptions_view.save_view &&
this.sideoptions_view.save_view.is_open) {
this.sideoptions_view.save_view.resize();
}
}
if (NEWSBLUR.app.story_list) {
NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view();
}
},
mark_story_as_shared: function(options) {
options = options || {};
var $share_button = this.$('.NB-sideoption-share-save');

View file

@ -0,0 +1,16 @@
NEWSBLUR.Views.StorySideoptionsView = Backbone.View.extend({
initialize: function() {
this.save_view = new NEWSBLUR.Views.StorySaveView({
model: this.model,
el: this.el,
sideoptions_view: this
});
this.share_view = new NEWSBLUR.Views.StoryShareView({
model: this.model,
el: this.el,
sideoptions_view: this
});
}
});

View file

@ -76,6 +76,7 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({
});
this.text_view.fetch_and_render(this.model, temporary_text);
this.$(".NB-story-detail").html(this.text_view.$el);
this.story_detail.render_starred_tags();
} else {
this.story_detail = new NEWSBLUR.Views.StoryDetailView({
model: this.model,

View file

@ -32,18 +32,22 @@ NEWSBLUR.Views.StoryTitlesHeader = Backbone.View.extend({
NEWSBLUR.reader.active_folder &&
(NEWSBLUR.reader.active_folder.get('fake') || !NEWSBLUR.reader.active_folder.get('folder_title'));
if (NEWSBLUR.reader.active_feed == 'starred') {
if (NEWSBLUR.reader.flags['starred_view']) {
$view = $(_.template('\
<div class="NB-folder NB-no-hover">\
<div class="NB-search-container"></div>\
<div class="NB-starred-icon"></div>\
<div class="NB-feedlist-manage-icon"></div>\
<div class="folder_title_text">Saved Stories</div>\
<span class="folder_title_text">Saved Stories<% if (tag) { %> - <%= tag %><% } %></span>\
</div>\
', {}));
', {
tag: NEWSBLUR.reader.flags['starred_tag']
}));
this.search_view = new NEWSBLUR.Views.FeedSearchView({
feedbar_view: this
}).render();
$view.prepend(this.search_view.$el);
this.search_view.blur_search();
$(".NB-search-container", $view).html(this.search_view.$el);
} else if (this.showing_fake_folder) {
$view = $(_.template('\
<div class="NB-folder NB-no-hover">\
@ -128,7 +132,8 @@ NEWSBLUR.Views.StoryTitlesHeader = Backbone.View.extend({
var $indicator = this.$('.NB-story-title-indicator');
var unread_hidden_stories;
if (NEWSBLUR.reader.flags['river_view']) {
unread_hidden_stories = NEWSBLUR.reader.active_folder.folders &&
unread_hidden_stories = NEWSBLUR.reader.active_folder &&
NEWSBLUR.reader.active_folder.folders &&
NEWSBLUR.reader.active_folder.folders.unread_counts &&
NEWSBLUR.reader.active_folder.folders.unread_counts().ng;
} else {

View file

@ -67,12 +67,20 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({
},
append_river_premium_only_notification: function() {
var $notice = $.make('div', { className: 'NB-feed-story-premium-only' }, [
$.make('div', { className: 'NB-feed-story-premium-only-text'}, [
'The full River of News is a ',
var message = [
'The full River of News is a ',
$.make('a', { href: '#', className: 'NB-splash-link' }, 'premium feature'),
'.'
];
if (NEWSBLUR.reader.flags['starred_view']) {
message = [
'Reading saved stories by tag is a ',
$.make('a', { href: '#', className: 'NB-splash-link' }, 'premium feature'),
'.'
])
];
}
var $notice = $.make('div', { className: 'NB-feed-story-premium-only' }, [
$.make('div', { className: 'NB-feed-story-premium-only-text'}, message)
]);
this.$('.NB-feed-story-premium-only').remove();
this.$(".NB-end-line").append($notice);
@ -193,14 +201,17 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({
if (NEWSBLUR.assets.preference('story_layout') == 'list') {
var pane_height = NEWSBLUR.reader.$s.$story_titles.height();
var endbar_height = 20;
var last_story_height = 100;
var last_story_height = 280;
endbar_height = pane_height - last_story_height;
if (endbar_height <= 20) endbar_height = 20;
var empty_space = pane_height - last_story_height - endbar_height;
if (empty_space > 0) endbar_height += empty_space + 1;
endbar_height /= 2; // Splitting padding between top and bottom
$end_stories_line.css('paddingBottom', endbar_height);
$end_stories_line.css('paddingTop', endbar_height);
// console.log(["endbar height list", endbar_height, empty_space, pane_height, last_story_height]);
}
this.$el.append($end_stories_line);

File diff suppressed because it is too large Load diff

View file

@ -257,7 +257,8 @@ NEWSBLUR.log = function(msg) {
else if (empty_on_missing) return 'data:image/png;base64,R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==';
else if (_.isNumber(feed.id)) return NEWSBLUR.URLs.favicon.replace('{id}', feed.id);
else if (feed.get('favicon_url')) return feed.get('favicon_url');
return NEWSBLUR.Globals.MEDIA_URL + '/img/silk/circular/world.png';
else if (feed.is_starred()) return NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tag.png';
return NEWSBLUR.Globals.MEDIA_URL + '/img/icons/circular/world.png';
},
deepCopy: function(obj) {

557
media/js/vendor/tag-it.js vendored Executable file
View file

@ -0,0 +1,557 @@
/*
* jQuery UI Tag-it!
*
* @version v2.0 (06/2011)
*
* Copyright 2011, Levy Carneiro Jr.
* Released under the MIT license.
* http://aehlke.github.com/tag-it/LICENSE
*
* Homepage:
* http://aehlke.github.com/tag-it/
*
* Authors:
* Levy Carneiro Jr.
* Martin Rehfeld
* Tobias Schmidt
* Skylar Challand
* Alex Ehlke
*
* Maintainer:
* Alex Ehlke - Twitter: @aehlke
*
* Dependencies:
* jQuery v1.4+
* jQuery UI v1.8+
*/
(function($) {
$.widget('ui.tagit', {
options: {
allowDuplicates : false,
caseSensitive : true,
fieldName : 'tags',
placeholderText : null, // Sets `placeholder` attr on input field.
readOnly : false, // Disables editing.
removeConfirmation: false, // Require confirmation to remove tags.
tagLimit : null, // Max number of tags allowed (null for unlimited).
createTagOnBlur : true, // Create a tag when input loses focus.
// Used for autocomplete, unless you override `autocomplete.source`.
availableTags : [],
// Use to override or add any options to the autocomplete widget.
//
// By default, autocomplete.source will map to availableTags,
// unless overridden.
autocomplete: {},
// Shows autocomplete before the user even types anything.
showAutocompleteOnFocus: false,
// When enabled, quotes are unneccesary for inputting multi-word tags.
allowSpaces: false,
// The below options are for using a single field instead of several
// for our form values.
//
// When enabled, will use a single hidden field for the form,
// rather than one per tag. It will delimit tags in the field
// with singleFieldDelimiter.
//
// The easiest way to use singleField is to just instantiate tag-it
// on an INPUT element, in which case singleField is automatically
// set to true, and singleFieldNode is set to that element. This
// way, you don't need to fiddle with these options.
singleField: false,
// This is just used when preloading data from the field, and for
// populating the field with delimited tags as the user adds them.
singleFieldDelimiter: ',',
// Set this to an input DOM node to use an existing form field.
// Any text in it will be erased on init. But it will be
// populated with the text of tags as they are created,
// delimited by singleFieldDelimiter.
//
// If this is not set, we create an input node for it,
// with the name given in settings.fieldName.
singleFieldNode: null,
// Whether to animate tag removals or not.
animate: true,
// Optionally set a tabindex attribute on the input that gets
// created for tag-it.
tabIndex: null,
// Event callbacks.
beforeTagAdded : null,
afterTagAdded : null,
beforeTagRemoved : null,
afterTagRemoved : null,
onTagClicked : null,
onTagLimitExceeded : null,
// DEPRECATED:
//
// /!\ These event callbacks are deprecated and WILL BE REMOVED at some
// point in the future. They're here for backwards-compatibility.
// Use the above before/after event callbacks instead.
onTagAdded : null,
onTagRemoved: null,
// `autocomplete.source` is the replacement for tagSource.
tagSource: null
// Do not use the above deprecated options.
},
_create: function() {
// for handling static scoping inside callbacks
var that = this;
// There are 2 kinds of DOM nodes this widget can be instantiated on:
// 1. UL, OL, or some element containing either of these.
// 2. INPUT, in which case 'singleField' is overridden to true,
// a UL is created and the INPUT is hidden.
if (this.element.is('input')) {
this.tagList = $('<ul></ul>').insertAfter(this.element);
this.options.singleField = true;
this.options.singleFieldNode = this.element;
this.element.css('display', 'none');
} else {
this.tagList = this.element.find('ul, ol').andSelf().last();
}
this.tagInput = $('<input type="text" />').addClass('ui-widget-content');
if (this.options.readOnly) this.tagInput.attr('disabled', 'disabled');
if (this.options.tabIndex) {
this.tagInput.attr('tabindex', this.options.tabIndex);
}
if (this.options.placeholderText) {
this.tagInput.attr('placeholder', this.options.placeholderText);
}
if (!this.options.autocomplete.source) {
this.options.autocomplete.source = function(search, showChoices) {
var filter = search.term.toLowerCase();
var choices = $.grep(this.options.availableTags, function(element) {
// Only match autocomplete options that begin with the search term.
// (Case insensitive.)
return (element.toLowerCase().indexOf(filter) === 0);
});
if (!this.options.allowDuplicates) {
choices = this._subtractArray(choices, this.assignedTags());
}
showChoices(choices);
};
}
if (this.options.showAutocompleteOnFocus) {
this.tagInput.focus(function(event, ui) {
that._showAutocomplete();
});
if (typeof this.options.autocomplete.minLength === 'undefined') {
this.options.autocomplete.minLength = 0;
}
}
// Bind autocomplete.source callback functions to this context.
if ($.isFunction(this.options.autocomplete.source)) {
this.options.autocomplete.source = $.proxy(this.options.autocomplete.source, this);
}
// DEPRECATED.
if ($.isFunction(this.options.tagSource)) {
this.options.tagSource = $.proxy(this.options.tagSource, this);
}
this.tagList
.addClass('tagit')
.addClass('ui-widget ui-widget-content ui-corner-all')
// Create the input field.
.append($('<li class="tagit-new"></li>').append(this.tagInput))
.click(function(e) {
var target = $(e.target);
if (target.hasClass('tagit-label')) {
var tag = target.closest('.tagit-choice');
if (!tag.hasClass('removed')) {
that._trigger('onTagClicked', e, {tag: tag, tagLabel: that.tagLabel(tag)});
}
} else {
// Sets the focus() to the input field, if the user
// clicks anywhere inside the UL. This is needed
// because the input field needs to be of a small size.
that.tagInput.focus();
}
});
// Single field support.
var addedExistingFromSingleFieldNode = false;
if (this.options.singleField) {
if (this.options.singleFieldNode) {
// Add existing tags from the input field.
var node = $(this.options.singleFieldNode);
var tags = node.val().split(this.options.singleFieldDelimiter);
node.val('');
$.each(tags, function(index, tag) {
that.createTag(tag, null, true);
addedExistingFromSingleFieldNode = true;
});
} else {
// Create our single field input after our list.
this.options.singleFieldNode = $('<input type="hidden" style="display:none;" value="" name="' + this.options.fieldName + '" />');
this.tagList.after(this.options.singleFieldNode);
}
}
// Add existing tags from the list, if any.
if (!addedExistingFromSingleFieldNode) {
this.tagList.children('li').each(function() {
if (!$(this).hasClass('tagit-new')) {
that.createTag($(this).text(), $(this).attr('class'), true);
$(this).remove();
}
});
}
// Events.
this.tagInput
.keydown(function(event) {
// Backspace is not detected within a keypress, so it must use keydown.
if (event.which == $.ui.keyCode.BACKSPACE && that.tagInput.val() === '') {
var tag = that._lastTag();
if (!that.options.removeConfirmation || tag.hasClass('remove')) {
// When backspace is pressed, the last tag is deleted.
that.removeTag(tag);
} else if (that.options.removeConfirmation) {
tag.addClass('remove ui-state-highlight');
}
} else if (that.options.removeConfirmation) {
that._lastTag().removeClass('remove ui-state-highlight');
}
// Comma/Space/Enter are all valid delimiters for new tags,
// except when there is an open quote or if setting allowSpaces = true.
// Tab will also create a tag, unless the tag input is empty,
// in which case it isn't caught.
if (
event.which === $.ui.keyCode.COMMA ||
event.which === $.ui.keyCode.ENTER ||
(
event.which == $.ui.keyCode.TAB &&
that.tagInput.val() !== ''
) ||
(
event.which == $.ui.keyCode.SPACE &&
that.options.allowSpaces !== true &&
(
$.trim(that.tagInput.val()).replace( /^s*/, '' ).charAt(0) != '"' ||
(
$.trim(that.tagInput.val()).charAt(0) == '"' &&
$.trim(that.tagInput.val()).charAt($.trim(that.tagInput.val()).length - 1) == '"' &&
$.trim(that.tagInput.val()).length - 1 !== 0
)
)
)
) {
// Enter submits the form if there's no text in the input.
if (!(event.which === $.ui.keyCode.ENTER && that.tagInput.val() === '')) {
event.preventDefault();
}
// Autocomplete will create its own tag from a selection and close automatically.
var menu = that.tagInput.autocomplete('widget').data('ui-menu');
if (!(that.tagInput.data('autocomplete-open') &&
that.tagInput.data('autocomplete-focus'))) {
that.createTag(that._cleanedInput());
}
}
}).blur(function(e){
// Create a tag when the element loses focus.
// If autocomplete is enabled and suggestion was clicked, don't add it.
if (!this.createTagOnBlur) return;
if (!(that.tagInput.data('autocomplete-open') &&
that.tagInput.data('autocomplete-focus'))) {
that.createTag(that._cleanedInput());
}
});
// Autocomplete.
if (this.options.availableTags || this.options.tagSource || this.options.autocomplete.source) {
var autocompleteOptions = {
select: function(event, ui) {
that.createTag(ui.item.value);
// Preventing the tag input to be updated with the chosen value.
return false;
}
};
$.extend(autocompleteOptions, this.options.autocomplete);
// tagSource is deprecated, but takes precedence here since autocomplete.source is set by default,
// while tagSource is left null by default.
autocompleteOptions.source = this.options.tagSource || autocompleteOptions.source;
this.tagInput.autocomplete(autocompleteOptions).bind('autocompleteopen', function(event, ui) {
that.tagInput.data('autocomplete-open', true);
}).bind('autocompleteclose', function(event, ui) {
that.tagInput.data('autocomplete-open', false);
}).bind('autocompletefocus', function(event, ui) {
that.tagInput.data('autocomplete-focus', true);
}).bind('autocompletefocus', function(event, ui) {
that.tagInput.data('autocomplete-focus', false);
});
}
},
_cleanedInput: function() {
// Returns the contents of the tag input, cleaned and ready to be passed to createTag
return $.trim(this.tagInput.val().replace(/^"(.*)"$/, '$1'));
},
_lastTag: function() {
return this.tagList.find('.tagit-choice:last:not(.removed)');
},
_tags: function() {
return this.tagList.find('.tagit-choice:not(.removed)');
},
assignedTags: function() {
// Returns an array of tag string values
var that = this;
var tags = [];
if (this.options.singleField) {
tags = $(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter);
if (tags[0] === '') {
tags = [];
}
} else {
this._tags().each(function() {
tags.push(that.tagLabel(this));
});
}
return tags;
},
_updateSingleTagsField: function(tags) {
// Takes a list of tag string values, updates this.options.singleFieldNode.val to the tags delimited by this.options.singleFieldDelimiter
$(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter)).trigger('change');
},
_subtractArray: function(a1, a2) {
var result = [];
for (var i = 0; i < a1.length; i++) {
if ($.inArray(a1[i], a2) == -1) {
result.push(a1[i]);
}
}
return result;
},
tagLabel: function(tag) {
// Returns the tag's string label.
if (this.options.singleField) {
return $(tag).find('.tagit-label:first').text();
} else {
return $(tag).find('input:first').val();
}
},
_showAutocomplete: function() {
this.tagInput.autocomplete('search', '');
},
addClassAutocomplete: function(className) {
this.tagInput.autocomplete('widget').addClass(className);
},
_findTagByLabel: function(name) {
var that = this;
var tag = null;
this._tags().each(function(i) {
if (that._formatStr(name) == that._formatStr(that.tagLabel(this))) {
tag = $(this);
return false;
}
});
return tag;
},
_isNew: function(name) {
return !this._findTagByLabel(name);
},
_formatStr: function(str) {
if (this.options.caseSensitive) {
return str;
}
return $.trim(str.toLowerCase());
},
_effectExists: function(name) {
return Boolean($.effects && ($.effects[name] || ($.effects.effect && $.effects.effect[name])));
},
createTag: function(value, additionalClass, duringInitialization) {
var that = this;
value = $.trim(value);
if (this.tagInput.data('autocomplete-open')) {
this.tagInput.autocomplete('close');
}
if(this.options.preprocessTag) {
value = this.options.preprocessTag(value);
}
if (value === '') {
return false;
}
if (!this.options.allowDuplicates && !this._isNew(value)) {
var existingTag = this._findTagByLabel(value);
if (this._trigger('onTagExists', null, {
existingTag: existingTag,
duringInitialization: duringInitialization
}) !== false) {
if (this._effectExists('highlight')) {
existingTag.effect('highlight');
}
}
return false;
}
if (this.options.tagLimit && this._tags().length >= this.options.tagLimit) {
this._trigger('onTagLimitExceeded', null, {duringInitialization: duringInitialization});
return false;
}
var label = $(this.options.onTagClicked ? '<a class="tagit-label"></a>' : '<span class="tagit-label"></span>').text(value);
// Create tag.
var tag = $('<li></li>')
.addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all')
.addClass(additionalClass)
.append(label)
.click(function(e) {
// Removes a tag when the little 'x' is clicked.
that.removeTag(tag);
});
if (this.options.readOnly){
tag.addClass('tagit-choice-read-only');
} else {
tag.addClass('tagit-choice-editable');
// Button for removing the tag.
var removeTagIcon = $('<span></span>')
.addClass('ui-icon ui-icon-close');
var removeTag = $('<a><span class="text-icon">\xd7</span></a>') // \xd7 is an X
.addClass('tagit-close')
.append(removeTagIcon);
tag.append(removeTag);
}
// Unless options.singleField is set, each tag has a hidden input field inline.
if (!this.options.singleField) {
var escapedValue = label.html();
tag.append('<input type="hidden" style="display:none;" value="' + escapedValue + '" name="' + this.options.fieldName + '" />');
}
if (this._trigger('beforeTagAdded', null, {
tag: tag,
tagLabel: this.tagLabel(tag),
duringInitialization: duringInitialization
}) === false) {
return;
}
if (this.options.singleField) {
var tags = this.assignedTags();
tags.push(value);
this._updateSingleTagsField(tags);
}
// DEPRECATED.
this._trigger('onTagAdded', null, tag);
this.tagInput.val('');
// Insert tag.
this.tagInput.parent().before(tag);
this._trigger('afterTagAdded', null, {
tag: tag,
tagLabel: this.tagLabel(tag),
duringInitialization: duringInitialization
});
if (this.options.showAutocompleteOnFocus && !duringInitialization) {
setTimeout(function () { that._showAutocomplete(); }, 0);
}
},
removeTag: function(tag, animate) {
animate = typeof animate === 'undefined' ? this.options.animate : animate;
tag = $(tag);
// DEPRECATED.
this._trigger('onTagRemoved', null, tag);
if (this._trigger('beforeTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)}) === false) {
return;
}
if (this.options.singleField) {
var tags = this.assignedTags();
var removedTagLabel = this.tagLabel(tag);
tags = $.grep(tags, function(el){
return el != removedTagLabel;
});
this._updateSingleTagsField(tags);
}
if (animate) {
tag.addClass('removed'); // Excludes this tag from _tags.
var hide_args = this._effectExists('blind') ? ['blind', {direction: 'horizontal'}, 'fast'] : ['fast'];
var thisTag = this;
hide_args.push(function() {
tag.remove();
thisTag._trigger('afterTagRemoved', null, {tag: tag, tagLabel: thisTag.tagLabel(tag)});
});
tag.fadeOut('fast').hide.apply(tag, hide_args).dequeue();
} else {
tag.remove();
this._trigger('afterTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)});
}
},
removeTagByLabel: function(tagLabel, animate) {
var toRemove = this._findTagByLabel(tagLabel);
if (!toRemove) {
throw "No such tag exists with the name '" + tagLabel + "'";
}
this.removeTag(toRemove, animate);
},
removeAll: function() {
// Removes all tags.
var that = this;
this._tags().each(function(index, tag) {
that.removeTag(tag, false);
});
}
});
})(jQuery);

View file

@ -388,7 +388,7 @@ CELERYBEAT_SCHEDULE = {
},
'activate-next-new-user': {
'task': 'activate-next-new-user',
'schedule': datetime.timedelta(minutes=2),
'schedule': datetime.timedelta(minutes=8),
'options': {'queue': 'beat_tasks'},
},
}

View file

@ -34,7 +34,6 @@
NEWSBLUR.Bookmarklet.prototype = {
// ==================
// = Initialization =
// ==================
@ -62,8 +61,7 @@
this.attach_css();
this.make_modal();
this.open_modal();
this.get_page_content();
this.$modal.bind('click', $.rescope(this.handle_clicks, this));
var $comment = $('textarea[name=newsblur_comment]', this.$modal);
@ -75,6 +73,8 @@
$content.bind('keyup', $.rescope(this.update_share_button_title, this));
$comment.bind('keydown', 'ctrl+return', $.rescope(this.share_story, this));
$comment.bind('keydown', 'meta+return', $.rescope(this.share_story, this));
this.get_page_content();
},
make_modal: function() {

View file

@ -83,13 +83,19 @@
<div class="NB-feeds-header-container NB-feeds-header-starred-container">
<div class="NB-feeds-header NB-feeds-header-starred NB-empty">
<div class="NB-feeds-header-count unread_count"></div>
<div class="NB-feeds-header-count feed_counts_floater unread_count"></div>
<div class="NB-feeds-header-icon"></div>
<div class="NB-feedlist-collapse-icon"></div>
<div class="NB-feeds-header-title">
Saved Stories
</div>
</div>
</div>
<div class="NB-starred-folder">
<ul class="NB-starred-feeds NB-feedlist"></ul>
</div>
</div>
<div class="left-center-footer">

View file

@ -11,6 +11,8 @@ urlpatterns = patterns('',
(r'^try/?', reader_views.index),
(r'^site/(?P<feed_id>\d+)?', reader_views.index),
(r'^folder/(?P<folder_name>\d+)?', reader_views.index),
url(r'^saved/(?P<tag_name>\d+)?', reader_views.index, name='saved-stories-tag'),
(r'^saved/?', reader_views.index),
(r'^social/\d+/.*?', reader_views.index),
(r'^user/.*?', reader_views.index),
(r'^null/.*?', reader_views.index),

View file

@ -46,7 +46,7 @@ def format_story_link_date__long(date, now=None):
if not now:
now = datetime.datetime.now()
date = date.replace(tzinfo=None)
midnight = midnight_today()
midnight = midnight_today(now)
parsed_date = DateFormat(date)
if date >= midnight: