Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

fixing merge conflict

  • Loading branch information...
commit 065395b6889d79f8ad596d691f2a1b351bd1e49f 1 parent b5ccc81
@roycyang roycyang authored
Showing with 2,124 additions and 639 deletions.
  1. +1 −1  LICENSE.md
  2. +2 −2 README.md
  3. +1 −1  api/newsblur.py
  4. +1 −1  apps/analyzer/fixtures/classifiers.json
  5. +1 −1  apps/feed_import/fixtures/opml_import.json
  6. +1 −1  apps/reader/fixtures/subscriptions.json
  7. +54 −13 apps/reader/views.py
  8. +1 −1  apps/rss_feeds/fixtures/bootstrap.json
  9. +1 −1  apps/rss_feeds/fixtures/rss_feeds.json
  10. +2 −0  apps/rss_feeds/models.py
  11. +176 −20 apps/social/models.py
  12. +18 −0 apps/social/tasks.py
  13. +1 −0  apps/social/urls.py
  14. +78 −14 apps/social/views.py
  15. +9 −2 assets.yml
  16. +1 −1  config/fixtures/bootstrap.json
  17. +2 −0  config/hosts
  18. +1 −3 config/mongodb.prod.conf
  19. +22 −34 config/nginx.newsblur.conf
  20. +4 −4 config/postgresql.conf
  21. +1 −1  config/supervisor_celeryd.conf
  22. +20 −9 fabfile.py
  23. +14 −21 local_settings.py.template
  24. +118 −20 media/css/reader.css
  25. +403 −9 media/css/social/social_page.css
  26. BIN  media/img/mobile/SidebarAllMyFiles.icns
  27. BIN  media/img/mobile/SidebarLaptop.icns
  28. BIN  media/img/mobile/SidebariMac.icns
  29. BIN  media/img/mobile/SidebariPad.icns
  30. BIN  media/img/mobile/SidebariPhone.icns
  31. BIN  media/img/mobile/iphone-icon-newsblur.png
  32. BIN  media/img/reader/background-control-light.png
  33. BIN  media/img/reader/diigo.png
  34. BIN  media/img/reader/evernote.png
  35. BIN  media/img/reader/kippt.png
  36. +44 −3 media/js/newsblur/common/assetmodel.js
  37. +1 −2  media/js/newsblur/models/feeds.js
  38. +5 −1 media/js/newsblur/models/folders.js
  39. +7 −4 media/js/newsblur/models/stories.js
  40. +173 −180 media/js/newsblur/reader/reader.js
  41. +1 −1  media/js/newsblur/reader/reader_intro.js
  42. +1 −1  media/js/newsblur/reader/reader_keyboard.js
  43. +12 −0 media/js/newsblur/reader/reader_preferences.js
  44. +1 −1  media/js/newsblur/reader/reader_send_email.js
  45. +3 −4 media/js/newsblur/reader/reader_social_profile.js
  46. +36 −7 media/js/newsblur/reader/reader_utils.js
  47. +1 −1  media/js/newsblur/static/about.js
  48. +1 −1  media/js/newsblur/static/faq.js
  49. +3 −2 media/js/newsblur/views/feed_list_view.js
  50. +4 −0 media/js/newsblur/views/feed_title_view.js
  51. +94 −47 media/js/newsblur/views/original_tab_view.js
  52. +1 −1  media/js/newsblur/views/profile_badge_view.js
  53. +6 −5 media/js/newsblur/views/story_comment_view.js
  54. +1 −1  media/js/newsblur/views/story_comments_view.js
  55. +18 −13 media/js/newsblur/views/story_detail_view.js
  56. +143 −108 media/js/newsblur/views/story_list_view.js
  57. +130 −40 media/js/newsblur/views/story_share_view.js
  58. +9 −5 media/js/newsblur/views/story_titles_view.js
  59. +25 −5 settings.py
  60. +5 −4 templates/mail/email_base.txt
  61. +6 −6 templates/mail/email_base.xhtml
  62. +2 −0  templates/mail/email_new_account.xhtml
  63. +14 −0 templates/mail/email_new_follower.txt
  64. +36 −0 templates/mail/email_new_follower.xhtml
  65. +1 −1  templates/reader/feeds.xhtml
  66. +109 −20 templates/social/social_page.xhtml
  67. +1 −1  templates/static/about.xhtml
  68. +9 −4 templates/static/api.xhtml
  69. +238 −4 templates/static/api.yml
  70. +2 −2 templates/static/faq.xhtml
  71. +1 −1  templates/static/press.xhtml
  72. +31 −0 utils/bootstrap_intel.py
  73. +2 −0  utils/munin/newsblur_updates.py
  74. +13 −2 utils/story_functions.py
  75. +1 −1  utils/templatetags/utils_tags.py
View
2  LICENSE.md
@@ -1,7 +1,7 @@
The MIT License
===============
-Copyright (c) 2009-2010 Samuel Clay <samuel@ofbrooklyn.com>.
+Copyright (c) 2009-2012 Samuel Clay, NewsBlur <samuel@newsblur.com>.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
View
4 README.md
@@ -145,7 +145,7 @@ these after the installation below.
4. Run the development server. At this point, all dependencies should be installed and no
additional configuration is needed. If you find that something is not working at this
point, please email the resulting output to Samuel Clay at
- [samuel@ofbrooklyn.com](samuel@ofbrooklyn.com).
+ [samuel@newsblur.com](samuel@newsblur.com).
./manage.py runserver
@@ -230,7 +230,7 @@ reader, and feed importer. To run the test suite:
## Author
* Created by [Samuel Clay](http://www.samuelclay.com).
- * Email address: <samuel@ofbrooklyn.com>
+ * Email address: <samuel@newsblur.com>
* [@samuelclay](http://twitter.com/samuelclay) on Twitter.
View
2  api/newsblur.py
@@ -6,7 +6,7 @@
import cookielib
import json
-__author__ = "Dananjaya Ramanayake <dananjaya86@gmail.com>, Samuel Clay <samuel@ofbrooklyn.com>"
+__author__ = "Dananjaya Ramanayake <dananjaya86@gmail.com>, Samuel Clay <samuel@newsblur.com>"
__version__ = "1.0"
API_URL = "http://www.newsblur.com/"
View
2  apps/analyzer/fixtures/classifiers.json
@@ -55,7 +55,7 @@
"groups": [],
"user_permissions": [],
"password": "sha1$7b94b$ac9e6cf08d0fa16a67e56e319c0935aeb26db2a2",
- "email": "samuel@ofbrooklyn.com",
+ "email": "samuel@newsblur.com",
"date_joined": "2009-01-04 17:32:58"
}
}
View
2  apps/feed_import/fixtures/opml_import.json
@@ -13,7 +13,7 @@
"groups": [],
"user_permissions": [],
"password": "sha1$7b94b$ac9e6cf08d0fa16a67e56e319c0935aeb26db2a2",
- "email": "samuel@ofbrooklyn.com",
+ "email": "samuel@newsblur.com",
"date_joined": "2009-01-04 17:32:58"
}
}
View
2  apps/reader/fixtures/subscriptions.json
@@ -150,7 +150,7 @@
"groups": [],
"user_permissions": [],
"password": "sha1$7b94b$ac9e6cf08d0fa16a67e56e319c0935aeb26db2a2",
- "email": "samuel@ofbrooklyn.com",
+ "email": "samuel@newsblur.com",
"date_joined": "2009-01-04 17:32:58"
}
},
View
67 apps/reader/views.py
@@ -16,6 +16,7 @@
from django.core.mail import mail_admins
from django.core.validators import email_re
from django.core.mail import EmailMultiAlternatives
+from django.contrib.sites.models import Site
from mongoengine.queryset import OperationError
from pymongo.helpers import OperationFailure
from operator import itemgetter
@@ -33,7 +34,9 @@
from apps.rss_feeds.models import Feed, MFeedPage, DuplicateFeed, MStory, MStarredStory, FeedLoadtime
except:
pass
-from apps.social.models import MSharedStory, MSocialProfile, MSocialSubscription, MActivity
+from apps.social.models import MSharedStory, MSocialProfile, MSocialServices
+from apps.social.models import MSocialSubscription, MActivity
+from apps.social.views import load_social_page
from utils import json_functions as json
from utils.user_functions import get_user, ajax_login_required
from utils.feed_functions import relative_timesince
@@ -48,9 +51,20 @@
SINGLE_DAY = 60*60*24
-@never_cache
@render_to('reader/feeds.xhtml')
def index(request):
+ if request.method == "GET" and request.subdomain and request.subdomain != 'dev':
+ username = request.subdomain
+ try:
+ if '.' in username:
+ username = username.split('.')[0]
+ user = User.objects.get(username__iexact=username)
+ except User.DoesNotExist:
+ return HttpResponseRedirect('http://%s%s' % (
+ Site.objects.get_current().domain.replace('www', 'dev'),
+ reverse('index')))
+ return load_social_page(request, user_id=user.pk, username=request.subdomain)
+
# XXX TODO: Remove me on launch.
if request.method == "GET" and request.user.is_anonymous() and not request.REQUEST.get('letmein'):
return {}, 'reader/social_signup.xhtml'
@@ -211,6 +225,7 @@ def load_feeds(request):
}
social_feeds = MSocialSubscription.feeds(**social_params)
social_profile = MSocialProfile.profile(user.pk)
+ social_services = MSocialServices.profile(user.pk)
user.profile.dashboard_date = datetime.datetime.now()
user.profile.save()
@@ -219,6 +234,7 @@ def load_feeds(request):
'feeds': feeds.values() if version == 2 else feeds,
'social_feeds': social_feeds,
'social_profile': social_profile,
+ 'social_services': social_services,
'folders': json.decode(folders.folders),
'starred_count': starred_count,
}
@@ -228,11 +244,11 @@ def load_feeds(request):
def load_feed_favicons(request):
user = get_user(request)
feed_ids = request.REQUEST.getlist('feed_ids')
- user_subs = UserSubscription.objects.select_related('feed').filter(user=user, active=True)
- if feed_ids and len(feed_ids) > 0:
- user_subs = user_subs.filter(feed__in=feed_ids)
+
+ if not feed_ids:
+ user_subs = UserSubscription.objects.select_related('feed').filter(user=user, active=True)
+ feed_ids = [sub['feed__pk'] for sub in user_subs.values('feed__pk')]
- feed_ids = [sub['feed__pk'] for sub in user_subs.values('feed__pk')]
feed_icons = dict([(i.feed_id, i.data) for i in MFeedIcon.objects(feed_id__in=feed_ids)])
return feed_icons
@@ -240,10 +256,13 @@ def load_feed_favicons(request):
def load_feeds_flat(request):
user = request.user
include_favicons = request.REQUEST.get('include_favicons', False)
+ update_counts = request.REQUEST.get('update_counts', False)
+
feeds = {}
iphone_version = "1.2"
if include_favicons == 'false': include_favicons = False
+ if update_counts == 'false': update_counts = False
if not user.is_authenticated():
return HttpResponseForbidden()
@@ -285,7 +304,23 @@ def make_feeds_folder(items, parent_folder="", depth=0):
make_feeds_folder(folder, flat_folder_name, depth+1)
make_feeds_folder(folders)
- data = dict(flat_folders=flat_folders, feeds=feeds, user=user.username, iphone_version=iphone_version)
+
+ social_params = {
+ 'user_id': user.pk,
+ 'include_favicon': include_favicons,
+ 'update_counts': update_counts,
+ }
+ social_feeds = MSocialSubscription.feeds(**social_params)
+ social_profile = MSocialProfile.profile(user.pk)
+
+ data = {
+ "flat_folders": flat_folders,
+ "feeds": feeds,
+ "social_feeds": social_feeds,
+ "social_profile": social_profile,
+ "user": user.username,
+ "iphone_version": iphone_version,
+ }
return data
@ratelimit(minutes=1, requests=20)
@@ -643,14 +678,20 @@ def load_river_stories(request):
# starred_stories = {}
# Intelligence classifiers for all feeds involved
- classifier_feeds = list(MClassifierFeed.objects(user_id=user.pk,
- feed_id__in=found_feed_ids))
- classifier_authors = list(MClassifierAuthor.objects(user_id=user.pk,
+ if found_feed_ids:
+ classifier_feeds = list(MClassifierFeed.objects(user_id=user.pk,
feed_id__in=found_feed_ids))
- classifier_titles = list(MClassifierTitle.objects(user_id=user.pk,
+ classifier_authors = list(MClassifierAuthor.objects(user_id=user.pk,
+ feed_id__in=found_feed_ids))
+ classifier_titles = list(MClassifierTitle.objects(user_id=user.pk,
+ feed_id__in=found_feed_ids))
+ classifier_tags = list(MClassifierTag.objects(user_id=user.pk,
feed_id__in=found_feed_ids))
- classifier_tags = list(MClassifierTag.objects(user_id=user.pk,
- feed_id__in=found_feed_ids))
+ else:
+ classifier_feeds = []
+ classifier_authors = []
+ classifier_titles = []
+ classifier_tags = []
classifiers = sort_classifiers_by_feed(user=user, feed_ids=found_feed_ids,
classifier_feeds=classifier_feeds,
classifier_authors=classifier_authors,
View
2  apps/rss_feeds/fixtures/bootstrap.json
@@ -20,7 +20,7 @@
"groups": [],
"user_permissions": [],
"password": "sha1$d5473$d07ce4495b088ff0f41a62d5113d0189ce8f0096",
- "email": "samuel@ofbrooklyn.com",
+ "email": "samuel@newsblur.com",
"date_joined": "2011-07-18 00:23:49"
}
}
View
2  apps/rss_feeds/fixtures/rss_feeds.json
@@ -125,7 +125,7 @@
"groups": [],
"user_permissions": [],
"password": "sha1$7b94b$ac9e6cf08d0fa16a67e56e319c0935aeb26db2a2",
- "email": "samuel@ofbrooklyn.com",
+ "email": "samuel@newsblur.com",
"date_joined": "2009-01-04 17:32:58"
}
}
View
2  apps/rss_feeds/models.py
@@ -933,6 +933,8 @@ def format_story(cls, story_db, feed_id=None, text=False):
text = re.sub(r'\n+', '\n\n', text)
text = re.sub(r'\t+', '\t', text)
story['text'] = text
+ if '<ins' in story['story_content'] or '<del' in story['story_content']:
+ story['has_modifications'] = True
return story
View
196 apps/social/models.py
@@ -24,6 +24,7 @@
from vendor import tweepy
from utils import log as logging
from utils.feed_functions import relative_timesince
+from utils.story_functions import truncate_chars
from utils import json_functions as json
RECOMMENDATIONS_LIMIT = 5
@@ -125,13 +126,19 @@ def save(self, *args, **kwargs):
if not self.username:
self.import_user_fields()
if not self.subscription_count:
- self.count(skip_save=True)
+ self.count_follows(skip_save=True)
if self.bio and len(self.bio) > MSocialProfile.bio.max_length:
self.bio = self.bio[:80]
super(MSocialProfile, self).save(*args, **kwargs)
if self.user_id not in self.following_user_ids:
self.follow_user(self.user_id)
- self.count()
+ self.count_follows()
+
+ @property
+ def blurblog_url(self):
+ return "http://%s.%s" % (
+ self.username_slug,
+ Site.objects.get_current().domain.replace('www', 'dev'))
def recommended_users(self):
r = redis.Redis(connection_pool=settings.REDIS_POOL)
@@ -221,7 +228,7 @@ def profile(cls, user_id):
profile = cls.objects.get(user_id=user_id)
except cls.DoesNotExist:
return {}
- return profile.to_json(full=True)
+ return profile.to_json(include_follows=True)
@classmethod
def profiles(cls, user_ids):
@@ -259,7 +266,7 @@ def feed(self):
return params
def page(self):
- params = self.to_json(full=True)
+ params = self.to_json(include_follows=True)
params.update({
'feed_title': self.title,
'custom_css': self.custom_css,
@@ -271,8 +278,17 @@ def profile_photo_url(self):
if self.photo_url:
return self.photo_url
return settings.MEDIA_URL + 'img/reader/default_profile_photo.png'
+
+ @property
+ def email_photo_url(self):
+ if self.photo_url:
+ if self.photo_url.startswith('//'):
+ self.photo_url = 'http:' + self.photo_url
+ return self.photo_url
+ domain = Site.objects.get_current().domain.replace('www', 'dev')
+ return 'http://' + domain + settings.MEDIA_URL + 'img/reader/default_profile_photo.png'
- def to_json(self, compact=False, full=False, common_follows_with_user=None):
+ def to_json(self, compact=False, include_follows=False, common_follows_with_user=None):
# domain = Site.objects.get_current().domain
domain = Site.objects.get_current().domain.replace('www', 'dev')
params = {
@@ -284,8 +300,7 @@ def to_json(self, compact=False, full=False, common_follows_with_user=None):
'feed_title': self.title,
'feed_address': "http://%s%s" % (domain, reverse('shared-stories-rss-feed',
kwargs={'user_id': self.user_id, 'username': self.username_slug})),
- 'feed_link': "http://%s%s" % (domain, reverse('load-social-page',
- kwargs={'user_id': self.user_id, 'username': self.username_slug})),
+ 'feed_link': self.blurblog_url,
}
if not compact:
params.update({
@@ -300,11 +315,11 @@ def to_json(self, compact=False, full=False, common_follows_with_user=None):
'stories_last_month': self.stories_last_month,
'average_stories_per_month': self.average_stories_per_month,
})
- if full:
+ if include_follows:
params.update({
'photo_service': self.photo_service,
- 'following_user_ids': self.following_user_ids,
- 'follower_user_ids': self.follower_user_ids,
+ 'following_user_ids': self.following_user_ids_without_self,
+ 'follower_user_ids': self.follower_user_ids_without_self,
})
if common_follows_with_user:
with_user, _ = MSocialProfile.objects.get_or_create(user_id=common_follows_with_user)
@@ -317,16 +332,28 @@ def to_json(self, compact=False, full=False, common_follows_with_user=None):
return params
+ @property
+ def following_user_ids_without_self(self):
+ if self.user_id in self.following_user_ids:
+ return [u for u in self.following_user_ids if u != self.user_id]
+ return self.following_user_ids
+
+ @property
+ def follower_user_ids_without_self(self):
+ if self.user_id in self.follower_user_ids:
+ return [u for u in self.follower_user_ids if u != self.user_id]
+ return self.follower_user_ids
+
def import_user_fields(self, skip_save=False):
user = User.objects.get(pk=self.user_id)
self.username = user.username
self.email = user.email
- def count(self, skip_save=False):
+ def count_follows(self, skip_save=False):
self.subscription_count = UserSubscription.objects.filter(user__pk=self.user_id).count()
self.shared_stories_count = MSharedStory.objects.filter(user_id=self.user_id).count()
- self.following_count = len(self.following_user_ids)
- self.follower_count = len(self.follower_user_ids)
+ self.following_count = len(self.following_user_ids_without_self)
+ self.follower_count = len(self.follower_user_ids_without_self)
if not skip_save:
self.save()
@@ -340,7 +367,7 @@ def follow_user(self, user_id, check_unfollowed=False):
self.following_user_ids.append(user_id)
if user_id in self.unfollowed_user_ids:
self.unfollowed_user_ids.remove(user_id)
- self.count()
+ self.count_follows()
self.save()
if self.user_id == user_id:
@@ -349,7 +376,7 @@ def follow_user(self, user_id, check_unfollowed=False):
followee, _ = MSocialProfile.objects.get_or_create(user_id=user_id)
if self.user_id not in followee.follower_user_ids:
followee.follower_user_ids.append(self.user_id)
- followee.count()
+ followee.count_follows()
followee.save()
following_key = "F:%s:F" % (self.user_id)
@@ -364,6 +391,9 @@ def follow_user(self, user_id, check_unfollowed=False):
subscription_user_id=user_id)
socialsub.needs_unread_recalc = True
socialsub.save()
+
+ from apps.social.tasks import EmailNewFollower
+ EmailNewFollower.delay(follower_user_id=self.user_id, followee_user_id=user_id)
def is_following_user(self, user_id):
return user_id in self.following_user_ids
@@ -382,13 +412,13 @@ def unfollow_user(self, user_id):
self.following_user_ids.remove(user_id)
if user_id not in self.unfollowed_user_ids:
self.unfollowed_user_ids.append(user_id)
- self.count()
+ self.count_follows()
self.save()
followee = MSocialProfile.objects.get(user_id=user_id)
if self.user_id in followee.follower_user_ids:
followee.follower_user_ids.remove(self.user_id)
- followee.count()
+ followee.count_follows()
followee.save()
following_key = "F:%s:F" % (self.user_id)
@@ -408,7 +438,49 @@ def common_follows(self, user_id, direction='followers'):
follows_inter = [int(f) for f in follows_inter]
follows_diff = [int(f) for f in follows_diff]
+ if user_id in follows_inter:
+ follows_inter.remove(user_id)
+ if user_id in follows_diff:
+ follows_diff.remove(user_id)
+
return follows_inter, follows_diff
+
+ def send_email_for_new_follower(self, follower_user_id):
+ user = User.objects.get(pk=self.user_id)
+ if not user.email or not user.profile.send_emails:
+ return
+
+ follower_profile = MSocialProfile.objects.get(user_id=follower_user_id)
+ photo_url = follower_profile.profile_photo_url
+ if 'graph.facebook.com' in photo_url:
+ follower_profile.photo_url = photo_url + '?type=large'
+ elif 'twimg' in photo_url:
+ follower_profile.photo_url = photo_url.replace('_normal', '')
+
+ common_followers, _ = self.common_follows(follower_user_id, direction='followers')
+ common_followings, _ = self.common_follows(follower_user_id, direction='following')
+ common_followers.remove(self.user_id)
+ common_followings.remove(self.user_id)
+ common_followers = MSocialProfile.profiles(common_followers)
+ common_followings = MSocialProfile.profiles(common_followings)
+
+ data = {
+ 'user': user,
+ 'follower_profile': follower_profile,
+ 'common_followers': common_followers,
+ 'common_followings': common_followings,
+ }
+
+ text = render_to_string('mail/email_new_follower.txt', data)
+ html = render_to_string('mail/email_new_follower.xhtml', data)
+ subject = "%s is now following your Blurblog on NewsBlur!" % follower_profile.username
+ msg = EmailMultiAlternatives(subject, text,
+ from_email='NewsBlur <%s>' % settings.HELLO_EMAIL,
+ to=['%s <%s>' % (user.username, user.email)])
+ msg.attach_alternative(html, "text/html")
+ msg.send()
+
+ logging.user(user, "~BB~FM~SBSending email for new follower: %s" % follower_profile.username)
def save_feed_story_history_statistics(self):
"""
@@ -854,6 +926,7 @@ class MSharedStory(mongo.Document):
story_permalink = mongo.StringField()
story_guid = mongo.StringField(unique_with=('user_id',))
story_tags = mongo.ListField(mongo.StringField(max_length=250))
+ posted_to_services = mongo.ListField(mongo.StringField(max_length=20))
meta = {
'collection': 'shared_stories',
@@ -891,7 +964,7 @@ def save(self, *args, **kwargs):
super(MSharedStory, self).save(*args, **kwargs)
author, _ = MSocialProfile.objects.get_or_create(user_id=self.user_id)
- author.count()
+ author.count_follows()
MActivity.new_shared_story(user_id=self.user_id, story_title=self.story_title,
comments=self.comments, story_feed_id=self.story_feed_id,
@@ -902,9 +975,18 @@ def delete(self, *args, **kwargs):
share_key = "S:%s:%s" % (self.story_feed_id, self.guid_hash)
r.srem(share_key, self.user_id)
+ comment_key = "C:%s:%s" % (self.story_feed_id, self.guid_hash)
+ r.srem(comment_key, self.user_id)
+
+ MActivity.remove_shared_story(user_id=self.user_id, story_feed_id=self.story_feed_id,
+ story_id=self.story_guid)
+
super(MSharedStory, self).delete(*args, **kwargs)
def set_source_user_id(self, source_user_id, original_comments=None):
+ if source_user_id == self.user_id:
+ return
+
def find_source(source_user_id, seen_user_ids):
parent_shared_story = MSharedStory.objects.filter(user_id=source_user_id,
story_guid=self.story_guid,
@@ -1104,6 +1186,39 @@ def stories_with_comments_and_profiles(cls, stories, user, check_all=False):
profiles = [profile.to_json(compact=True) for profile in profiles]
return stories, profiles
+
+ def blurblog_permalink(self):
+ profile = MSocialProfile.objects.get(user_id=self.user_id)
+ return "%s/story/%s" % (
+ profile.blurblog_url,
+ self.guid_hash[:6]
+ )
+
+ def generate_post_to_service_message(self):
+ message = self.comments
+ if not message or len(message) < 1:
+ message = self.story_title
+
+ message = truncate_chars(message, 116)
+ message += " " + self.blurblog_permalink()
+ print message
+
+ return message
+
+ def post_to_service(self, service):
+ if service in self.posted_to_services:
+ return
+
+ message = self.generate_post_to_service_message()
+ social_service = MSocialServices.objects.get(user_id=self.user_id)
+ if service == 'twitter':
+ posted = social_service.post_to_twitter(message)
+ elif service == 'facebook':
+ posted = social_service.post_to_facebook(message)
+
+ if posted:
+ self.posted_to_services.append(service)
+ self.save()
class MSocialServices(mongo.Document):
@@ -1154,6 +1269,14 @@ def to_json(self):
}
}
+ @classmethod
+ def profile(cls, user_id):
+ try:
+ profile = cls.objects.get(user_id=user_id)
+ except cls.DoesNotExist:
+ return {}
+ return profile.to_json()
+
def twitter_api(self):
twitter_consumer_key = settings.TWITTER_CONSUMER_KEY
twitter_consumer_secret = settings.TWITTER_CONSUMER_SECRET
@@ -1189,7 +1312,7 @@ def sync_twitter_friends(self):
profile.bio = profile.bio or twitter_user.description
profile.website = profile.website or twitter_user.url
profile.save()
- profile.count()
+ profile.count_follows()
if not profile.photo_url or not profile.photo_service:
self.set_photo('twitter')
@@ -1216,7 +1339,7 @@ def sync_facebook_friends(self):
profile.bio = profile.bio or facebook_user.get('bio')
profile.website = profile.website or facebook_user.get('website')
profile.save()
- profile.count()
+ profile.count_follows()
if not profile.photo_url or not profile.photo_service:
self.set_photo('facebook')
@@ -1300,7 +1423,26 @@ def set_photo(self, service):
hashlib.md5(user.email).hexdigest()
profile.save()
return profile
+
+ def post_to_twitter(self, message):
+ try:
+ api = self.twitter_api()
+ api.update_status(status=message)
+ except tweepy.TweepError, e:
+ print e
+ return
+
+ return True
+
+ def post_to_facebook(self, message):
+ try:
+ api = self.facebook_api()
+ api.put_wall_post(message=message)
+ except facebook.GraphAPIError, e:
+ print e
+ return
+ return True
class MInteraction(mongo.Document):
user_id = mongo.IntField()
@@ -1578,3 +1720,17 @@ def new_shared_story(cls, user_id, story_title, comments, story_feed_id, story_i
if share_date:
a.date = share_date
a.save()
+
+ @classmethod
+ def remove_shared_story(cls, user_id, story_feed_id, story_id):
+ try:
+ a = cls.objects.get(user_id=user_id,
+ with_user_id=user_id,
+ category='sharedstory',
+ feed_id=story_feed_id,
+ content_id=story_id)
+ except cls.DoesNotExist:
+ return
+
+ a.delete()
+
View
18 apps/social/tasks.py
@@ -0,0 +1,18 @@
+from celery.task import Task
+from apps.social.models import MSharedStory, MSocialProfile
+
+
+class PostToService(Task):
+
+ def run(self, shared_story_id, service):
+ try:
+ shared_story = MSharedStory.objects.get(id=shared_story_id)
+ shared_story.post_to_service(service)
+ except MSharedStory.DoesNotExist:
+ print "Story not found (%s). Can't post to: %s" % (shared_story_id, service)
+
+class EmailNewFollower(Task):
+
+ def run(self, follower_user_id, followee_user_id):
+ user_profile = MSocialProfile.objects.get(user_id=followee_user_id)
+ user_profile.send_email_for_new_follower(follower_user_id)
View
1  apps/social/urls.py
@@ -4,6 +4,7 @@
urlpatterns = patterns('',
url(r'^request_invite/?$', views.request_invite, name='request-invite'),
url(r'^share_story/?$', views.mark_story_as_shared, name='mark-story-as-shared'),
+ url(r'^unshare_story/?$', views.mark_story_as_unshared, name='mark-story-as-unshared'),
url(r'^load_user_friends/?$', views.load_user_friends, name='load-user-friends'),
url(r'^profile/?$', views.profile, name='profile'),
url(r'^load_user_profile/?$', views.load_user_profile, name='load-user-profile'),
View
92 apps/social/views.py
@@ -10,6 +10,7 @@
from apps.rss_feeds.models import MStory, Feed, MStarredStory
from apps.social.models import MSharedStory, MSocialServices, MSocialProfile, MSocialSubscription, MCommentReply
from apps.social.models import MRequestInvite, MInteraction, MActivity
+from apps.social.tasks import PostToService
from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag
from apps.analyzer.models import apply_classifier_titles, apply_classifier_feeds, apply_classifier_authors, apply_classifier_tags
from apps.analyzer.models import get_classifiers_for_user, sort_classifiers_by_feed
@@ -172,11 +173,18 @@ def load_social_page(request, user_id, username=None):
page = request.REQUEST.get('page')
if page: offset = limit * (int(page) - 1)
+ social_profile = MSocialProfile.objects.get(user_id=social_user_id)
mstories = MSharedStory.objects(user_id=social_user.pk).order_by('-shared_date')[offset:offset+limit]
stories = Feed.format_stories(mstories)
if not stories:
- return dict(stories=[])
+ return {
+ "user": user,
+ "stories": [],
+ "feeds": {},
+ "social_user": social_user,
+ "social_profile": social_profile.page(),
+ }
story_feed_ids = list(set(s['story_feed_id'] for s in stories))
feeds = Feed.objects.filter(pk__in=story_feed_ids)
@@ -189,7 +197,19 @@ def load_social_page(request, user_id, username=None):
story['shared_date'] = shared_date
stories, profiles = MSharedStory.stories_with_comments_and_profiles(stories, user, check_all=True)
- social_profile = MSocialProfile.objects.get(user_id=social_user_id)
+ profiles = dict([(p['user_id'], p) for p in profiles])
+
+ for s, story in enumerate(stories):
+ for u, user_id in enumerate(story['shared_by_friends']):
+ stories[s]['shared_by_friends'][u] = profiles[user_id]
+ for u, user_id in enumerate(story['shared_by_public']):
+ stories[s]['shared_by_public'][u] = profiles[user_id]
+ for c, comment in enumerate(story['comments']):
+ stories[s]['comments'][c]['user'] = profiles[comment['user_id']]
+ if comment['source_user_id']:
+ stories[s]['comments'][c]['source_user'] = profiles[comment['source_user_id']]
+ for r, reply in enumerate(comment['replies']):
+ stories[s]['comments'][c]['replies'][r]['user'] = profiles[reply['user_id']]
params = {
'user': user,
@@ -226,10 +246,14 @@ def mark_story_as_shared(request):
story_id = request.POST['story_id']
comments = request.POST.get('comments', '')
source_user_id = request.POST.get('source_user_id')
+ post_to_services = request.POST.getlist('post_to_services')
story = MStory.objects(story_feed_id=feed_id, story_guid=story_id).limit(1).first()
if not story:
- return {'code': -1, 'message': 'Story not found. Reload this site.'}
+ return {
+ 'code': -1,
+ 'message': 'The original story is gone. This would be a nice bug to fix. Speak up.'
+ }
shared_story = MSharedStory.objects.filter(user_id=request.user.pk,
story_feed_id=feed_id,
@@ -240,7 +264,8 @@ def mark_story_as_shared(request):
story_values = dict(user_id=request.user.pk, comments=comments,
has_comments=bool(comments), **story_db)
shared_story = MSharedStory.objects.create(**story_values)
- shared_story.set_source_user_id(source_user_id)
+ if source_user_id:
+ shared_story.set_source_user_id(int(source_user_id))
socialsubs = MSocialSubscription.objects.filter(subscription_user_id=request.user.pk)
for socialsub in socialsubs:
socialsub.needs_unread_recalc = True
@@ -248,11 +273,9 @@ def mark_story_as_shared(request):
logging.user(request, "~FCSharing ~FM%s: ~SB~FB%s" % (story.story_title[:20], comments[:30]))
else:
shared_story = shared_story[0]
- # original_comments = shared_story.comments
shared_story.comments = comments
shared_story.has_comments = bool(comments)
shared_story.save()
- # shared_story.set_source_user_id(source_user_id, original_comments=original_comments)
logging.user(request, "~FCUpdating shared story ~FM%s: ~SB~FB%s" % (
story.story_title[:20], comments[:30]))
@@ -264,10 +287,50 @@ def mark_story_as_shared(request):
story = stories[0]
story['shared_comments'] = shared_story['comments'] or ""
+ if post_to_services:
+ for service in post_to_services:
+ if service not in shared_story.posted_to_services:
+ PostToService.delay(shared_story_id=shared_story.id, service=service)
+
return {'code': code, 'story': story, 'user_profiles': profiles}
@ajax_login_required
@json.json_view
+def mark_story_as_unshared(request):
+ feed_id = int(request.POST['feed_id'])
+ story_id = request.POST['story_id']
+
+ story = MStory.objects(story_feed_id=feed_id, story_guid=story_id).limit(1).first()
+ if not story:
+ return {'code': -1, 'message': 'Story not found. Reload this site.'}
+
+ try:
+ shared_story = MSharedStory.objects.get(user_id=request.user.pk,
+ story_feed_id=feed_id,
+ story_guid=story_id)
+ except MSharedStory.DoesNotExist:
+ return {'code': -1, 'message': 'Shared story not found.'}
+
+ socialsubs = MSocialSubscription.objects.filter(subscription_user_id=request.user.pk)
+ for socialsub in socialsubs:
+ socialsub.needs_unread_recalc = True
+ socialsub.save()
+ logging.user(request, "~FC~SKUn-sharing ~FM%s: ~SB~FB%s" % (shared_story.story_title[:20],
+ shared_story.comments[:30]))
+ shared_story.delete()
+
+ story.count_comments()
+
+ story = Feed.format_story(story)
+ stories, profiles = MSharedStory.stories_with_comments_and_profiles([story],
+ request.user,
+ check_all=True)
+ story = stories[0]
+
+ return {'code': 1, 'message': "Story unshared.", 'story': story, 'user_profiles': profiles}
+
+@ajax_login_required
+@json.json_view
def save_comment_reply(request):
code = 1
feed_id = int(request.POST['story_feed_id'])
@@ -354,7 +417,8 @@ def profile(request):
user = get_user(request.user)
user_id = request.GET.get('user_id', user.pk)
user_profile = MSocialProfile.objects.get(user_id=user_id)
- user_profile = user_profile.to_json(full=True, common_follows_with_user=user.pk)
+ user_profile.count_follows()
+ user_profile = user_profile.to_json(include_follows=True, common_follows_with_user=user.pk)
profile_ids = set(user_profile['followers_youknow'] + user_profile['followers_everybody'] +
user_profile['following_youknow'] + user_profile['following_everybody'])
profiles = MSocialProfile.profiles(profile_ids)
@@ -387,7 +451,7 @@ def load_user_profile(request):
return {
'services': social_services,
- 'user_profile': social_profile.to_json(full=True),
+ 'user_profile': social_profile.to_json(include_follows=True),
}
@ajax_login_required
@@ -406,7 +470,7 @@ def save_user_profile(request):
logging.user(request, "~BB~FRSaving social profile")
- return dict(code=1, user_profile=profile.to_json(full=True))
+ return dict(code=1, user_profile=profile.to_json(include_follows=True))
@json.json_view
def load_user_friends(request):
@@ -420,7 +484,7 @@ def load_user_friends(request):
return {
'services': social_services,
'autofollow': social_services.autofollow,
- 'user_profile': social_profile.to_json(full=True),
+ 'user_profile': social_profile.to_json(include_follows=True),
'following_profiles': following_profiles,
'follower_profiles': follower_profiles,
'recommended_users': recommended_users,
@@ -459,7 +523,7 @@ def follow(request):
logging.user(request, "~BB~FRFollowing: %s" % follow_profile.username)
return {
- "user_profile": profile.to_json(full=True),
+ "user_profile": profile.to_json(include_follows=True),
"follow_profile": follow_profile.to_json(common_follows_with_user=request.user.pk),
"follow_subscription": follow_subscription,
}
@@ -489,7 +553,7 @@ def unfollow(request):
logging.user(request, "~BB~FRUnfollowing: %s" % unfollow_profile.username)
return {
- 'user_profile': profile.to_json(full=True),
+ 'user_profile': profile.to_json(include_follows=True),
'unfollow_profile': unfollow_profile.to_json(common_follows_with_user=request.user.pk),
}
@@ -518,7 +582,7 @@ def shared_stories_rss_feed(request, user_id, username):
social_profile = MSocialProfile.objects.get(user_id=user_id)
data = {}
- data['title'] = social_profile.blog_title
+ data['title'] = social_profile.title
link = reverse('shared-stories-public', kwargs={'username': user.username})
data['link'] = "http://www.newsblur.com/%s" % link
data['description'] = "Stories shared by %s on NewsBlur." % user.username
@@ -600,4 +664,4 @@ def load_interactions(request):
return {
'interactions': interactions,
'page': page,
- }
+ }
View
11 assets.yml
@@ -50,7 +50,7 @@ javascripts:
- media/js/vendor/jquery.flot.js
- media/js/vendor/jquery.tipsy.js
- media/js/vendor/jquery.chosen.js
- - media/js/vendor/jquery.linkify.js
+ # - media/js/vendor/jquery.linkify.js
- media/js/vendor/bootstrap.*.js
- media/js/vendor/audio.js
- media/js/vendor/socket.io-client.*.js
@@ -90,6 +90,11 @@ javascripts:
- media/js/vendor/jquery.tinysort.js
- media/js/vendor/jquery.simplemodal-1.3.js
- media/js/vendor/jquery.corners.js
+ blurblog:
+ - media/js/vendor/jquery-*.js
+ - media/js/vendor/underscore-*.js
+ - media/js/vendor/underscore.string.js
+ - media/js/vendor/backbone-*.js
stylesheets:
common:
@@ -104,4 +109,6 @@ stylesheets:
- media/css/mobile/mobile.css
bookmarklet:
- media/css/bookmarklet/reset.css
- - media/css/modals.css
+ - media/css/modals.css
+ blurblog:
+ - media/css/social/social_page.css
View
2  config/fixtures/bootstrap.json
@@ -37,7 +37,7 @@
"groups": [],
"user_permissions": [],
"password": "sha1$d5473$d07ce4495b088ff0f41a62d5113d0189ce8f0096",
- "email": "samuel@ofbrooklyn.com",
+ "email": "samuel@newsblur.com",
"date_joined": "2011-07-18 00:23:49"
}
},
View
2  config/hosts
@@ -8,6 +8,8 @@
199.15.252.50 db02 db02.newsblur.com
199.15.253.226 db03 db03.newsblur.com
199.15.249.98 db04 db04.newsblur.com
+199.15.249.99 db05 db05.newsblur.com
+199.15.249.100 db06 db06.newsblur.com
199.15.250.231 task01 task01.newsblur.com
199.15.250.250 task02 task02.newsblur.com
199.15.250.233 task03 task03.newsblur.com
View
4 config/mongodb.prod.conf
@@ -14,7 +14,7 @@ logappend=true
#port = 27017
-slowms=1000
+slowms=100
rest = true
#profile = 2
@@ -88,5 +88,3 @@ noauth = true
# in replica set configuration, specify the name of the replica set
replSet = nbset
-
-journal = true
View
56 config/nginx.newsblur.conf
@@ -1,36 +1,40 @@
upstream app_server {
server 127.0.0.1:8000 fail_timeout=10 max_fails=3 ;
- server app02:8000 fail_timeout=10 max_fails=3 down;
}
upstream icon_server {
- server 127.0.0.1:3030 fail_timeout=10 max_fails=3 ;
+ server 127.0.0.1:3030 fail_timeout=2 max_fails=3;
server 127.0.0.1:8000 backup;
}
-# server {
-# server_name newsblur.com;
-# rewrite ^(.*) http://www.newsblur.com$1 permanent;
-# }
-
server {
listen 80;
listen 443 default_server ssl;
- ssl on;
+ # ssl on;
ssl_certificate /home/sclay/newsblur/config/certificates/newsblur.com.crt;
ssl_certificate_key /home/sclay/newsblur/config/certificates/newsblur.com.key;
client_max_body_size 4M;
- server_name www.newsblur.com push.newsblur.com 199.15.250.228 127.0.0.1;
+ server_name www.newsblur.com newsblur.com dev.newsblur.com *.newsblur.com;
# if ($host = 'newsblur.com') {
# rewrite ^/(.*)$ https://www.newsblur.com/$1 permanent;
# }
-
+ if (-f /home/sclay/newsblur/media/maintenance.html) {
+ return 503;
+ }
+
+ error_page 502 @down;
+ location @down {
+ root /home/sclay/newsblur/;
+ rewrite ^(.*)$ /templates/502.html break;
+ }
+
error_page 503 @maintenance;
location @maintenance {
- rewrite ^(.*)$ /home/sclay/newsblur/media/maintenance.html break;
+ root /home/sclay/newsblur/;
+ rewrite ^(.*)$ /media/maintenance.html break;
}
location /media/ {
@@ -46,14 +50,14 @@ server {
}
location /favicon.ico {
- alias /home/sclay/newsblur/media/img/favicon.png;
- expires max;
- access_log off;
+ alias /home/sclay/newsblur/media/img/favicon.png;
+ expires max;
+ access_log off;
}
location ^~ /crossdomain.xml {
expires max;
- alias /home/sclay/newsblur/media/crossdomain.xml;
+ alias /home/sclay/newsblur/media/crossdomain.xml;
types {
text/x-cross-domain-policy xml;
}
@@ -61,19 +65,11 @@ server {
location ^~ /robots.txt {
expires max;
- alias /home/sclay/newsblur/media/robots.txt;
+ alias /home/sclay/newsblur/media/robots.txt;
}
location /munin/ {
- alias /var/cache/munin/www/;
- }
-
- location /nginx_status {
- stub_status on;
- access_log off;
- allow 127.0.0.1;
- allow 199.15.250.228;
- deny all;
+ alias /var/cache/munin/www/;
}
location ^~ /rss_feeds/icon/ {
@@ -82,18 +78,10 @@ server {
proxy_set_header Host $http_host;
proxy_redirect off;
- if (!-f $request_filename) {
- proxy_pass http://icon_server;
- break;
- }
+ proxy_pass http://icon_server;
}
location / {
- if (-f /home/sclay/newsblur/media/maintenance.html) {
- return 503;
- }
-
- proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
View
8 config/postgresql.conf
@@ -38,15 +38,15 @@
# The default values of these variables are driven from the -D command-line
# option or PGDATA environment variable, represented here as ConfigDir.
-data_directory = '/var/lib/postgresql/9.0/main' # use data in another directory
+data_directory = '/var/lib/postgresql/9.1/main' # use data in another directory
# (change requires restart)
-hba_file = '/etc/postgresql/9.0/main/pg_hba.conf' # host-based authentication file
+hba_file = '/etc/postgresql/9.1/main/pg_hba.conf' # host-based authentication file
# (change requires restart)
-ident_file = '/etc/postgresql/9.0/main/pg_ident.conf' # ident configuration file
+ident_file = '/etc/postgresql/9.1/main/pg_ident.conf' # ident configuration file
# (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
-external_pid_file = '/var/run/postgresql/9.0-main.pid' # write an extra PID file
+external_pid_file = '/var/run/postgresql/9.1-main.pid' # write an extra PID file
# (change requires restart)
View
2  config/supervisor_celeryd.conf
@@ -1,5 +1,5 @@
[program:celery]
-command=/home/sclay/newsblur/manage.py celeryd --loglevel=INFO -Q new_feeds,push_feeds,update_feeds
+command=/home/sclay/newsblur/manage.py celeryd --loglevel=INFO -Q new_feeds,work_queue,push_feeds,update_feeds
directory=/home/sclay/newsblur
user=sclay
numprocs=1
View
29 fabfile.py
@@ -41,7 +41,9 @@
'db': ['db01.newsblur.com',
'db02.newsblur.com',
'db03.newsblur.com',
- 'db04.newsblur.com'],
+ 'db04.newsblur.com',
+ 'db05.newsblur.com',
+ 'db06.newsblur.com'],
'task': ['task01.newsblur.com',
'task02.newsblur.com',
'task03.newsblur.com',
@@ -265,13 +267,12 @@ def setup_db():
setup_baremetal()
setup_db_firewall()
setup_db_motd()
- # setup_rabbitmq()
copy_task_settings()
setup_memcached()
- setup_postgres()
+ # setup_postgres()
setup_mongo()
setup_gunicorn(supervisor=False)
- setup_redis()
+ # setup_redis()
setup_db_munin()
def setup_task():
@@ -531,6 +532,14 @@ def copy_certificates():
run('mkdir -p %s/config/certificates/' % env.NEWSBLUR_PATH)
put('config/certificates/comodo/newsblur.com.crt', '%s/config/certificates/' % env.NEWSBLUR_PATH)
put('config/certificates/comodo/newsblur.com.key', '%s/config/certificates/' % env.NEWSBLUR_PATH)
+
+def maintenance_on():
+ with cd(env.NEWSBLUR_PATH):
+ run('mv media/maintenance.html.unused media/maintenance.html')
+
+def maintenance_off():
+ with cd(env.NEWSBLUR_PATH):
+ run('mv media/maintenance.html media/maintenance.html.unused')
# ==============
# = Setup - DB =
@@ -566,17 +575,17 @@ def setup_memcached():
sudo('apt-get -y install memcached')
def setup_postgres(standby=False):
- shmmax = 572506112
-# sudo('apt-get -y install postgresql postgresql-client postgresql-contrib libpq-dev')
+ shmmax = 577060864
+ sudo('apt-get -y install postgresql postgresql-client postgresql-contrib libpq-dev')
put('config/postgresql%s.conf' % (
('_standby' if standby else ''),
- ), '/etc/postgresql/9.0/main/postgresql.conf', use_sudo=True)
+ ), '/etc/postgresql/9.1/main/postgresql.conf', use_sudo=True)
sudo('echo "%s" > /proc/sys/kernel/shmmax' % shmmax)
sudo('echo "\nkernel.shmmax = %s" > /etc/sysctl.conf' % shmmax)
sudo('sysctl -p')
if standby:
- put('config/postgresql_recovery.conf', '/var/lib/postgresql/9.0/recovery.conf', use_sudo=True)
+ put('config/postgresql_recovery.conf', '/var/lib/postgresql/9.1/recovery.conf', use_sudo=True)
sudo('/etc/init.d/postgresql stop')
sudo('/etc/init.d/postgresql start')
@@ -587,9 +596,11 @@ def setup_mongo():
sudo('echo "deb http://downloads-distro.mongodb.org/repo/debian-sysvinit dist 10gen" >> /etc/apt/sources.list')
sudo('apt-get update')
sudo('apt-get -y install mongodb-10gen')
+ put('config/mongodb.prod.conf', '/etc/mongodb.conf', use_sudo=True)
+ sudo('/etc/init.d/mongodb restart')
def setup_redis():
- redis_version = '2.4.13'
+ redis_version = '2.4.15'
with cd(env.VENDOR_PATH):
run('wget http://redis.googlecode.com/files/redis-%s.tar.gz' % redis_version)
run('tar -xzf redis-%s.tar.gz' % redis_version)
View
35 local_settings.py.template
@@ -6,16 +6,16 @@ import pymongo
# ===================
ADMINS = (
- ('Samuel Clay', 'samuel@ofbrooklyn.com'),
+ ('Samuel Clay', 'samuel@newsblur.com'),
)
SERVER_EMAIL = 'server@newsblur.com'
HELLO_EMAIL = 'hello@newsblur.com'
NEWSBLUR_URL = 'http://www.newsblur.com'
-# ==================
-# = Global Settngs =
-# ==================
+# ===================
+# = Global Settings =
+# ===================
DEBUG = True
DEBUG_ASSETS = DEBUG
@@ -29,7 +29,7 @@ CACHE_BACKEND = 'dummy:///'
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Set this to the username that is shown on the homepage to unauthenticated users.
-HOMEPAGE_USERNAME = 'conesus'
+HOMEPAGE_USERNAME = 'popular'
# Google Reader OAuth API Keys
OAUTH_KEY = 'www.example.com'
@@ -42,6 +42,15 @@ S3_BACKUP_BUCKET = 'newsblur_backups'
STRIPE_SECRET = "YOUR-SECRET-API-KEY"
STRIPE_PUBLISHABLE = "YOUR-PUBLISHABLE-API-KEY"
+# ===============
+# = Social APIs =
+# ===============
+
+FACEBOOK_APP_ID = '111111111111111'
+FACEBOOK_SECRET = '99999999999999999999999999999999'
+TWITTER_CONSUMER_KEY = 'ooooooooooooooooooooo'
+TWITTER_CONSUMER_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
+
# =============
# = Databases =
# =============
@@ -80,7 +89,6 @@ BROKER_USER = "newsblur"
BROKER_PASSWORD = "newsblur"
BROKER_VHOST = "newsblurvhost"
-
# ===========
# = Logging =
# ===========
@@ -94,19 +102,4 @@ if len(logging._handlerList) < 1:
format='%(asctime)-12s: %(message)s',
datefmt='%b %d %H:%M:%S',
handler=logging.StreamHandler)
-<<<<<<< HEAD
-S3_ACCESS_KEY = 'XXX'
-S3_SECRET = 'SECRET'
-S3_BACKUP_BUCKET = 'newsblur_backups'
-
-# ===============
-# = Social APIs =
-# ===============
-
-FACEBOOK_APP_ID = '111111111111111'
-FACEBOOK_SECRET = '99999999999999999999999999999999'
-TWITTER_CONSUMER_KEY = 'ooooooooooooooooooooo'
-TWITTER_CONSUMER_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
-=======
->>>>>>> jammit
View
138 media/css/reader.css
@@ -378,11 +378,12 @@ body.NB-theme-serif #story_pane .NB-feed-story-content {
.NB-feedlists .NB-socialfeeds {
border-bottom: 1px solid #A0A0A0;
}
-..NB-feedlists .NB-socialfeeds .feed .feed_title {
+.NB-feedlists .NB-socialfeeds .feed .feed_title {
text-shadow: 0 1px 0 #DAE2E8;
}
-
-
+.NB-feedlists .NB-socialfeeds img.feed_favicon {
+ border-radius: 3px;
+}
/* ============= */
/* = Feed List = */
/* ============= */
@@ -494,6 +495,10 @@ body.NB-theme-serif #story_pane .NB-feed-story-content {
.NB-feedlist .feed.NB-feed-inactive {
display: none;
}
+.NB-feedlist .feed.NB-feed-self-blurblog,
+.NB-feedlist-hide-read-feeds .NB-feedlist .feed.NB-feed-self-blurblog {
+ display: block;
+}
.NB-feedlist .feed.NB-feed-unfetched {
}
@@ -991,7 +996,9 @@ background: transparent;
width: 16px;
height: 16px;
}
-
+#story_titles .NB-feedbar .feed.NB-feed-social .feed_favicon {
+ border-radius: 3px;
+}
#story_titles .NB-feedbar .feed .feed_title {
/* float: left;*/
display: block;
@@ -1629,6 +1636,7 @@ background: transparent;
z-index: 2;
width: 100%;
display: none;
+ opacity: .9;
}
.NB-view-river .NB-feed-story-view-floater {
@@ -1728,7 +1736,6 @@ background: transparent;
border-bottom: 1px solid #000;
border-top: 1px solid #707070;
z-index: 2;
- opacity: .9;
}
#story_pane .NB-feed-story-header-feed.NB-feed-story-river-same-feed {
z-index: 0;
@@ -1770,8 +1777,12 @@ background: transparent;
/* text-shadow: 0 1px 0 #E0E0E0;*/
}
+#story_pane .NB-feed-stories .NB-feed-story .NB-feed-story-content div {
+ max-width: 100%;
+}
#story_pane .NB-feed-stories .NB-feed-story img {
max-width: 100%;
+ height: auto;
}
#story_pane .NB-feed-story {
position: relative;
@@ -2216,10 +2227,6 @@ background: transparent;
#story_pane .NB-story-comments-public-header-wrapper {
cursor: default;
}
-#story_pane .NB-story-comments-shares-teaser-wrapper {
- border-top: 0;
- padding-top: 0;
-}
#story_pane .NB-story-comments-public-teaser,
#story_pane .NB-story-comments-public-header {
background-color: #B1B6B4;
@@ -2240,6 +2247,12 @@ background: transparent;
color: #404040;
text-shadow: 0 1px 0 white;
}
+
+#story_pane .NB-story-comments-shares-teaser-wrapper {
+ border-top: 0;
+ padding-top: 0;
+}
+
#story_pane .NB-story-comments-shares-teaser {
background-color: whiteSmoke;
color: #202020;
@@ -2322,6 +2335,11 @@ background: transparent;
#story_pane .NB-feed-stories.NB-feed-view-story .NB-feed-story {
padding: 0;
+ display: none;
+}
+
+#story_pane .NB-feed-stories.NB-feed-view-story .NB-feed-story.NB-selected {
+ display: block;
}
#story_pane .audiojs audio {
@@ -2414,12 +2432,35 @@ background: transparent;
text-shadow: 0 1px 0 #F6F6F6;
color: #202020;
}
-.NB-sideoption-share .NB-sideoption-share-optional {
- text-transform: uppercase;
+.NB-sideoption-share .NB-sideoption-share-crosspost {
+ margin-right: -4px;
+}
+.NB-sideoption-share .NB-sideoption-share-crosspost-twitter,
+.NB-sideoption-share .NB-sideoption-share-crosspost-facebook {
float: right;
- color: #808080;
- font-size: 10px;
- text-shadow: 0 1px 0 #F6F6F6;
+ width: 16px;
+ height: 16px;
+ margin: 0 0 0 6px;
+ opacity: .4;
+ cursor: pointer;
+ -webkit-filter: grayscale(100%);
+ display: none;
+}
+.NB-sideoption-share .NB-sideoption-share-crosspost-twitter:hover,
+.NB-sideoption-share .NB-sideoption-share-crosspost-facebook:hover {
+ opacity: .7;
+ -webkit-filter: none;
+}
+.NB-sideoption-share .NB-sideoption-share-crosspost-twitter.NB-active,
+.NB-sideoption-share .NB-sideoption-share-crosspost-facebook.NB-active {
+ opacity: 1;
+ -webkit-filter: none;
+}
+.NB-sideoption-share .NB-sideoption-share-crosspost-twitter {
+ background: transparent url('/media/embed/reader/twitter_icon.png') no-repeat 0 0;
+}
+.NB-sideoption-share .NB-sideoption-share-crosspost-facebook {
+ background: transparent url('/media/embed/reader/facebook_icon.png') no-repeat 0 0;
}
.NB-sideoption-share .NB-sideoption-share-comments {
width: 100%;
@@ -2435,15 +2476,28 @@ background: transparent;
width: 92%;
background-color: #639510;
cursor: pointer;
- -moz-box-shadow:2px 2px 0 #95AB76;
- -webkit-box-shadow:2px 2px 0 #95AB76;
- box-shadow:2px 2px 0 #95AB76;
+ -moz-box-shadow: 2px 2px 0 #95AB76;
+ -webkit-box-shadow: 2px 2px 0 #95AB76;
+ box-shadow: 2px 2px 0 #95AB76;
text-shadow: 0 1px 0 #101010;
}
.NB-sideoption-share .NB-sideoption-share-save.NB-saving {
background-color: #b5b4bB;
text-shadow: none;
}
+.NB-sideoption-share .NB-sideoption-share-unshare {
+ color: #404040;
+ text-shadow: 0 1px 0 #E0E0E0;
+ line-height: 1;
+ font-size: 11px;
+ padding: 2px 6px;
+ margin: 6px 0;
+ width: 92%;
+ font-weight: normal;
+ -moz-box-shadow: 1px 1px 0 #95AB76;
+ -webkit-box-shadow: 1px 1px 0 #95AB76;
+ box-shadow: 1px 1px 0 #95AB76;
+}
.NB-sideoption-share .NB-error {
font-size: 10px;
color: #6A1000;
@@ -4995,6 +5049,14 @@ form.opml_import_form input {
text-align: center;
width: 95%;
}
+.NB-menu-manage .NB-menu-manage-confirm .NB-menu-manage-story-share-unshare {
+ width: 95%;
+ text-align: center;
+ margin-bottom: 0;
+}
+.NB-menu-manage .NB-menu-manage-confirm .NB-sideoption-share-comments {
+ height: 28px;
+}
.NB-menu-manage .NB-menu-manage-confirm .NB-add-folders {
float: left;
}
@@ -5052,6 +5114,15 @@ form.opml_import_form input {
.NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-pinboard {
background: transparent url('/media/embed/reader/pinboard.png') no-repeat 0 0;
}
+.NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-diigo {
+ background: transparent url('/media/embed/reader/diigo.png') no-repeat 0 0;
+}
+.NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-kippt {
+ background: transparent url('/media/embed/reader/kippt.png') no-repeat 0 0;
+}
+.NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-evernote {
+ background: transparent url('/media/embed/reader/evernote.png') no-repeat 0 0;
+}
.NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-googleplus {
background: transparent url('/media/embed/reader/googleplus.png') no-repeat 0 0;
}
@@ -5110,6 +5181,27 @@ form.opml_import_form input {
.NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-pinboard .NB-menu-manage-thirdparty-pinboard {
opacity: 1;
}
+.NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-diigo .NB-menu-manage-image,
+.NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-diigo .NB-menu-manage-thirdparty-icon {
+ opacity: .2;
+}
+.NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-diigo .NB-menu-manage-thirdparty-diigo {
+ opacity: 1;
+}
+.NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-kippt .NB-menu-manage-image,
+.NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-kippt .NB-menu-manage-thirdparty-icon {
+ opacity: .2;
+}
+.NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-kippt .NB-menu-manage-thirdparty-kippt {
+ opacity: 1;
+}
+.NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-evernote .NB-menu-manage-image,
+.NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-evernote .NB-menu-manage-thirdparty-icon {
+ opacity: .2;
+}
+.NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-evernote .NB-menu-manage-thirdparty-evernote {
+ opacity: 1;
+}
.NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-googleplus .NB-menu-manage-image,
.NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-googleplus .NB-menu-manage-thirdparty-icon {
opacity: .2;
@@ -6887,6 +6979,15 @@ form.opml_import_form input {
.NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-pinboard] {
background: transparent url('/media/embed/reader/pinboard.png') no-repeat 0 0;
}
+.NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-diigo] {
+ background: transparent url('/media/embed/reader/diigo.png') no-repeat 0 0;
+}
+.NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-kippt] {
+ background: transparent url('/media/embed/reader/kippt.png') no-repeat 0 0;
+}
+.NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-evernote] {
+ background: transparent url('/media/embed/reader/evernote.png') no-repeat 0 0;
+}
.NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-googleplus] {
background: transparent url('/media/embed/reader/googleplus.png') no-repeat 0 0;
}
@@ -7173,7 +7274,6 @@ form.opml_import_form input {
margin: 12px 0;
}
.NB-static-api table {
- width: 600px;
border-spacing: 0 0;
margin: 24px 0 12px 20px;
background: transparent url('/media/embed/reader/static_bullet_white.png') no-repeat 0 0;
@@ -7192,7 +7292,6 @@ form.opml_import_form input {
white-space: nowrap;
/* border-bottom: 1px solid #F6F6f6;*/
}
-<<<<<<< HEAD
.NB-static-api table td:last-child {
border-right: none;
}
@@ -7443,7 +7542,6 @@ form.opml_import_form input {
margin-bottom: 54px;
}
}
-<<<<<<< HEAD
/* ================= */
/* = Friends Modal = */
View
412 media/css/social/social_page.css
@@ -1,3 +1,7 @@
+/* ========== */
+/* = Global = */
+/* ========== */
+
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 13px;
@@ -5,10 +9,31 @@ body {
margin: 0;
}
+.NB-hidden {
+ display: none;
+}
+
+.NB-left {
+ float: left;
+}
+.NB-right {
+ float: right;
+}
+.NB-raquo {
+ font-size: 18px;
+ vertical-align: baseline;
+ line-height: 12px;
+}
+
/* ========== */
/* = Layout = */
/* ========== */
+.NB-page {
+ background: #EBC55F url(/media/img/reader/background-control-light.png) repeat 0 0;
+ padding: 0 12px;
+}
+
/* ========== */
/* = Header = */
/* ========== */
@@ -16,7 +41,6 @@ body {
header {
padding: 64px 14px;
text-shadow: 1px 1px 0 #E0E0E0;
- background-color: #EBC55F;
overflow: hidden;
}
@@ -128,6 +152,19 @@ header {
.NB-title .NB-title-left {
overflow: hidden;
}
+
+/* ========= */
+/* = Story = */
+/* ========= */
+
+.NB-mark {
+ margin: 0 auto 36px;
+ max-width: 800px;
+ border-top-left-radius: 6px;
+ border-top-right-radius: 6px;
+ overflow: hidden;
+}
+
/* ===================== */
/* = Story Feed Header = */
/* ===================== */
@@ -201,7 +238,24 @@ header {
#E9EAEF 10%,
#F5F8FA 84%
);
+ border-top: 1px solid #C0C0C0;
+}
+
+.NB-story-header-wrapper {
border-bottom: 1px solid #D0D0D0;
+ border-right: 1px solid #909090;
+ border-left: 1px solid #909090;
+}
+
+@media all and (max-width: 800px) {
+ .NB-story-header {
+ padding-right: 100px;
+ }
+}
+@media all and (max-width: 600px) {
+ .NB-story-header {
+ padding-right: 12px;
+ }
}
.NB-story-title {
@@ -264,30 +318,370 @@ header {
font-size: 7px;
}
+.NB-story-header .NB-story-modifications-button {
+ width: 16px;
+ height: 16px;
+ background: transparent url('/media/embed/reader/code_icon.png') no-repeat 0 0;
+ float: left;
+ margin: 3px 12px 0px 0px;
+ opacity: .6;
+ cursor: pointer;
+}
+.NB-story-header .NB-story-modifications-button:hover {
+ opacity: 1;
+}
+
/* =========== */
/* = Stories = */
/* =========== */
.NB-divider {
- border-top: 1px solid #F6F6F6;
- border-bottom: 1px solid #E8E8E8;
+/* border-top: 1px solid #F6F6F6;*/
+/* border-bottom: 1px solid #E8E8E8;*/
height: 0;
}
.NB-story {
- margin: 0 28px;
- padding: 12px 212px 24px 0;
- max-width: 700px;
+ background-color: #FAFAFA;
+ padding: 0 28px;
+ max-width: 800px;
+ border-right: 1px solid #909090;
+ border-bottom: 1px solid #909090;
+ border-left: 1px solid #909090;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
}
-.NB-story a {
+.NB-story-content {
+ padding: 12px 200px 24px 0;
+}
+@media all and (max-width: 800px) {
+ .NB-story-content {
+ padding-right: 100px;
+ }
+}
+@media all and (max-width: 700px) {
+ .NB-story-content {
+ padding-right: 50px;
+ }
+}
+@media all and (max-width: 600px) {
+ .NB-story-content {
+ padding-right: 0px;
+ }
+}
+
+.NB-story-content a {
color: #1F4499;
}
-.NB-story a:hover {
+.NB-story-content a:hover {
color: #99481D;
}
-.NB-story img {
+.NB-story-content img {
max-width: 100%;
+ height: auto;
+}
+.NB-story-content ins {
+ text-decoration: none;
+ color: inherit;
+}
+.NB-story-content del {
+ display: none;
+}
+.NB-pref-show-changes .NB-story-content ins {
+ text-decoration: underline;
+ color: #27452D;
+}
+.NB-pref-show-changes .NB-story-content del {
+ display: block;
+ color: #661616;
+}
+
+/* ======================= */
+/* = Shares and Comments = */
+/* ======================= */
+
+.NB-story-comments a {
+ text-decoration: none;
+}
+.NB-story-comments a:hover {
+ color: #99481D;
+}
+.NB-story-comments a img {
+ border: none;
+}
+.NB-story-comments {
+ margin: 0 0 32px 0;
+ padding: 1px 0 0;
+ max-width: 800px;
+ border-top: 2px solid #353535;
+ border-bottom: 1px solid #353535;
+ font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
+}
+.NB-story-comment {
+ border-top: 1px solid #A6A6A6;
+ background-color: #FCFCFC;
+ position: relative;
+ padding: 0 12px 2px 54px;
+ line-height: 20px;
+ overflow: hidden;
+}
+.NB-story-comment .NB-user-avatar {
+ position: absolute;
+ left: 6px;
+ top: 6px;
+ cursor: pointer;
+}
+.NB-story-comment .NB-user-avatar.NB-story-comment-reshare {
+ top: 22px;
+ left: 6px;
+ z-index: 1;
+}
+.NB-story-comment .NB-user-avatar img {
+ border-radius: 6px;
+ margin: 2px 0 0 1px;
+ width: 38px;
+ height: 38px;
+}
+.NB-story-comment .NB-user-avatar.NB-story-comment-reshare img {
+ height: 24px;
+ width: 24px;
+}
+.NB-story-comment .NB-story-comment-author-container {
+ overflow: hidden;
+ margin: 6px 0 0;
+}
+.NB-story-comment .NB-story-comment-reshares {
+ position: absolute;
+ top: 0;
+ left: 8px;
+ z-index: 0;
+}
+.NB-story-comment .NB-story-comment-reshares .NB-user-avatar {
+ top: 8px;
+ left: 12px;
+}
+.NB-story-comment .NB-story-comment-reshares .NB-user-avatar img {
+ width: 22px;
+ height: 22px;
+ border-radius: 3px;
+}
+.NB-story-comment .NB-story-comment-username {
+ float: left;
+ font-size: 11px;
+ color: #1D4BA6;
+ font-weight: bold;
+ margin: 0 10px 0 0;
+ text-shadow: 0 -1px 0 #F0F0F0;
+ cursor: pointer;
+}
+.NB-story-comment .NB-story-comment-date {
+ text-transform: uppercase;
+ font-size: 10px;
+ color: #9D9D9D;
+ font-weight: bold;
+ float: left;
+}
+.NB-story-comment .NB-story-comment-content {
+ float: left;
+ color: #303030;
+}
+.NB-story-comment .NB-story-comment-reply-button {
+ padding: 4px 24px 4px 12px;
+ float: left;
+ cursor: pointer;
+}
+.NB-story-comment .NB-story-comment-reply-button .NB-story-comment-reply-button-wrapper {
+ text-transform: uppercase;
+ background-color: #E9AF86;
+ color: white;
+ padding: 1px 4px;
+ line-height: 9px;
+ font-size: 9px;
+}
+.NB-story-comment .NB-story-comment-reply-button:hover .NB-story-comment-reply-button-wrapper {
+ background-color: #DE772B;
+}
+.NB-story-comment .NB-story-comment-reply-button:active .NB-story-comment-reply-button-wrapper {
+ background-color: #9F3A00;
+}
+
+.NB-story-comment-reply {
+ border-top: 1px solid #E0E0E0;
+ padding: 4px 0;
+ overflow: hidden;
+ clear: both;
+ position: relative;
+ padding: 6px 0 6px 32px;
+ line-height: 18px;
+}
+.NB-story-comment-reply .NB-story-comment-reply-photo {
+ width: 24px;
+ height: 24px;
+ border-radius: 3px;
+ position: absolute;
+ left: 0px;
+ top: 10px;
+ cursor: pointer;
+}
+
+.NB-story-comment-edit-button {
+ padding: 4px 24px 4px 12px;
+ float: left;
+ cursor: pointer;
+}
+.NB-story-comment-edit-button .NB-story-comment-edit-button-wrapper {
+ text-transform: uppercase;
+ background-color: #74A2E7;
+ color: white;
+ padding: 1px 4px;
+ line-height: 9px;
+ font-size: 9px;
+}
+.NB-story-comment-edit-button:hover .NB-story-comment-edit-button-wrapper {
+ background-color: #5073BC;
+}
+.NB-story-comment-edit-button:active .NB-story-comment-edit-button-wrapper {
+ background-color: #2A3B72;
+}
+.NB-story-comment-share-edit-button {
+ padding-right: 0;
+}
+.NB-story-comment-reply-content {
+ clear: both;
+ color: #303030;
+ float: left;
+}
+
+.NB-story-comment-reply-form {
+ padding-top: 11px;
+}
+.NB-story-comment-reply-form .NB-story-comment-reply-username {
+ margin: 1px 8px 6px 0;
+}
+.NB-story-comment-reply-form .NB-story-comment-reply-comments {
+ margin: 0 8px 4px 0;
+ width: 62%;
+ display: block;
+ float: left;
+ font-size: 12px;
+}
+.NB-story-comment-reply-form .NB-modal-submit-button {
+ float: left;
+ font-size: 10px;
+ padding: 2px 8px;
+ line-height: 16px;
+ margin: 0;
+}
+.NB-story-comment-reply-form .NB-error {
+ font-size: 10px;
+ color: #6A1000;
+ padding: 4px 0 0;
+ line-height: 14px;
+ font-weight: bold;
+ clear: both;
+}
+.NB-story-comments-public-teaser-wrapper,
+.NB-story-comments-public-header-wrapper {
+ border-top: 1px solid #353535;
+ padding: 1px 0;
+ cursor: pointer;
+}
+.NB-story-comments-public-header-wrapper {
+ cursor: default;
+}
+.NB-story-comments-public-teaser,
+.NB-story-comments-public-header {
+ background-color: #B1B6B4;
+ color: white;
+ text-shadow: 0 1px 0 #505050;
+ font-weight: bold;
+ text-transform: uppercase;
+ font-size: 10px;
+ padding: 2px 12px;
+ overflow: hidden;
+ -webkit-transition: all .12s ease-out;
+ -moz-transition: all .12s ease-out;
+ -o-transition: all .12s ease-out;
+ -ms-transition: all .12s ease-out;
+}
+.NB-story-comments-public-header {
+ background-color: whiteSmoke;
+ color: #404040;
+ text-shadow: 0 1px 0 white;
+}
+.NB-story-comments-shares-teaser-wrapper {
+ border-top: 0;
+ padding-top: 0;
+}
+
+.NB-story-comments-shares-teaser {
+ background-color: whiteSmoke;
+ color: #202020;
+ cursor: default;
+ text-shadow: 0 1px 0 #FFF;
+ font-weight: bold;
+ text-transform: uppercase;
+ font-size: 10px;
+ padding: 8px 12px 0px;
+ overflow: hidden;
+ height: 27px;
+ -webkit-transition: all .12s ease-out;
+ -moz-transition: all .12s ease-out;
+ -o-transition: all .12s ease-out;
+ -ms-transition: all .12s ease-out;
+}
+
+.NB-story-comments-public-teaser-wrapper:hover .NB-story-comments-public-teaser {
+ background-color: #2B478C;
+ background-image: none;
+}
+.NB-story-comments-public-teaser b {
+ padding: 0 1px;
+ font-size: 12px;
+}
+.NB-story-share-label {
+ display: inline-block;
+ margin: 0 4px 0 0;
+}
+.NB-story-share-profiles {
+ display: inline-block;
+ vertical-align: top;