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'^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_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_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'^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_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'), 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 boto
import redis import redis
import requests import requests
import zlib
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.shortcuts import render from django.shortcuts import render
from django.contrib.auth.decorators import login_required 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.validators import email_re
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.utils import feedgenerator
from mongoengine.queryset import OperationError from mongoengine.queryset import OperationError
from apps.recommendations.models import RecommendedFeed from apps.recommendations.models import RecommendedFeed
from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag 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.profile.models import Profile
from apps.reader.models import UserSubscription, UserSubscriptionFolders, RUserStory, Feature from apps.reader.models import UserSubscription, UserSubscriptionFolders, RUserStory, Feature
from apps.reader.forms import SignupForm, LoginForm, FeatureForm 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.statistics.models import MStatistics
# from apps.search.models import SearchStarredStory # from apps.search.models import SearchStarredStory
try: try:
@ -257,7 +259,9 @@ def load_feeds(request):
len(scheduled_feeds)) len(scheduled_feeds))
ScheduleImmediateFetches.apply_async(kwargs=dict(feed_ids=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 = { social_params = {
'user_id': user.pk, 'user_id': user.pk,
@ -283,6 +287,7 @@ def load_feeds(request):
'user_profile': user.profile, 'user_profile': user.profile,
'folders': json.decode(folders.folders), 'folders': json.decode(folders.folders),
'starred_count': starred_count, 'starred_count': starred_count,
'starred_counts': starred_counts,
'categories': categories 'categories': categories
} }
return data return data
@ -592,13 +597,16 @@ def load_single_feed(request, feed_id):
starred_stories = MStarredStory.objects(user_id=user.pk, starred_stories = MStarredStory.objects(user_id=user.pk,
story_feed_id=feed.pk, story_feed_id=feed.pk,
story_hash__in=story_hashes)\ 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, shared_stories = MSharedStory.objects(user_id=user.pk,
story_feed_id=feed_id, story_feed_id=feed_id,
story_hash__in=story_hashes)\ story_hash__in=story_hashes)\
.only('story_hash', 'shared_date', 'comments') .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,
shared_stories = dict([(story.story_hash, dict(shared_date=story.shared_date, comments=story.comments)) 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]) for story in shared_stories])
checkpoint4 = time.time() checkpoint4 = time.time()
@ -618,9 +626,10 @@ def load_single_feed(request, feed_id):
story['read_status'] = 0 story['read_status'] = 0
if story['story_hash'] in starred_stories: if story['story_hash'] in starred_stories:
story['starred'] = True 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) user.profile.timezone)
story['starred_date'] = format_story_link_date__long(starred_date, now) 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: if story['story_hash'] in shared_stories:
story['shared'] = True story['shared'] = True
shared_date = localtime_for_timezone(shared_stories[story['story_hash']]['shared_date'], 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)) limit = int(request.REQUEST.get('limit', 10))
page = int(request.REQUEST.get('page', 0)) page = int(request.REQUEST.get('page', 0))
query = request.REQUEST.get('query') query = request.REQUEST.get('query')
tag = request.REQUEST.get('tag')
story_hashes = request.REQUEST.getlist('h')[:100] story_hashes = request.REQUEST.getlist('h')[:100]
now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone) now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone)
message = None message = None
@ -751,10 +761,20 @@ def load_starred_stories(request):
# results = SearchStarredStory.query(user.pk, query) # results = SearchStarredStory.query(user.pk, query)
# story_ids = [result.db_id for result in results] # story_ids = [result.db_id for result in results]
if user.profile.is_premium: 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: else:
stories = [] stories = []
message = "You must be a premium subscriber to search." 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: elif story_hashes:
mstories = MStarredStory.objects( mstories = MStarredStory.objects(
user_id=user.pk, user_id=user.pk,
@ -831,6 +851,60 @@ def starred_story_hashes(request):
return dict(starred_story_hashes=story_hashes) 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 @json.json_view
def load_river_stories__redis(request): def load_river_stories__redis(request):
limit = 12 limit = 12
@ -913,7 +987,8 @@ def load_river_stories__redis(request):
user_id=user.pk, user_id=user.pk,
story_feed_id__in=found_feed_ids story_feed_id__in=found_feed_ids
).only('story_hash', 'starred_date') ).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]) for story in starred_stories])
else: else:
starred_stories = {} starred_stories = {}
@ -953,9 +1028,10 @@ def load_river_stories__redis(request):
story['long_parsed_date'] = format_story_link_date__long(story_date, nowtz) story['long_parsed_date'] = format_story_link_date__long(story_date, nowtz)
if story['story_hash'] in starred_stories: if story['story_hash'] in starred_stories:
story['starred'] = True 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) user.profile.timezone)
story['starred_date'] = format_story_link_date__long(starred_date, now) story['starred_date'] = format_story_link_date__long(starred_date, now)
story['user_tags'] = starred_stories[story['story_hash']]['user_tags']
story['intelligence'] = { story['intelligence'] = {
'feed': apply_classifier_feeds(classifier_feeds, story['story_feed_id']), 'feed': apply_classifier_feeds(classifier_feeds, story['story_feed_id']),
'author': apply_classifier_authors(classifier_authors, story), 'author': apply_classifier_authors(classifier_authors, story),
@ -1685,11 +1761,13 @@ def iframe_buster(request):
@ajax_login_required @ajax_login_required
@json.json_view @json.json_view
def mark_story_as_starred(request): def mark_story_as_starred(request):
code = 1 code = 1
feed_id = int(request.REQUEST['feed_id']) feed_id = int(request.REQUEST['feed_id'])
story_id = request.REQUEST['story_id'] story_id = request.REQUEST['story_id']
message = "" user_tags = request.REQUEST.getlist('user_tags')
story, _ = MStory.find_story(story_feed_id=feed_id, story_id=story_id) message = ""
story, _ = MStory.find_story(story_feed_id=feed_id, story_id=story_id)
if not story: if not story:
return {'code': -1, 'message': "Could not find story to save."} 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('user_id', None)
story_db.pop('starred_date', None) story_db.pop('starred_date', None)
story_db.pop('id', None) story_db.pop('id', None)
story_db.pop('user_tags', None)
now = datetime.datetime.now() 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( starred_story, created = MStarredStory.objects.get_or_create(
story_hash=story.story_hash, story_hash=story.story_hash,
user_id=story_values.pop('user_id'), user_id=story_values.pop('user_id'),
defaults=story_values) defaults=story_values)
if created: if created:
logging.user(request, "~FCStarring: ~SB%s" % (story.story_title[:50]))
MActivity.new_starred_story(user_id=request.user.pk, MActivity.new_starred_story(user_id=request.user.pk,
story_title=story.story_title, story_title=story.story_title,
story_feed_id=feed_id, story_feed_id=feed_id,
story_id=starred_story.story_guid) story_id=starred_story.story_guid)
else: else:
code = -1 starred_story.user_tags = user_tags
message = "Already saved this story." starred_story.save()
logging.user(request, "~FC~BRAlready stared:~SN~FC ~SB%s" % (story.story_title[:50]))
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') @required_params('story_id')
@ajax_login_required @ajax_login_required
@ -1723,6 +1808,7 @@ def mark_story_as_starred(request):
def mark_story_as_unstarred(request): def mark_story_as_unstarred(request):
code = 1 code = 1
story_id = request.POST['story_id'] story_id = request.POST['story_id']
starred_counts = None
starred_story = MStarredStory.objects(user_id=request.user.pk, story_guid=story_id) starred_story = MStarredStory.objects(user_id=request.user.pk, story_guid=story_id)
if not starred_story: if not starred_story:
@ -1730,10 +1816,12 @@ def mark_story_as_unstarred(request):
if starred_story: if starred_story:
logging.user(request, "~FCUnstarring: ~SB%s" % (starred_story[0].story_title[:50])) logging.user(request, "~FCUnstarring: ~SB%s" % (starred_story[0].story_title[:50]))
starred_story.delete() starred_story.delete()
MStarredStoryCounts.count_tags_for_user(request.user.pk)
starred_counts = MStarredStoryCounts.user_counts(request.user.pk)
else: else:
code = -1 code = -1
return {'code': code} return {'code': code, 'starred_counts': starred_counts}
@ajax_login_required @ajax_login_required
@json.json_view @json.json_view

View file

@ -9,6 +9,7 @@ import zlib
import hashlib import hashlib
import redis import redis
import pymongo import pymongo
import HTMLParser
from collections import defaultdict from collections import defaultdict
from operator import itemgetter from operator import itemgetter
from bson.objectid import ObjectId from bson.objectid import ObjectId
@ -1215,6 +1216,8 @@ class Feed(models.Model):
story['id'] = story_db.story_guid or story_db.story_date story['id'] = story_db.story_guid or story_db.story_date
if hasattr(story_db, 'starred_date'): if hasattr(story_db, 'starred_date'):
story['starred_date'] = 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'): if hasattr(story_db, 'shared_date'):
story['shared_date'] = story_db.shared_date story['shared_date'] = story_db.shared_date
if include_permalinks and hasattr(story_db, 'blurblog_permalink'): if include_permalinks and hasattr(story_db, 'blurblog_permalink'):
@ -1643,7 +1646,12 @@ class MStory(mongo.Document):
@property @property
def feed_guid_hash(self): def feed_guid_hash(self):
return "%s:%s" % (self.story_feed_id, self.guid_hash) 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): def save(self, *args, **kwargs):
story_title_max = MStory._fields['story_title'].max_length story_title_max = MStory._fields['story_title'].max_length
story_content_type_max = MStory._fields['story_content_type'].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_guid = mongo.StringField()
story_hash = mongo.StringField() story_hash = mongo.StringField()
story_tags = mongo.ListField(mongo.StringField(max_length=250)) 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)) image_urls = mongo.ListField(mongo.StringField(max_length=1024))
meta = { meta = {
@ -1967,12 +1976,26 @@ class MStarredStory(mongo.Document):
db_id=str(self.id)) db_id=str(self.id))
@classmethod @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( stories_db = cls.objects(
Q(user_id=user_id) & Q(user_id=user_id) &
(Q(story_title__icontains=query) | (Q(story_title__icontains=query) |
Q(story_author_name__icontains=query) | Q(story_author_name__icontains=query) |
Q(story_tags__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] ).order_by('-starred_date')[offset:offset+limit]
stories = Feed.format_stories(stories_db) stories = Feed.format_stories(stories_db)
@ -2036,7 +2059,63 @@ class MStarredStory(mongo.Document):
return original_text 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): class MFetchHistory(mongo.Document):
feed_id = mongo.IntField(unique=True) feed_id = mongo.IntField(unique=True)
feed_fetch_history = mongo.DynamicField() feed_fetch_history = mongo.DynamicField()

View file

@ -7,6 +7,7 @@ import re
import mongoengine as mongo import mongoengine as mongo
import random import random
import requests import requests
import HTMLParser
from collections import defaultdict from collections import defaultdict
from BeautifulSoup import BeautifulSoup from BeautifulSoup import BeautifulSoup
from mongoengine.queryset import Q from mongoengine.queryset import Q
@ -1407,7 +1408,7 @@ class MSharedStory(mongo.Document):
def __unicode__(self): def __unicode__(self):
user = User.objects.get(pk=self.user_id) user = User.objects.get(pk=self.user_id)
return "%s: %s (%s)%s%s" % (user.username, return "%s: %s (%s)%s%s" % (user.username,
self.story_title[:20], self.decoded_story_title[:20],
self.story_feed_id, self.story_feed_id,
': ' if self.has_comments else '', ': ' if self.has_comments else '',
self.comments[:20]) self.comments[:20])
@ -1420,6 +1421,11 @@ class MSharedStory(mongo.Document):
def feed_guid_hash(self): def feed_guid_hash(self):
return "%s:%s" % (self.story_feed_id or "0", self.guid_hash) 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): def canonical(self):
return { return {
"user_id": self.user_id, "user_id": self.user_id,
@ -1634,7 +1640,7 @@ class MSharedStory(mongo.Document):
if interactive: if interactive:
feed = Feed.get_by_id(story.story_feed_id) 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 if accept_story in ['n', 'N']: continue
story_db = dict([(k, v) for k, v in story._data.items() 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) shared_feed_ids.append(story.story_feed_id)
publish_new_stories = True publish_new_stories = True
logging.user(popular_user, "~FCSharing: ~SB~FM%s (%s shares, %s min)" % ( logging.user(popular_user, "~FCSharing: ~SB~FM%s (%s shares, %s min)" % (
story.story_title[:50], story.decoded_story_title[:50],
story_info['count'], story_info['count'],
cutoff)) cutoff))
@ -1914,7 +1920,7 @@ class MSharedStory(mongo.Document):
def generate_post_to_service_message(self, truncate=None, include_url=True): def generate_post_to_service_message(self, truncate=None, include_url=True):
message = strip_tags(self.comments) message = strip_tags(self.comments)
if not message or len(message) < 1: if not message or len(message) < 1:
message = self.story_title message = self.decoded_story_title
if include_url and truncate: if include_url and truncate:
message = truncate_chars(message, truncate - 18 - 30) message = truncate_chars(message, truncate - 18 - 30)
feed = Feed.get_by_id(self.story_feed_id) feed = Feed.get_by_id(self.story_feed_id)
@ -2023,7 +2029,7 @@ class MSharedStory(mongo.Document):
'story_feed': story_feed, 'story_feed': story_feed,
'mute_url': mute_url, '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) text = render_to_string('mail/email_reply.txt', data)
html = pynliner.fromString(render_to_string('mail/email_reply.xhtml', 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" % ( logging.user(reply_user, "~BB~FM~SBSending %s/%s email%s for new reply: %s" % (
sent_emails, len(notify_user_ids), sent_emails, len(notify_user_ids),
'' if len(notify_user_ids) == 1 else 's', '' 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.emailed_replies.append(reply.reply_id)
self.save() self.save()
@ -2087,7 +2093,7 @@ class MSharedStory(mongo.Document):
'story_feed': story_feed, 'story_feed': story_feed,
'mute_url': mute_url, '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) text = render_to_string('mail/email_reshare.txt', data)
html = pynliner.fromString(render_to_string('mail/email_reshare.xhtml', 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" % ( logging.user(reshare_user, "~BB~FM~SBSending %s email for story re-share: %s" % (
original_user.username, original_user.username,
self.story_title[:30])) self.decoded_story_title[:30]))
def calculate_image_sizes(self, force=False): def calculate_image_sizes(self, force=False):
if not self.story_content_z: if not self.story_content_z:
@ -2286,7 +2292,7 @@ class MSocialServices(mongo.Document):
return return
twitter_user = api.me() 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_username = twitter_user.screen_name
self.twitter_refreshed_date = datetime.datetime.utcnow() self.twitter_refreshed_date = datetime.datetime.utcnow()
self.syncing_twitter = False self.syncing_twitter = False
@ -2524,6 +2530,18 @@ class MSocialServices(mongo.Document):
profile.save() profile.save()
return profile 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): def post_to_twitter(self, shared_story):
message = shared_story.generate_post_to_service_message(truncate=140) 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, api.put_object('me', '%s:share' % settings.FACEBOOK_NAMESPACE,
link=shared_story.blurblog_permalink(), link=shared_story.blurblog_permalink(),
type="link", type="link",
name=shared_story.story_title, name=shared_story.decoded_story_title,
description=content, description=content,
website=shared_story.blurblog_permalink(), website=shared_story.blurblog_permalink(),
message=message, message=message,
@ -2564,7 +2582,7 @@ class MSocialServices(mongo.Document):
try: try:
api = self.appdotnet_api() api = self.appdotnet_api()
api.createPost(text=message, links=[{ api.createPost(text=message, links=[{
'text': shared_story.story_title, 'text': shared_story.decoded_story_title,
'url': shared_story.blurblog_permalink() 'url': shared_story.blurblog_permalink()
}]) }])
except Exception, e: 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, starred_stories = MStarredStory.objects(user_id=user.pk,
story_hash__in=story_hashes)\ 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, shared_stories = MSharedStory.objects(user_id=user.pk,
story_hash__in=story_hashes)\ story_hash__in=story_hashes)\
.only('story_hash', 'shared_date', 'comments') .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, shared_stories = dict([(story.story_hash, dict(shared_date=story.shared_date,
comments=story.comments)) comments=story.comments))
for story in shared_stories]) 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: if story['story_hash'] in starred_stories:
story['starred'] = True 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) user.profile.timezone)
story['starred_date'] = format_story_link_date__long(starred_date, now) 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: if story['story_hash'] in shared_stories:
story['shared'] = True story['shared'] = True
story['shared_comments'] = strip_tags(shared_stories[story['story_hash']]['comments']) story['shared_comments'] = strip_tags(shared_stories[story['story_hash']]['comments'])
@ -239,8 +242,9 @@ def load_river_blurblog(request):
starred_stories = MStarredStory.objects( starred_stories = MStarredStory.objects(
user_id=user.pk, user_id=user.pk,
story_hash__in=story_hashes story_hash__in=story_hashes
).only('story_hash', 'starred_date') ).only('story_hash', 'starred_date', 'user_tags')
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]) for story in starred_stories])
shared_stories = MSharedStory.objects(user_id=user.pk, shared_stories = MSharedStory.objects(user_id=user.pk,
story_hash__in=story_hashes)\ 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) story['long_parsed_date'] = format_story_link_date__long(story_date, nowtz)
if story['story_hash'] in starred_stories: if story['story_hash'] in starred_stories:
story['starred'] = True 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['starred_date'] = format_story_link_date__long(starred_date, now)
story['user_tags'] = starred_stories[story['story_hash']]['user_tags']
story['intelligence'] = { story['intelligence'] = {
'feed': apply_classifier_feeds(classifier_feeds, story['story_feed_id'], 'feed': apply_classifier_feeds(classifier_feeds, story['story_feed_id'],
social_user_ids=story['friend_user_ids']), 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['link'] = social_profile.blurblog_url
data['description'] = "Stories shared by %s on NewsBlur." % user.username data['description'] = "Stories shared by %s on NewsBlur." % user.username
data['lastBuildDate'] = datetime.datetime.utcnow() data['lastBuildDate'] = datetime.datetime.utcnow()
data['generator'] = 'NewsBlur - http://www.newsblur.com' data['generator'] = 'NewsBlur - %s' % settings.NEWSBLUR_URL
data['docs'] = None data['docs'] = None
data['author_name'] = user.username data['author_name'] = user.username
data['feed_url'] = "http://%s%s" % ( data['feed_url'] = "http://%s%s" % (

View file

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

View file

@ -24,19 +24,19 @@ public class AllSharedStoriesReading extends Reading {
setTitle(getResources().getString(R.string.all_shared_stories)); 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()); readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver());
addStoryToMarkAsRead(readingAdapter.getStory(passedPosition));
getSupportLoaderManager().initLoader(0, null, this); 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 @Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) { public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
StoryOrder storyOrder = PrefsUtils.getStoryOrderForFolder(this, PrefConstants.ALL_SHARED_STORIES_FOLDER_NAME); 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)); stories = contentResolver.query(FeedProvider.ALL_STORIES_URI, null, DatabaseConstants.getStorySelectionFromState(currentState), null, DatabaseConstants.getStorySortOrder(storyOrder));
setTitle(getResources().getString(R.string.all_stories_row_title)); 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()); readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver());
addStoryToMarkAsRead(readingAdapter.getStory(passedPosition));
getSupportLoaderManager().initLoader(0, null, this); 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 @Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) { public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
StoryOrder storyOrder = PrefsUtils.getStoryOrderForFolder(this, PrefConstants.ALL_STORIES_FOLDER_NAME); 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 { public class FeedReading extends Reading {
String feedId; String feedId;
private Feed feed;
@Override @Override
protected void onCreate(Bundle savedInstanceBundle) { protected void onCreate(Bundle savedInstanceBundle) {
@ -33,25 +32,26 @@ public class FeedReading extends Reading {
Cursor feedClassifierCursor = contentResolver.query(classifierUri, null, null, null, null); Cursor feedClassifierCursor = contentResolver.query(classifierUri, null, null, null, null);
Classifier classifier = Classifier.fromCursor(feedClassifierCursor); 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); Cursor feedCursor = contentResolver.query(feedUri, null, null, null, null);
Feed feed = Feed.fromCursor(feedCursor);
feedCursor.moveToFirst();
feed = Feed.fromCursor(feedCursor);
feedCursor.close(); feedCursor.close();
setTitle(feed.title); setTitle(feed.title);
int unreadCount = FeedUtils.getFeedUnreadCount(this.feed, this.currentState);
this.startingUnreadCount = unreadCount;
this.currentUnreadCount = unreadCount;
readingAdapter = new FeedReadingAdapter(getSupportFragmentManager(), feed, classifier); readingAdapter = new FeedReadingAdapter(getSupportFragmentManager(), feed, classifier);
addStoryToMarkAsRead(readingAdapter.getStory(passedPosition));
getSupportLoaderManager().initLoader(0, null, this); 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 @Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) { public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
Uri storiesURI = FeedProvider.FEED_STORIES_URI.buildUpon().appendPath(feedId).build(); 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); folderName = getIntent().getStringExtra(Reading.EXTRA_FOLDERNAME);
setTitle(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()); readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver());
addStoryToMarkAsRead(readingAdapter.getStory(passedPosition));
getSupportLoaderManager().initLoader(0, null, this); 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 @Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) { 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))); 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 passedPosition;
protected int currentState; protected int currentState;
protected final Object STORIES_MUTEX = new Object();
protected Cursor stories;
protected ViewPager pager; protected ViewPager pager;
protected Button overlayLeft, overlayRight; protected Button overlayLeft, overlayRight;
protected ProgressBar overlayProgress, overlayProgressRight, overlayProgressLeft; protected ProgressBar overlayProgress, overlayProgressRight, overlayProgressLeft;
@ -79,20 +82,13 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
protected ReadingAdapter readingAdapter; protected ReadingAdapter readingAdapter;
protected ContentResolver contentResolver; protected ContentResolver contentResolver;
private APIManager apiManager; private APIManager apiManager;
protected Cursor stories;
private boolean noMoreApiPages; private boolean noMoreApiPages;
private boolean stopLoading; private boolean stopLoading;
protected volatile boolean requestedPage; // set high iff a syncservice request for stories is already in flight protected volatile boolean requestedPage; // set high iff a syncservice request for stories is already in flight
private int currentApiPage = 0; 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 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 overlayRangeTopPx;
private float overlayRangeBotPx; private float overlayRangeBotPx;
@ -116,9 +112,6 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
this.overlaySend = (Button) findViewById(R.id.reading_overlay_send); this.overlaySend = (Button) findViewById(R.id.reading_overlay_send);
fragmentManager = getSupportFragmentManager(); fragmentManager = getSupportFragmentManager();
storiesToMarkAsRead = new HashSet<Story>();
storiesAlreadySeen = new HashSet<Story>();
passedPosition = getIntent().getIntExtra(EXTRA_POSITION, 0); passedPosition = getIntent().getIntExtra(EXTRA_POSITION, 0);
currentState = getIntent().getIntExtra(ItemsList.EXTRA_STATE, 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 // this likes to default to 'on' for some platforms
enableProgressCircle(overlayProgressLeft, false); enableProgressCircle(overlayProgressLeft, false);
enableProgressCircle(overlayProgressRight, false); enableProgressCircle(overlayProgressRight, false);
} }
@ -152,30 +146,34 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
@Override @Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (cursor != null) { synchronized (STORIES_MUTEX) {
readingAdapter.swapCursor(cursor); if (cursor != null) {
stories = cursor; 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();
} }
ReadingItemFragment fragment = getReadingFragment();
if (fragment != null ) { if (this.pager == null) {
fragment.updateStory(readingAdapter.getStory(pager.getCurrentItem())); // if this is the first time we've found a cursor, we know the onCreate chain is done
fragment.updateSaveButton(); 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.setOnPageChangeListener(this);
pager.setAdapter(readingAdapter); 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 // setCurrentItem sometimes fails to pass the first page to the callback, so call it manually
// for the first one. // for the first one.
this.onPageSelected(passedPosition); this.onPageSelected(passedPosition);
@ -194,6 +192,11 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
this.enableOverlays(); this.enableOverlays();
} }
/**
* Query the DB for the current unreadcount for this view.
*/
protected abstract int getUnreadCount();
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu); super.onCreateOptionsMenu(menu);
@ -264,6 +267,10 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
if (noMoreData) { if (noMoreData) {
this.noMoreApiPages = true; this.noMoreApiPages = true;
} }
updateCursor();
}
private void updateCursor() {
getSupportLoaderManager().restartLoader(0, null, this); getSupportLoaderManager().restartLoader(0, null, this);
} }
@ -287,10 +294,9 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
this.pageHistory.add(story); this.pageHistory.add(story);
} }
} }
addStoryToMarkAsRead(story); markStoryRead(story);
checkStoryCount(position);
} }
this.enableOverlays(); checkStoryCount(position);
} }
// interface ScrollChangeListener // interface ScrollChangeListener
@ -341,17 +347,16 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
private void enableOverlays() { private void enableOverlays() {
this.setOverlayAlpha(1.0f); this.setOverlayAlpha(1.0f);
this.overlayLeft.setEnabled(this.getLastReadPosition(false) != -1); this.overlayLeft.setEnabled(this.getLastReadPosition(false) != -1);
this.overlayRight.setText((this.currentUnreadCount > 0) ? R.string.overlay_next : R.string.overlay_done); this.overlayRight.setText((getUnreadCount() > 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.setBackgroundResource((getUnreadCount() > 0) ? R.drawable.selector_overlay_bg_right : R.drawable.selector_overlay_bg_right_done);
if (this.startingUnreadCount == 0 ) { if (this.startingUnreadCount == 0 ) {
// sessions with no unreads just show a full progress bar // sessions with no unreads just show a full progress bar
this.overlayProgress.setMax(1); this.overlayProgress.setMax(1);
this.overlayProgress.setProgress(1); this.overlayProgress.setProgress(1);
} else { } else {
int unreadProgress = this.startingUnreadCount - this.currentUnreadCount; int unreadProgress = this.startingUnreadCount - getUnreadCount();
this.overlayProgress.setMax(this.startingUnreadCount); this.overlayProgress.setMax(this.startingUnreadCount);
this.overlayProgress.setProgress(unreadProgress); this.overlayProgress.setProgress(unreadProgress);
} }
@ -368,7 +373,6 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
} }
invalidateOptionsMenu(); invalidateOptionsMenu();
} }
public void onWindowFocusChanged(boolean hasFocus) { public void onWindowFocusChanged(boolean hasFocus) {
@ -413,62 +417,26 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
protected abstract void triggerRefresh(int page); protected abstract void triggerRefresh(int page);
@Override
protected void onPause() {
flushStoriesMarkedRead();
super.onPause();
}
@Override @Override
protected void onStop() { protected void onStop() {
this.stopLoading = true; this.stopLoading = true;
super.onStop(); super.onStop();
} }
/** private void markStoryRead(Story story) {
* Log a story as having been read. The local DB and remote server will be updated synchronized (STORIES_MUTEX) {
* batch-wise when the activity pauses. FeedUtils.markStoryAsRead(story, this);
*/ updateCursor();
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();
}
} }
enableOverlays();
} }
private void markStoryUnread(Story story) { private void markStoryUnread(Story story) {
synchronized (STORIES_MUTEX) {
// first, ensure the story isn't queued up to be marked as read FeedUtils.markStoryUnread(story, this);
this.storiesToMarkAsRead.remove(story); updateCursor();
}
// next, call the API to un-mark it as read, just in case we missed the batch enableOverlays();
// operation, or it was read long before now.
FeedUtils.markStoryUnread(story, Reading.this, this.apiManager);
this.currentUnreadCount++;
this.storiesAlreadySeen.remove(story);
this.enableOverlays();
} }
// NB: this callback is for the text size slider // 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. * Click handler for the righthand overlay nav button.
*/ */
public void overlayRight(View v) { public void overlayRight(View v) {
if (this.currentUnreadCount == 0) { if (getUnreadCount() == 0) {
// if there are no unread stories, go back to the feed list // if there are no unread stories, go back to the feed list
Intent i = new Intent(this, Main.class); Intent i = new Intent(this, Main.class);
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
@ -506,7 +474,6 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
return null; return null;
} }
}.execute(); }.execute();
//}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
} }
@ -530,7 +497,7 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
break unreadSearch; break unreadSearch;
} }
} else { } else {
if ((candidate == pager.getCurrentItem()) || (story.read) || (this.storiesAlreadySeen.contains(story))) { if ((candidate == pager.getCurrentItem()) || (story.read) ) {
candidate++; candidate++;
continue unreadSearch; continue unreadSearch;
} else { } 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. * Click handler for the progress indicator on the righthand overlay nav button.
*/ */
public void overlayCount(View v) { public void overlayCount(View v) {
String unreadText = getString((this.currentUnreadCount == 1) ? R.string.overlay_count_toast_1 : R.string.overlay_count_toast_N); String unreadText = getString((getUnreadCount() == 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(); Toast.makeText(this, String.format(unreadText, getUnreadCount()), Toast.LENGTH_SHORT).show();
} }
public void overlaySend(View v) { public void overlaySend(View v) {

View file

@ -26,6 +26,12 @@ public class SavedStoriesReading extends Reading {
getSupportLoaderManager().initLoader(0, null, this); getSupportLoaderManager().initLoader(0, null, this);
} }
@Override
protected int getUnreadCount() {
// effectively disable the notion of unreads for this feed
return 0;
}
@Override @Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) { public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
return new CursorLoader(this, FeedProvider.STARRED_STORIES_URI, null, null, null, DatabaseConstants.getStorySortOrder(StoryOrder.NEWEST)); 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 userId;
private String username; private String username;
private SocialFeed socialFeed;
@Override @Override
protected void onCreate(Bundle savedInstanceBundle) { protected void onCreate(Bundle savedInstanceBundle) {
@ -30,22 +29,22 @@ public class SocialFeedReading extends Reading {
userId = getIntent().getStringExtra(Reading.EXTRA_USERID); userId = getIntent().getStringExtra(Reading.EXTRA_USERID);
username = getIntent().getStringExtra(Reading.EXTRA_USERNAME); 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)); setTitle(getIntent().getStringExtra(EXTRA_USERNAME));
int unreadCount = FeedUtils.getFeedUnreadCount(this.socialFeed, this.currentState);
this.startingUnreadCount = unreadCount;
this.currentUnreadCount = unreadCount;
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver()); readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver());
addStoryToMarkAsRead(readingAdapter.getStory(passedPosition));
getSupportLoaderManager().initLoader(0, null, this); 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 @Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) { public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
Uri storiesURI = FeedProvider.SOCIALFEED_STORIES_URI.buildUpon().appendPath(userId).build(); 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) { 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); 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 @Override
@ -431,6 +443,11 @@ public class FeedProvider extends ContentProvider {
selectionArgs = new String[] { uri.getLastPathSegment() }; selectionArgs = new String[] { uri.getLastPathSegment() };
return db.query(DatabaseConstants.STORY_TABLE, DatabaseConstants.STORY_COLUMNS, selection, selectionArgs, null, null, sortOrder); 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 // Querying for all stories
case ALL_STORIES: case ALL_STORIES:
String allStoriesQuery = "SELECT " + TextUtils.join(",", DatabaseConstants.STORY_COLUMNS) + ", " + DatabaseConstants.FEED_TITLE + ", " + String allStoriesQuery = "SELECT " + TextUtils.join(",", DatabaseConstants.STORY_COLUMNS) + ", " + DatabaseConstants.FEED_TITLE + ", " +
@ -598,7 +615,8 @@ public class FeedProvider extends ContentProvider {
@Override @Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 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)) { switch (uriMatcher.match(uri)) {
case ALL_FEEDS: 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. // 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: case FEED_COUNT:
db.execSQL("UPDATE " + DatabaseConstants.FEED_TABLE + " SET " + selectionArgs[0] + " = " + selectionArgs[0] + " - 1 WHERE " + DatabaseConstants.FEED_ID + " = " + selectionArgs[1]); 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: case SOCIALFEED_COUNT:
db.execSQL("UPDATE " + DatabaseConstants.SOCIALFEED_TABLE + " SET " + selectionArgs[0] + " = " + selectionArgs[0] + " - 1 WHERE " + DatabaseConstants.SOCIAL_FEED_ID + " = " + selectionArgs[1]); 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: case STARRED_STORIES_COUNT:
int rows = db.update(DatabaseConstants.STARRED_STORY_COUNT_TABLE, values, null, null); int rows = db.update(DatabaseConstants.STARRED_STORY_COUNT_TABLE, values, null, null);
if (rows == 0 ) { if (rows == 0 ) {

View file

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

View file

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

View file

@ -141,6 +141,13 @@ public class APIManager {
return response.getResponse(gson, NewsBlurResponse.class); 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) { public NewsBlurResponse markStoryAsStarred(final String feedId, final String storyId) {
final ValueMultimap values = new ValueMultimap(); final ValueMultimap values = new ValueMultimap();
values.put(APIConstants.PARAMETER_FEEDID, feedId); 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"; 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 // 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"; 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>() { new AsyncTask<Void, Void, NewsBlurResponse>() {
@Override @Override
protected NewsBlurResponse doInBackground(Void... arg) { protected NewsBlurResponse doInBackground(Void... arg) {
APIManager apiManager = new APIManager(context);
return apiManager.markStoryAsUnread(story.feedId, story.storyHash); return apiManager.markStoryAsUnread(story.feedId, story.storyHash);
} }
@Override @Override
@ -214,7 +215,52 @@ public class FeedUtils {
} }
} }
}.execute(); }.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. * the local DB and on the server.
*/ */
public static void markStoriesAsRead( Collection<Story> stories, final Context context ) { public static void markStoriesAsRead( Collection<Story> stories, final Context context ) {
// the list of story hashes to mark read // the list of story hashes to mark read
final ArrayList<String> storyHashes = new ArrayList<String>(); final ArrayList<String> storyHashes = new ArrayList<String>();
// a list of local DB ops to perform // a list of local DB ops to perform
@ -261,7 +306,6 @@ public class FeedUtils {
for (Story story : stories) { for (Story story : stories) {
story.read = true; story.read = true;
} }
} }
private static void appendStoryReadOperations(Story story, List<ContentProviderOperation> operations) { private static void appendStoryReadOperations(Story story, List<ContentProviderOperation> operations) {
@ -276,20 +320,28 @@ public class FeedUtils {
} else { } else {
selectionArgs = new String[] { DatabaseConstants.FEED_NEGATIVE_COUNT, story.feedId } ; selectionArgs = new String[] { DatabaseConstants.FEED_NEGATIVE_COUNT, story.feedId } ;
} }
operations.add(ContentProviderOperation.newUpdate(FeedProvider.FEED_COUNT_URI).withValues(emptyValues).withSelection("", selectionArgs).build()); operations.add(ContentProviderOperation.newUpdate(FeedProvider.FEED_COUNT_URI).withValues(emptyValues).withSelection("", selectionArgs).build());
if (!TextUtils.isEmpty(story.socialUserId)) { HashSet<String> socialIds = new HashSet<String>();
String[] socialSelectionArgs; if (!TextUtils.isEmpty(story.socialUserId)) {
if (story.getIntelligenceTotal() > 0) { socialIds.add(story.socialUserId);
socialSelectionArgs = new String[] { DatabaseConstants.SOCIAL_FEED_POSITIVE_COUNT, story.socialUserId } ; }
} else if (story.getIntelligenceTotal() == 0) { if (story.friendUserIds != null) {
socialSelectionArgs = new String[] { DatabaseConstants.SOCIAL_FEED_NEUTRAL_COUNT, story.socialUserId } ; for (String id : story.friendUserIds) {
} else { socialIds.add(id);
socialSelectionArgs = new String[] { DatabaseConstants.SOCIAL_FEED_NEGATIVE_COUNT, story.socialUserId } ; }
} }
operations.add(ContentProviderOperation.newUpdate(FeedProvider.SOCIALCOUNT_URI).withValues(emptyValues).withSelection("", socialSelectionArgs).build()); 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(); Uri storyUri = FeedProvider.STORY_URI.buildUpon().appendPath(story.id).build();
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();

View file

@ -444,8 +444,8 @@ body {
color: rgba(0, 0, 0, .4); color: rgba(0, 0, 0, .4);
opacity: .4; opacity: .4;
font-size: 16px; font-size: 16px;
padding: 78px 24px 0; padding: 78px 16px 0;
position: absolute; margin: 48px 0;
text-shadow: 0 1px 0 rgba(255, 255, 255, .4); text-shadow: 0 1px 0 rgba(255, 255, 255, .4);
top: 40%; top: 40%;
width: 100%; width: 100%;
@ -733,6 +733,10 @@ body {
opacity: 1; opacity: 1;
display: block; 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-feedlists .folder .folder_title .feed_counts_floater,
.NB-feeds-header .feed_counts_floater { .NB-feeds-header .feed_counts_floater {
@ -793,9 +797,15 @@ body {
.NB-feedlist .feed.NB-toplevel:hover .feed_favicon { .NB-feedlist .feed.NB-toplevel:hover .feed_favicon {
display: none; display: none;
} }
.NB-feedlist .feed.NB-toplevel.NB-no-hover .feed_favicon {
display: block;
}
.NB-feedlist .folder_title.NB-toplevel:hover { .NB-feedlist .folder_title.NB-toplevel:hover {
background: none; background: none;
} }
.NB-feedlist .folder_title.NB-toplevel.NB-no-hover {
background: inherit;
}
.NB-feedlist .feed_counts { .NB-feedlist .feed_counts {
position: absolute; position: absolute;
@ -809,11 +819,11 @@ body {
.NB-feedlist .feed.NB-selected, .NB-feedlist .feed.NB-selected,
.NB-feeds-header.NB-selected, .NB-feeds-header.NB-selected,
.NB-feedlist .folder.NB-selected > .folder_title { .NB-feedlist .folder.NB-selected > .folder_title {
background-color: #FDED8D; background-color: #FFFFD2;
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#FFFFD2), to(#FDED8D)); background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#FFFFD2), to(#F5F9B4));
background: -moz-linear-gradient(center top , #FFFFD2 0%, #FDED8D 100%); background: -moz-linear-gradient(center top , #FFFFD2 0%, #F5F9B4 100%);
border-top: 1px solid #EBE0BE; border-top: 1px solid #D6D682;
border-bottom: 1px solid #E3D0AE; border-bottom: 1px solid #D6D682;
} }
.NB-feedlist .folder.NB-selected > .folder_title { .NB-feedlist .folder.NB-selected > .folder_title {
text-shadow: 0 1px 0 rgba(255, 255, 255, .5); text-shadow: 0 1px 0 rgba(255, 255, 255, .5);
@ -1088,11 +1098,11 @@ body {
.NB-feed-story-header-info ::-moz-selection { .NB-feed-story-header-info ::-moz-selection {
background: transparent; background: transparent;
} }
.NB-feed-story-header-info ::selection { .NB-feed-story-header-info ::selection {
background: transparent; background: transparent;
} }
#story_titles { #story_titles {
@ -1757,10 +1767,7 @@ background: transparent;
color: #304080; color: #304080;
border-top: 1px solid #EFEEC3; border-top: 1px solid #EFEEC3;
border-bottom: 1px solid #EFEEC3; border-bottom: 1px solid #EFEEC3;
background-color: #FFFDDF; background-color: #FFFEE2;
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%);
} }
#story_titles .NB-story-title:hover:not(.NB-selected) { #story_titles .NB-story-title:hover:not(.NB-selected) {
@ -2075,11 +2082,11 @@ background: transparent;
font-weight: bold; font-weight: bold;
} }
.NB-feed-story-feed .NB-feed-story-header-title:hover { .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 { .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 { .NB-feed-story .NB-feed-story-content {
padding: 12px 0 0; padding: 12px 0 0;
max-width: 700px; max-width: 700px;
min-height: 12px;
} }
.NB-feed-story .NB-narrow-content .NB-feed-story-content { .NB-feed-story .NB-narrow-content .NB-feed-story-content {
margin-right: 28px; margin-right: 28px;
@ -3070,6 +3078,92 @@ background: transparent;
color: #90928B; color: #90928B;
opacity: 1; 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 { .NB-sideoption-share-wrapper {
height: 0; height: 0;
overflow: hidden; overflow: hidden;
@ -3153,7 +3247,9 @@ background: transparent;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
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 { .NB-sideoption-share .NB-sideoption-share-save {
font-size: 10px; font-size: 10px;
@ -3495,9 +3591,12 @@ background: transparent;
.NB-feeds-header-river-container .NB-feeds-header { .NB-feeds-header-river-container .NB-feeds-header {
border-bottom: 1px solid #B7BBAA; border-bottom: 1px solid #B7BBAA;
} }
.NB-feeds-header-starred.NB-feeds-header { .NB-feeds-header-starred-container {
border-top: 1px solid #B7BBAA; 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 { .NB-feeds-header-river-container .NB-feeds-header.NB-empty .NB-feeds-header-count {
display: none; display: none;
} }
@ -3512,13 +3611,23 @@ background: transparent;
} }
.NB-feeds-header-starred .NB-feeds-header-count { .NB-feeds-header-starred .NB-feeds-header-count {
background-color: #11448B; background-color: #506B9A;
display: block; display: block;
padding: 3px 4px 1px; padding: 2px 3px 2px;
margin: 3px 3px 0 0; 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); 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 { .NB-feeds-header-starred.NB-empty .NB-feeds-header-count {
display: none; display: none;
} }
@ -7466,6 +7575,13 @@ form.opml_import_form input {
.NB-modal-exception .NB-fieldset-fields .NB-error { .NB-modal-exception .NB-fieldset-fields .NB-error {
padding: 6px 0 6px 4px; 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 { .NB-modal-feed-settings .NB-preference-label {
float: left; float: left;
margin: 6px 0; margin: 6px 0;
@ -7475,30 +7591,7 @@ form.opml_import_form input {
float: left; float: left;
overflow: hidden; 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 = */ /* = Feedchooser Modal = */
@ -8215,16 +8308,6 @@ form.opml_import_form input {
border-radius: 3px; border-radius: 3px;
border-bottom: 1px solid rgba(0, 0, 0, .1); 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 { .NB-modal-preferences .NB-preference-story-pane-position input {
margin-top: 4px; margin-top: 4px;
} }
@ -8402,6 +8485,52 @@ form.opml_import_form input {
font-size: 12px; font-size: 12px;
width: 100px; 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 = */ /* = 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.folders = new NEWSBLUR.Collections.Folders([]);
this.favicons = {}; this.favicons = {};
this.stories = new NEWSBLUR.Collections.Stories(); this.stories = new NEWSBLUR.Collections.Stories();
this.starred_feeds = new NEWSBLUR.Collections.StarredFeeds();
this.queued_read_stories = {}; this.queued_read_stories = {};
this.classifiers = {}; this.classifiers = {};
this.friends = {}; this.friends = {};
@ -226,23 +227,49 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
mark_story_as_starred: function(story_id, callback) { mark_story_as_starred: function(story_id, callback) {
var self = this; var self = this;
this.starred_count += 1;
var story = this.get_story(story_id); 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', { this.make_request('/reader/mark_story_as_starred', {
story_id: story_id, story_id: story_id,
feed_id: story.get('story_feed_id') feed_id: story.get('story_feed_id'),
}, callback); user_tags: story.get('user_tags')
}, pre_callback);
}, },
mark_story_as_unstarred: function(story_id, callback) { mark_story_as_unstarred: function(story_id, callback) {
var self = this; var self = this;
this.starred_count -= 1;
var story = this.get_story(story_id); 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', { this.make_request('/reader/mark_story_as_unstarred', {
story_id: story_id story_id: story_id
}, callback); }, pre_callback);
}, },
mark_feed_as_read: function(feed_id, cutoff_timestamp, direction, mark_active, 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.folders.reset(_.compact(subscriptions.folders), {parse: true});
self.starred_count = subscriptions.starred_count; 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.user_profile.set(subscriptions.social_profile);
self.social_services = subscriptions.social_services; 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'), feed_title: data.feed_title || this.active_feed.get('feed_title'),
updated: data.updated || this.active_feed.get('updated'), updated: data.updated || this.active_feed.get('updated'),
feed_address: data.feed_address || this.active_feed.get('feed_address') 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.feed_id = feed_id;
this.starred_stories = data.starred_stories; 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 self = this;
var pre_callback = function(data) { var pre_callback = function(data) {
@ -566,7 +591,8 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
this.make_request('/reader/starred_stories', { this.make_request('/reader/starred_stories', {
page: page, page: page,
query: NEWSBLUR.reader.flags.search query: NEWSBLUR.reader.flags.search,
tag: tag
}, pre_callback, error_callback, { }, pre_callback, error_callback, {
'ajax_group': (page ? 'feed_page' : 'feed'), 'ajax_group': (page ? 'feed_page' : 'feed'),
'request_type': 'GET' 'request_type': 'GET'
@ -848,8 +874,10 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
get_feed: function(feed_id) { get_feed: function(feed_id) {
var self = this; var self = this;
if (_.string.include(feed_id, 'social:')) { if (_.string.startsWith(feed_id, 'social:')) {
return this.social_feeds.get(feed_id); return this.social_feeds.get(feed_id);
} else if (_.string.startsWith(feed_id, 'starred:')) {
return this.starred_feeds.get(feed_id);
} else { } else {
return this.feeds.get(feed_id); return this.feeds.get(feed_id);
} }
@ -870,6 +898,18 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
return this.feeds; 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() { get_folders: function() {
var self = this; var self = this;

View file

@ -69,21 +69,52 @@ NEWSBLUR.Modal.prototype = {
$.modal.close(callback); $.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 $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; 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')); 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) { if (feed.id == current_feed_id) {
$option.attr('selected', true); $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; return $chooser;
}, },

View file

@ -6,6 +6,10 @@ NEWSBLUR.Router = Backbone.Router.extend({
"site/:site_id/:slug": "site", "site/:site_id/:slug": "site",
"site/:site_id/": "site", "site/:site_id/": "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",
"folder/:folder_name/": "folder", "folder/:folder_name/": "folder",
"social/:user_id/:slug": "social", "social/:user_id/:slug": "social",
@ -24,7 +28,7 @@ NEWSBLUR.Router = Backbone.Router.extend({
}, },
site: function(site_id, slug) { site: function(site_id, slug) {
NEWSBLUR.log(["site", site_id, slug]); // NEWSBLUR.log(["site", site_id, slug]);
site_id = parseInt(site_id, 10); site_id = parseInt(site_id, 10);
var feed = NEWSBLUR.assets.get_feed(site_id); var feed = NEWSBLUR.assets.get_feed(site_id);
if (feed) { 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: function(folder_name) {
folder_name = folder_name.replace(/-/g, ' '); folder_name = folder_name.replace(/-/g, ' ');
NEWSBLUR.log(["folder", folder_name]); // NEWSBLUR.log(["folder", folder_name]);
var options = {router: true}; var options = {router: true};
if (folder_name == "everything") { if (folder_name == "everything") {
NEWSBLUR.reader.open_river_stories(null, null, options); NEWSBLUR.reader.open_river_stories(null, null, options);

View file

@ -78,6 +78,14 @@ NEWSBLUR.Models.Feed = Backbone.Model.extend({
return true; 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) { rename: function(new_title) {
this.set('feed_title', new_title); this.set('feed_title', new_title);
NEWSBLUR.assets.rename_feed(this.id, new_title); NEWSBLUR.assets.rename_feed(this.id, new_title);
@ -107,6 +115,10 @@ NEWSBLUR.Models.Feed = Backbone.Model.extend({
return true; return true;
}, },
is_starred: function() {
return false;
},
is_light: function() { is_light: function() {
var is_light = this._is_light; var is_light = this._is_light;
if (!_.isUndefined(is_light)) { if (!_.isUndefined(is_light)) {

View file

@ -204,6 +204,16 @@ NEWSBLUR.Collections.Folders = Backbone.Collection.extend({
return names; 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() { feed_ids_in_folder: function() {
return _.compact(_.flatten(this.map(function(item) { return _.compact(_.flatten(this.map(function(item) {
return item.feed_ids_in_folder(); 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) { 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(); var feed_counts = item.feed.unread_counts();
counts['ps'] += feed_counts['ps']; counts['ps'] += feed_counts['ps'];
counts['nt'] += feed_counts['nt']; counts['nt'] += feed_counts['nt'];
counts['ng'] += feed_counts['ng']; counts['ng'] += feed_counts['ng'];
seen_feeds.push(item.feed.id);
} else if (item.is_folder()) { } 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['ps'] += folder_counts['ps'];
counts['nt'] += folder_counts['nt']; counts['nt'] += folder_counts['nt'];
counts['ng'] += folder_counts['ng']; counts['ng'] += folder_counts['ng'];

View file

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

View file

@ -45,7 +45,9 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
return false; 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')) { if (this.feed.get('exception_type')) {
this.$modal.removeClass('NB-modal-feed-settings'); this.$modal.removeClass('NB-modal-feed-settings');
} else { } else {
@ -56,10 +58,13 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
}, },
get_feed_settings: function() { get_feed_settings: function() {
if (this.feed.is_starred()) return;
var $loading = $('.NB-modal-loading', this.$modal); var $loading = $('.NB-modal-loading', this.$modal);
$loading.addClass('NB-active'); $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)); 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('img', { className: 'NB-modal-feed-image feed_favicon', src: $.favicon(this.feed) }),
$.make('div', { className: 'NB-modal-feed-heading' }, [ $.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-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' }, [ $.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'}, [ $.make('div', { className: 'NB-preference-label'}, [
'Reading view' 'Reading view'
]), ]),
$.make('div', { className: 'NB-preference-options' }, [ $.make('div', { className: 'NB-preference-options NB-view-settings' }, [
$.make('div', [ $.make('div', { className: "NB-view-setting-original" }, [
$.make('input', { id: 'NB-preference-view-1', type: 'radio', name: 'view_settings', value: 'page' }),
$.make('label', { 'for': 'NB-preference-view-1' }, [ $.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('div', [
$.make('input', { id: 'NB-preference-view-2', type: 'radio', name: 'view_settings', value: 'feed' }),
$.make('label', { 'for': 'NB-preference-view-2' }, [ $.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('div', [
$.make('input', { id: 'NB-preference-view-3', type: 'radio', name: 'view_settings', value: 'story' }),
$.make('label', { 'for': 'NB-preference-view-3' }, [ $.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') }) $.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-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-error' }),
$.make('div', { className: 'NB-exception-feed-history' }) $.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('h5', [
$.make('div', { className: 'NB-exception-option-meta' }), $.make('div', { className: 'NB-exception-option-meta' }),
$.make('span', { className: 'NB-exception-option-option NB-exception-only' }, 'Option 3:'), $.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') }) $.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-modal-submit-button NB-modal-submit-green NB-modal-submit-link' }, 'Fetch Feed From Website'),
$.make('div', { className: 'NB-error' }), $.make('div', { className: 'NB-error' }),
$.make('div', { className: 'NB-exception-page-history' }) $.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('div', { className: 'NB-fieldset NB-exception-option NB-exception-option-delete NB-exception-block-only NB-modal-submit' }, [
$.make('h5', [ $.make('h5', [
$.make('span', { className: 'NB-exception-option-option NB-exception-only' }, 'Option 4:'), $.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-tab NB-tab-feeds' }, [
$.make('div', { className: 'NB-preference NB-preference-view' }, [ $.make('div', { className: 'NB-preference NB-preference-view' }, [
$.make('div', { className: 'NB-preference-options' }, [ $.make('div', { className: 'NB-preference-options NB-view-settings' }, [
$.make('div', [ $.make('div', { className: "NB-view-setting-original" }, [
$.make('input', { id: 'NB-preference-view-1', type: 'radio', name: 'default_view', value: 'page' }),
$.make('label', { 'for': 'NB-preference-view-1' }, [ $.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('div', [
$.make('input', { id: 'NB-preference-view-2', type: 'radio', name: 'default_view', value: 'feed' }),
$.make('label', { 'for': 'NB-preference-view-2' }, [ $.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('div', [
$.make('input', { id: 'NB-preference-view-3', type: 'radio', name: 'default_view', value: 'story' }),
$.make('label', { 'for': 'NB-preference-view-3' }, [ $.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' }, [ this.$modal = $.make('div', { className: 'NB-modal-recommend NB-modal' }, [
$.make('div', { className: 'NB-modal-feed-chooser-container'}, [ $.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('div', { className: 'NB-modal-loading' }),
$.make('h2', { className: 'NB-modal-title' }, [ $.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' }, [ this.$modal = $.make('div', { className: 'NB-modal-statistics NB-modal' }, [
$.make('div', { className: 'NB-modal-feed-chooser-container'}, [ $.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('div', { className: 'NB-modal-loading' }),
$.make('h2', { className: 'NB-modal-title' }, 'Statistics &amp; History'), $.make('h2', { className: 'NB-modal-title' }, 'Statistics &amp; History'),

View file

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

View file

@ -32,8 +32,12 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({
NEWSBLUR.assets.social_feeds.bind('reset', _.bind(function() { NEWSBLUR.assets.social_feeds.bind('reset', _.bind(function() {
this.make_social_feeds(); this.make_social_feeds();
}, this)); }, this));
NEWSBLUR.assets.social_feeds.bind('change:selected', this.selected, this); NEWSBLUR.assets.starred_feeds.bind('reset', _.bind(function(models, options) {
NEWSBLUR.assets.feeds.bind('change:selected', this.selected, this); 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()) { if (!NEWSBLUR.assets.folders.size()) {
NEWSBLUR.assets.load_feeds(); NEWSBLUR.assets.load_feeds();
} }
@ -106,7 +110,7 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({
_.defer(_.bind(function() { _.defer(_.bind(function() {
NEWSBLUR.reader.open_dialog_after_feeds_loaded(); NEWSBLUR.reader.open_dialog_after_feeds_loaded();
NEWSBLUR.reader.toggle_focus_in_slider(); NEWSBLUR.reader.toggle_focus_in_slider();
this.selected(); this.scroll_to_selected();
if (NEWSBLUR.reader.socket) { if (NEWSBLUR.reader.socket) {
NEWSBLUR.reader.send_socket_active_feeds(); NEWSBLUR.reader.send_socket_active_feeds();
} else { } 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')); $('.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() { load_router: function() {
if (!NEWSBLUR.router) { if (!NEWSBLUR.router) {
NEWSBLUR.router = new NEWSBLUR.Router; NEWSBLUR.router = new NEWSBLUR.Router;
@ -218,51 +253,25 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({
// = Actions = // = Actions =
// =========== // ===========
selected: function(model, value, options) { scroll_to_show_selected_feed: function() {
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) {
var $feed_lists = this.$s.$feed_lists; var $feed_lists = this.$s.$feed_lists;
if (!feed_view) { var model = NEWSBLUR.assets.feeds.selected() ||
var model = NEWSBLUR.assets.feeds.selected() || NEWSBLUR.assets.social_feeds.selected(); NEWSBLUR.assets.social_feeds.selected() ||
if (!model || !model.get('selected')) return; NEWSBLUR.assets.starred_feeds.selected();
var feed_view = _.detect(model.views, _.bind(function(view) { if (!model) return;
return !!view.$el.closest(this.$s.$feed_lists).length; var feed_view = _.detect(model.views, _.bind(function(view) {
}, this)); return !!view.$el.closest(this.$s.$feed_lists).length;
if (!feed_view) return; }, this));
} if (!feed_view) return;
var is_feed_visible = $feed_lists.isScrollVisible(feed_view.$el);
if (!$feed_lists.isScrollVisible(feed_view.$el)) {
if (!is_feed_visible) {
var scroll = feed_view.$el.position().top; var scroll = feed_view.$el.position().top;
var container = $feed_lists.scrollTop(); var container = $feed_lists.scrollTop();
var height = $feed_lists.outerHeight(); var height = $feed_lists.outerHeight();
$feed_lists.scrollTop(scroll+container-height/5); $feed_lists.scrollTop(scroll+container-height/5);
} }
return true;
}, },
scroll_to_show_highlighted_feed: function() { 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 $feed_lists = this.$s.$feed_lists;
var $selected_view;
if (!folder_view) { var folder = NEWSBLUR.assets.folders.selected();
var folder = NEWSBLUR.assets.folders.selected(); if (folder) {
if (!folder || !folder.get('selected')) return; $selected_view = folder.folder_view.$el;
folder_view = folder.folder_view; $selected_view = $selected_view.find('.folder_title').eq(0);
if (!folder_view) return;
} }
var $folder_title = folder_view.$el.find('.folder_title').eq(0); if (!$selected_view && NEWSBLUR.reader.active_feed == 'river:') {
var is_folder_visible = $feed_lists.isScrollVisible($folder_title); $selected_view = NEWSBLUR.reader.$s.$river_sites_header.closest(".NB-feeds-header-container");
// NEWSBLUR.log(["scroll_to_show_selected_folder", folder_view, folder_view.$el, $feed_lists, is_folder_visible]); } 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) { if (!is_folder_visible) {
var scroll = folder_view.$el.position().top; var scroll = $selected_view.position().top;
var container = $feed_lists.scrollTop(); var container = $feed_lists.scrollTop();
var height = $feed_lists.outerHeight(); var height = $feed_lists.outerHeight();
$feed_lists.scrollTop(scroll+container-height/5); $feed_lists.scrollTop(scroll+container-height/5);
} }
return true;
}, },
scroll_to_selected: function() { scroll_to_selected: function() {
this.scroll_to_show_selected_feed(); var found = this.scroll_to_show_selected_feed();
this.scroll_to_show_selected_folder(); if (!found) {
this.scroll_to_show_selected_folder();
}
}, },
start_sorting: function() { start_sorting: function() {

View file

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

View file

@ -64,7 +64,7 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({
render: function() { render: function() {
var feed = this.model; var feed = this.model;
var extra_classes = this.extra_classes(); 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 class="feed_counts">\
</div>\ </div>\
<% if (type == "story") { %>\ <% if (type == "story") { %>\
@ -102,10 +102,11 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({
', { ', {
feed : feed, feed : feed,
type : this.options.type, type : this.options.type,
disable_hover : this.options.disable_hover,
extra_classes : extra_classes, extra_classes : extra_classes,
toplevel : this.options.depth == 0, toplevel : this.options.depth == 0,
list_type : this.options.type == 'feed' ? 'li' : 'div', 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') { if (this.options.type == 'story') {
@ -263,6 +264,11 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({
NEWSBLUR.reader.open_feed_exception_modal(this.model.id); NEWSBLUR.reader.open_feed_exception_modal(this.model.id);
} else if (this.model.is_social()) { } else if (this.model.is_social()) {
NEWSBLUR.reader.open_social_stories(this.model.id, {force: true, $feed: this.$el}); 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 { } else {
NEWSBLUR.reader.open_feed(this.model.id, {$feed: this.$el}); 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'); var dblclick_pref = NEWSBLUR.assets.preference('doubleclick_feed');
if (dblclick_pref == "ignore") return; if (dblclick_pref == "ignore") return;
if (this.options.type == "story") return; if (this.options.type == "story") return;
if (this.options.starred_tag) return;
if ($('.NB-modal-feedchooser').is(':visible')) return; if ($('.NB-modal-feedchooser').is(':visible')) return;
this.flags.double_click = true; this.flags.double_click = true;
_.delay(_.bind(function() { _.delay(_.bind(function() {
this.flags.double_click = false; this.flags.double_click = false;
}, this), 500); }, this), 500);
if (dblclick_pref == "open_and_read") { if (dblclick_pref == "open_and_read") {
NEWSBLUR.reader.mark_feed_as_read(this.model.id); 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; return false;
}, },
@ -297,11 +307,12 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({
}, },
mark_feed_as_read: function(e, days) { mark_feed_as_read: function(e, days) {
if (this.options.starred_tag) return;
if (e) { if (e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
} }
this.flags.double_click = true; this.flags.double_click = true;
_.delay(_.bind(function() { _.delay(_.bind(function() {
this.flags.double_click = false; this.flags.double_click = false;
@ -359,11 +370,14 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({
show_manage_menu: function(e) { show_manage_menu: function(e) {
if (this.options.feed_chooser) return; if (this.options.feed_chooser) return;
var feed_type = this.model.is_social() ? 'socialfeed' :
this.model.is_starred() ? 'starred' :
'feed';
e.preventDefault(); e.preventDefault();
e.stopPropagation(); 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, feed_id: this.model.id,
toplevel: this.options.depth == 0, toplevel: this.options.depth == 0,
rightclick: e.which >= 2 rightclick: e.which >= 2

View file

@ -89,7 +89,7 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({
// console.log(["Not a feed or folder", item]); // 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); this.$('.folder').append($feeds);
} }

View file

@ -3,6 +3,7 @@ NEWSBLUR.Views.Sidebar = Backbone.View.extend({
el: '.NB-sidebar', el: '.NB-sidebar',
events: { 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-starred": "open_starred_stories",
"click .NB-feeds-header-river-sites": "open_river_stories", "click .NB-feeds-header-river-sites": "open_river_stories",
"click .NB-feeds-header-river-blurblogs .NB-feedlist-collapse-icon": "collapse_river_blurblog", "click .NB-feeds-header-river-blurblogs .NB-feedlist-collapse-icon": "collapse_river_blurblog",
@ -16,6 +17,37 @@ NEWSBLUR.Views.Sidebar = Backbone.View.extend({
// = Actions = // = 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) { check_river_blurblog_collapsed: function(options) {
options = options || {}; options = options || {};
var show_folder_counts = NEWSBLUR.assets.preference('folder_counts'); var show_folder_counts = NEWSBLUR.assets.preference('folder_counts');
@ -151,6 +183,48 @@ NEWSBLUR.Views.Sidebar = Backbone.View.extend({
return false; 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() { open_river_blurblogs_stories: function() {
return NEWSBLUR.reader.open_river_blurblogs_stories(); 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")); 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() { toggle_feed_story_share_dialog: function() {
this.story.story_share_view.toggle_feed_story_share_dialog(); 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-tag" : "save_classifier",
"click .NB-feed-story-author" : "save_classifier", "click .NB-feed-story-author" : "save_classifier",
"click .NB-feed-story-train" : "open_story_trainer", "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-comments-label" : "scroll_to_comments",
"click .NB-story-content-expander" : "expand_story" "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', this.toggle_classes, this);
this.model.bind('change:read_status', this.toggle_read_status, 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: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.render_header, this);
this.model.bind('change:intelligence', this.toggle_intelligence, this); this.model.bind('change:intelligence', this.toggle_intelligence, this);
this.model.bind('change:shared', this.render_comments, this); this.model.bind('change:shared', this.render_comments, this);
@ -66,12 +66,15 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({
render: function() { render: function() {
var params = this.get_render_params(); var params = this.get_render_params();
params['story_header'] = this.story_header_template(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, model: this.model,
el: this.el 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, story: this.model,
social_services: NEWSBLUR.assets.social_services, social_services: NEWSBLUR.assets.social_services,
profile: NEWSBLUR.assets.user_profile profile: NEWSBLUR.assets.user_profile
@ -91,11 +94,24 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({
return this; 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() { attach_handlers: function() {
this.watch_images_for_story_height(); this.watch_images_for_story_height();
this.attach_audio_handler(); this.attach_audio_handler();
this.attach_syntax_highlighter_handler(); this.attach_syntax_highlighter_handler();
this.attach_fitvid_handler(); this.attach_fitvid_handler();
this.render_starred_tags();
}, },
render_header: function(model, value, options) { 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-icon">&nbsp;</div>\
<div class="NB-sideoption-title"><%= story.get("starred") ? "Saved" : "Save this story" %></div>\ <div class="NB-sideoption-title"><%= story.get("starred") ? "Saved" : "Save this story" %></div>\
</div>\ </div>\
<%= story_save_view %>\
<div class="NB-sideoption NB-feed-story-share">\ <div class="NB-sideoption NB-feed-story-share">\
<div class="NB-sideoption-icon">&nbsp;</div>\ <div class="NB-sideoption-icon">&nbsp;</div>\
<div class="NB-sideoption-title"><%= story.get("shared") ? "Shared" : "Share this story" %></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() { watch_images_for_story_height: function() {
this.model.on('change:images_loaded', _.bind(function() {
this.resize_starred_tags();
}, this));
if (!this.is_truncatable()) return; if (!this.is_truncatable()) return;
this.truncate_delay = 100; 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 story = this.model;
var $sideoption_title = this.$('.NB-feed-story-save .NB-sideoption-title'); 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); NEWSBLUR.reader.open_story_trainer(this.model.id, feed_id, options);
}, },
star_story: function() { toggle_starred: function() {
this.model.star_story(); this.model.toggle_starred();
}, },
scroll_to_comments: function() { scroll_to_comments: function() {

View file

@ -34,7 +34,6 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({
feed_view_story_positions_keys: [] feed_view_story_positions_keys: []
}; };
this.flags = { this.flags = {
feed_view_images_loaded: {},
mousemove_timeout: false mousemove_timeout: false
}; };
this.counts = { this.counts = {
@ -224,21 +223,23 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({
if (!NEWSBLUR.assets.stories.visible().length) { if (!NEWSBLUR.assets.stories.visible().length) {
this.show_explainer_no_stories(); this.show_explainer_no_stories();
return; // return;
} }
var pane_height = NEWSBLUR.reader.$s.$story_pane.height(); var pane_height = NEWSBLUR.reader.$s.$story_pane.height();
var indicator_position = NEWSBLUR.assets.preference('lock_mouse_indicator'); var indicator_position = NEWSBLUR.assets.preference('lock_mouse_indicator');
var endbar_height = 20; 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_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_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; endbar_height = pane_height - indicator_position - last_story_height;
if (endbar_height <= 20) endbar_height = 20; if (endbar_height <= 20) endbar_height = 20;
var empty_space = pane_height - last_story_offset - last_story_height - endbar_height; var empty_space = pane_height - last_story_offset - last_story_height - endbar_height;
if (empty_space > 0) endbar_height += empty_space + 1; 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(); this.$('.NB-end-line').remove();
@ -246,11 +247,13 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({
var last_story = NEWSBLUR.assets.stories.last(); var last_story = NEWSBLUR.assets.stories.last();
if (!last_story.get('selected')) return; if (!last_story.get('selected')) return;
} }
endbar_height /= 2; // Splitting padding between top and bottom
var $end_stories_line = $.make('div', { var $end_stories_line = $.make('div', {
className: 'NB-end-line' className: 'NB-end-line'
}, [ }, [
$.make('div', { className: 'NB-fleuron' }) $.make('div', { className: 'NB-fleuron' })
]).css('paddingBottom', endbar_height); ]).css('paddingBottom', endbar_height).css('paddingTop', endbar_height);
this.$el.append($end_stories_line); this.$el.append($end_stories_line);
}, },
@ -377,15 +380,24 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({
}, },
append_river_premium_only_notification: function() { append_river_premium_only_notification: function() {
var $notice = $.make('div', { className: 'NB-feed-story-premium-only' }, [ var message = [
$.make('div', { className: 'NB-feed-story-premium-only-text'}, [ 'The full River of News is a ',
'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'), $.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-feed-story-premium-only').remove();
this.$(".NB-end-line").append($notice); this.$(".NB-end-line").append($notice);
console.log(["append_search_premium_only_notification", this.$(".NB-end-line")]);
}, },
append_search_premium_only_notification: function() { append_search_premium_only_notification: function() {
@ -405,14 +417,17 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({
// ============= // =============
is_feed_loaded_for_location_fetch: function() { 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) { if (images_begun) {
var images_loaded = _.keys(this.flags.feed_view_images_loaded).length && var images_loaded = NEWSBLUR.assets.stories.all(function(s) {
_.all(_.values(this.flags.feed_view_images_loaded), _.identity); return s.get('images_loaded');
return !!images_loaded; });
return images_loaded;
} }
return !!images_begun; return images_begun;
}, },
prefetch_story_locations_in_feed_view: function() { prefetch_story_locations_in_feed_view: function() {
@ -420,7 +435,7 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({
var stories = NEWSBLUR.assets.stories; var stories = NEWSBLUR.assets.stories;
if (!_.contains(['split', 'full'], NEWSBLUR.assets.preference('story_layout'))) return; 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; if (!NEWSBLUR.assets.stories.size()) return;
@ -451,7 +466,7 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({
if (this.is_feed_loaded_for_location_fetch()) { if (this.is_feed_loaded_for_location_fetch()) {
this.fetch_story_locations_in_feed_view({'reset_timer': true}); this.fetch_story_locations_in_feed_view({'reset_timer': true});
} else { } 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; var image_count = story.story_view.$('.NB-feed-story-content img').length;
if (!image_count) { if (!image_count) {
// NEWSBLUR.log(["No images", story.get('story_title')]); // NEWSBLUR.log(["No images", story.get('story_title')]);
this.flags.feed_view_images_loaded[story.id] = true; story.set('images_loaded', true);
} else if (!this.flags.feed_view_images_loaded[story.id]) { } else if (!story.get('images_loaded')) {
// Progressively load the images in each story, so that when one story // Progressively load the images in each story, so that when one story
// loads, the position is calculated and the next story can calculate // loads, the position is calculated and the next story can calculate
// its position (after its own images are loaded). // 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) { (function(story, image_count) {
story.story_view.$('.NB-feed-story-content img').load(function() { story.story_view.$('.NB-feed-story-content img').load(function() {
// NEWSBLUR.log(['Loaded image', story.get('story_title'), image_count]); // NEWSBLUR.log(['Loaded image', story.get('story_title'), image_count]);
if (image_count <= 1) { if (image_count <= 1) {
NEWSBLUR.app.story_list.flags.feed_view_images_loaded[story.id] = true; story.set('images_loaded', true);
} else { } else {
image_count--; 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() { initialize: function() {
this.sideoptions_view = this.options.sideoptions_view;
this.model.story_share_view = this; this.model.story_share_view = this;
}, },
@ -67,38 +68,11 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({
if (options.close || if (options.close ||
($sideoption.hasClass('NB-active') && !options.resize_open)) { ($sideoption.hasClass('NB-active') && !options.resize_open)) {
// Close // Close
$share.animate({ this.is_open = false;
'height': 0 this.resize({close: true});
}, {
'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');
}
} else { } else {
// Open/resize // Open/resize
this.is_open = true;
if (!options.resize_open) { if (!options.resize_open) {
this.$('.NB-error').remove(); this.$('.NB-error').remove();
} }
@ -108,14 +82,6 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({
$facebook_button.removeClass('NB-active'); $facebook_button.removeClass('NB-active');
$appdotnet_button.removeClass('NB-active'); $appdotnet_button.removeClass('NB-active');
this.update_share_button_label(); 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) { if (options.animate_scroll) {
var $scroll_container = NEWSBLUR.reader.$s.$story_titles; var $scroll_container = NEWSBLUR.reader.$s.$story_titles;
@ -130,61 +96,9 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({
$scroll_container.height() $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) this.resize(options);
});
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();
}
var share = _.bind(function(e) { var share = _.bind(function(e) {
e.preventDefault(); e.preventDefault();
this.mark_story_as_shared({'source': 'sideoption'}); 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) { mark_story_as_shared: function(options) {
options = options || {}; options = options || {};
var $share_button = this.$('.NB-sideoption-share-save'); 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.text_view.fetch_and_render(this.model, temporary_text);
this.$(".NB-story-detail").html(this.text_view.$el); this.$(".NB-story-detail").html(this.text_view.$el);
this.story_detail.render_starred_tags();
} else { } else {
this.story_detail = new NEWSBLUR.Views.StoryDetailView({ this.story_detail = new NEWSBLUR.Views.StoryDetailView({
model: this.model, model: this.model,

View file

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

View file

@ -67,12 +67,20 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({
}, },
append_river_premium_only_notification: function() { append_river_premium_only_notification: function() {
var $notice = $.make('div', { className: 'NB-feed-story-premium-only' }, [ var message = [
$.make('div', { className: 'NB-feed-story-premium-only-text'}, [ 'The full River of News is a ',
'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'), $.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-feed-story-premium-only').remove();
this.$(".NB-end-line").append($notice); this.$(".NB-end-line").append($notice);
@ -193,14 +201,17 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({
if (NEWSBLUR.assets.preference('story_layout') == 'list') { if (NEWSBLUR.assets.preference('story_layout') == 'list') {
var pane_height = NEWSBLUR.reader.$s.$story_titles.height(); var pane_height = NEWSBLUR.reader.$s.$story_titles.height();
var endbar_height = 20; var endbar_height = 20;
var last_story_height = 100; var last_story_height = 280;
endbar_height = pane_height - last_story_height; endbar_height = pane_height - last_story_height;
if (endbar_height <= 20) endbar_height = 20; if (endbar_height <= 20) endbar_height = 20;
var empty_space = pane_height - last_story_height - endbar_height; var empty_space = pane_height - last_story_height - endbar_height;
if (empty_space > 0) endbar_height += empty_space + 1; 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('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); 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 (empty_on_missing) return 'data:image/png;base64,R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==';
else if (_.isNumber(feed.id)) return NEWSBLUR.URLs.favicon.replace('{id}', feed.id); else if (_.isNumber(feed.id)) return NEWSBLUR.URLs.favicon.replace('{id}', feed.id);
else if (feed.get('favicon_url')) return feed.get('favicon_url'); 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) { 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': { 'activate-next-new-user': {
'task': 'activate-next-new-user', 'task': 'activate-next-new-user',
'schedule': datetime.timedelta(minutes=2), 'schedule': datetime.timedelta(minutes=8),
'options': {'queue': 'beat_tasks'}, 'options': {'queue': 'beat_tasks'},
}, },
} }

View file

@ -34,7 +34,6 @@
NEWSBLUR.Bookmarklet.prototype = { NEWSBLUR.Bookmarklet.prototype = {
// ================== // ==================
// = Initialization = // = Initialization =
// ================== // ==================
@ -62,8 +61,7 @@
this.attach_css(); this.attach_css();
this.make_modal(); this.make_modal();
this.open_modal(); this.open_modal();
this.get_page_content();
this.$modal.bind('click', $.rescope(this.handle_clicks, this)); this.$modal.bind('click', $.rescope(this.handle_clicks, this));
var $comment = $('textarea[name=newsblur_comment]', this.$modal); var $comment = $('textarea[name=newsblur_comment]', this.$modal);
@ -75,6 +73,8 @@
$content.bind('keyup', $.rescope(this.update_share_button_title, this)); $content.bind('keyup', $.rescope(this.update_share_button_title, this));
$comment.bind('keydown', 'ctrl+return', $.rescope(this.share_story, this)); $comment.bind('keydown', 'ctrl+return', $.rescope(this.share_story, this));
$comment.bind('keydown', 'meta+return', $.rescope(this.share_story, this)); $comment.bind('keydown', 'meta+return', $.rescope(this.share_story, this));
this.get_page_content();
}, },
make_modal: function() { 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-container NB-feeds-header-starred-container">
<div class="NB-feeds-header NB-feeds-header-starred NB-empty"> <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-feeds-header-icon"></div>
<div class="NB-feedlist-collapse-icon"></div>
<div class="NB-feeds-header-title"> <div class="NB-feeds-header-title">
Saved Stories Saved Stories
</div> </div>
</div> </div>
</div> </div>
<div class="NB-starred-folder">
<ul class="NB-starred-feeds NB-feedlist"></ul>
</div>
</div> </div>
<div class="left-center-footer"> <div class="left-center-footer">

View file

@ -11,6 +11,8 @@ urlpatterns = patterns('',
(r'^try/?', reader_views.index), (r'^try/?', reader_views.index),
(r'^site/(?P<feed_id>\d+)?', reader_views.index), (r'^site/(?P<feed_id>\d+)?', reader_views.index),
(r'^folder/(?P<folder_name>\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'^social/\d+/.*?', reader_views.index),
(r'^user/.*?', reader_views.index), (r'^user/.*?', reader_views.index),
(r'^null/.*?', 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: if not now:
now = datetime.datetime.now() now = datetime.datetime.now()
date = date.replace(tzinfo=None) date = date.replace(tzinfo=None)
midnight = midnight_today() midnight = midnight_today(now)
parsed_date = DateFormat(date) parsed_date = DateFormat(date)
if date >= midnight: if date >= midnight: