Merge branch 'master' into offline

# By Samuel Clay (29) and ojiikun (3)
# Via Samuel Clay (3) and ojiikun (1)
* master: (32 commits)
  New user queue every 5 minutes.
  Fixing unknown host error on DO servers.
  Using All Site Stories instead of Top Level on folder chooser.
  Adding new preference for auto-opening a default folder on load.
  Unread counter overlay widget for feeds and social feeds.
  Fixing memory leak when reloading feeds (after add/remove/move), found entirely through @mihaip's hard work. Thanks!
  Disallowing single subscriber feeds to be loaded by non-subscribers.
  Fixing #383. Need a way to retrieve starred stories by hash.
  Previous story button takes you up, no longer to previous story. Should've done this a while ago.
  Adding story intelligence score tip to API.
  Adding user_profile to /reader/feeds. Has is_premium field, as well as web preferences.
  Switching feed link to point ot the feed's permalink, not a feed filter on the blurblog.
  Preventing popular from sharing stories from the same feed id in the same batch.
  Upping delay on new user queue.
  Fixing jenny holzer quote at end of blurblogs.
  Adding better error logging/messaging for marking stories as starred.
  Adding better error logging/messaging for marking stories as starred.
  Fixing broken icon bug.
  Fixing missing user_id in comment's replies and likes.
  Posting popular shared stories to twitter.
  ...
This commit is contained in:
Samuel Clay 2013-09-04 14:10:43 -07:00
commit ceb63caa52
40 changed files with 342 additions and 131 deletions

View file

@ -270,9 +270,9 @@ class Profile(models.Model):
txn_type='subscr_payment')[0]
refund = paypal.refund_transaction(transaction.txn_id)
try:
refunded = int(float(refund['raw']['TOTALREFUNDEDAMOUNT'][0]))
refunded = int(float(refund.raw['TOTALREFUNDEDAMOUNT'][0]))
except KeyError:
refunded = int(transaction.amount)
refunded = int(transaction.payment_gross)
logging.user(self.user, "~FRRefunding paypal payment: $%s" % refunded)
self.cancel_premium()

View file

@ -280,6 +280,7 @@ def load_feeds(request):
'social_feeds': social_feeds,
'social_profile': social_profile,
'social_services': social_services,
'user_profile': user.profile,
'folders': json.decode(folders.folders),
'starred_count': starred_count,
'categories': categories
@ -666,6 +667,8 @@ def load_single_feed(request, feed_id):
if dupe_feed_id: data['dupe_feed_id'] = dupe_feed_id
if not usersub:
data.update(feed.canonical())
if not usersub and feed.num_subscribers <= 1:
data = dict(code=-1, message="You must be subscribed to this feed.")
# if page <= 1:
# import random
@ -732,6 +735,7 @@ def load_starred_stories(request):
limit = int(request.REQUEST.get('limit', 10))
page = int(request.REQUEST.get('page', 0))
query = request.REQUEST.get('query')
story_hashes = request.REQUEST.getlist('h')[:100]
now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone)
message = None
if page: offset = limit * (page - 1)
@ -744,6 +748,12 @@ def load_starred_stories(request):
else:
stories = []
message = "You must be a premium subscriber to search."
elif story_hashes:
mstories = MStarredStory.objects(
user_id=user.pk,
story_hash__in=story_hashes
).order_by('-starred_date')[offset:offset+limit]
stories = Feed.format_stories(mstories)
else:
mstories = MStarredStory.objects(
user_id=user.pk
@ -1615,13 +1625,14 @@ def login_as(request):
def iframe_buster(request):
logging.user(request, "~FB~SBiFrame bust!")
return HttpResponse(status=204)
@required_params('story_id', feed_id=int)
@ajax_login_required
@json.json_view
def mark_story_as_starred(request):
code = 1
feed_id = int(request.POST['feed_id'])
story_id = request.POST['story_id']
feed_id = int(request.REQUEST['feed_id'])
story_id = request.REQUEST['story_id']
message = ""
story, _ = MStory.find_story(story_feed_id=feed_id, story_id=story_id)
if not story:
@ -1651,6 +1662,7 @@ def mark_story_as_starred(request):
return {'code': code, 'message': message}
@required_params('story_id')
@ajax_login_required
@json.json_view
def mark_story_as_unstarred(request):

View file

@ -139,7 +139,10 @@ class IconImporter(object):
return
else:
# Load XOR bitmap
image = BmpImagePlugin.DibImageFile(image_file)
try:
image = BmpImagePlugin.DibImageFile(image_file)
except IOError:
return
if image.mode == 'RGBA':
# Windows XP 32-bit color depth icon without AND bitmap
pass

View file

@ -1432,12 +1432,12 @@ class MSharedStory(mongo.Document):
if not days:
days = 3
if not cutoff:
cutoff = 7
cutoff = 6
if not shared_feed_ids:
shared_feed_ids = []
# shared_stories_count = sum(json.decode(MStatistics.get('stories_shared')))
# cutoff = cutoff or max(math.floor(.025 * shared_stories_count), 3)
today = datetime.datetime.now() - datetime.timedelta(days=days)
if not shared_feed_ids:
shared_feed_ids = []
map_f = """
function() {
@ -1494,6 +1494,9 @@ class MSharedStory(mongo.Document):
if not story:
logging.user(popular_user, "~FRPopular stories, story not found: %s" % story_info)
continue
if story.story_feed_id in shared_feed_ids:
logging.user(popular_user, "~FRPopular stories, story feed just shared: %s" % story_info)
continue
if interactive:
feed = Feed.get_by_id(story.story_feed_id)
@ -1516,13 +1519,15 @@ class MSharedStory(mongo.Document):
}
shared_story, created = MSharedStory.objects.get_or_create(**story_values)
if created:
shared_story.post_to_service('twitter')
shared += 1
shared_feed_ids.append(story.story_feed_id)
publish_new_stories = True
logging.user(popular_user, "~FCSharing: ~SB~FM%s (%s shares, %s min)" % (
story.story_title[:50],
story_info['count'],
cutoff))
if publish_new_stories:
socialsubs = MSocialSubscription.objects.filter(subscription_user_id=popular_user.pk)
for socialsub in socialsubs:
@ -1745,9 +1750,11 @@ class MSharedStory(mongo.Document):
comment['source_user'] = profiles[comment['source_user_id']]
for r, reply in enumerate(comment['replies']):
if reply['user_id'] not in profiles: continue
comment['replies'][r]['user'] = profiles[reply['user_id']]
comment['liking_user_ids'] = list(comment['liking_users'])
for u, user_id in enumerate(comment['liking_users']):
if user_id not in profiles: continue
comment['liking_users'][u] = profiles[user_id]
return comment

View file

@ -67,9 +67,7 @@ class SharePopularStories(Task):
def run(self, **kwargs):
logging.debug(" ---> Sharing popular stories...")
shared = MSharedStory.share_popular_stories(interactive=False)
if not shared:
shared = MSharedStory.share_popular_stories(interactive=False, days=2)
MSharedStory.share_popular_stories(interactive=False)
class UpdateRecalcForSubscription(Task):

View file

@ -30,6 +30,7 @@
android:background="@drawable/selector_overlay_bg_left"
android:textSize="14sp"
android:padding="6dp"
android:layout_marginRight="1dp"
android:onClick="overlayLeft" />
<Button
@ -45,4 +46,22 @@
</LinearLayout>
<TextView
android:id="@+id/reading_overlay_count"
android:text=""
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="100dp"
android:layout_marginBottom="18dp"
android:background="@drawable/neutral_count_rect"
android:paddingLeft="3dp"
android:paddingRight="3dp"
android:shadowDy="1"
android:shadowRadius="1"
android:textColor="@color/white"
android:textSize="14sp"
android:textStyle="bold"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true" />
</RelativeLayout>

View file

@ -37,13 +37,6 @@ public class AllSharedStoriesReading extends Reading {
addStoryToMarkAsRead(readingAdapter.getStory(passedPosition));
}
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
addStoryToMarkAsRead(readingAdapter.getStory(position));
checkStoryCount(position);
}
@Override
public void triggerRefresh() {
triggerRefresh(1);

View file

@ -38,13 +38,6 @@ public class AllStoriesReading extends Reading {
addStoryToMarkAsRead(readingAdapter.getStory(passedPosition));
}
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
addStoryToMarkAsRead(readingAdapter.getStory(position));
checkStoryCount(position);
}
@Override
public void triggerRefresh() {
triggerRefresh(1);

View file

@ -12,6 +12,7 @@ import com.newsblur.domain.Classifier;
import com.newsblur.domain.Feed;
import com.newsblur.fragment.SyncUpdateFragment;
import com.newsblur.service.SyncService;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.StoryOrder;
@ -45,6 +46,8 @@ public class FeedReading extends Reading {
feed = Feed.fromCursor(feedCursor);
setTitle(feed.title);
this.unreadCount = FeedUtils.getFeedUnreadCount(this.feed, this.currentState);
readingAdapter = new FeedReadingAdapter(getSupportFragmentManager(), feed, stories, classifier);
setupPager();
@ -58,15 +61,6 @@ public class FeedReading extends Reading {
addStoryToMarkAsRead(readingAdapter.getStory(passedPosition));
}
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
if (readingAdapter.getStory(position) != null) {
addStoryToMarkAsRead(readingAdapter.getStory(position));
checkStoryCount(position);
}
}
@Override
public void updateAfterSync() {
requestedPage = false;
@ -112,5 +106,4 @@ public class FeedReading extends Reading {
@Override
public void closeAfterUpdate() { }
}

View file

@ -37,13 +37,6 @@ public class FolderReading extends Reading {
addStoryToMarkAsRead(readingAdapter.getStory(passedPosition));
}
@Override
public void onPageSelected(int position) {
addStoryToMarkAsRead(readingAdapter.getStory(position));
checkStoryCount(position);
super.onPageSelected(position);
}
@Override
public void triggerRefresh() {
triggerRefresh(1);

View file

@ -17,6 +17,7 @@ import android.view.View;
import android.widget.Button;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
@ -37,6 +38,7 @@ import com.newsblur.util.PrefsUtils;
import com.newsblur.util.ReadFilter;
import com.newsblur.util.StoryOrder;
import com.newsblur.util.UIUtils;
import com.newsblur.util.ViewUtils;
import com.newsblur.view.NonfocusScrollview.ScrollChangeListener;
public abstract class Reading extends NbFragmentActivity implements OnPageChangeListener, SyncUpdateFragment.SyncUpdateFragmentInterface, OnSeekBarChangeListener, ScrollChangeListener {
@ -57,15 +59,24 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
protected ViewPager pager;
protected Button overlayLeft, overlayRight;
protected TextView overlayCount;
protected FragmentManager fragmentManager;
protected ReadingAdapter readingAdapter;
protected ContentResolver contentResolver;
private APIManager apiManager;
protected SyncUpdateFragment syncFragment;
protected Cursor stories;
private Set<Story> storiesToMarkAsRead;
// subclasses may set this to a nonzero value to enable the unread count overlay
protected int unreadCount = 0;
// keep a local cache of stories we have viewed within this activity cycle. We need
// this to track unread counts since it would be too costly to query and update the DB
// on every page change.
private Set<Story> storiesAlreadySeen;
private float overlayRangeTopPx;
private float overlayRangeBotPx;
@ -78,10 +89,12 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
setContentView(R.layout.activity_reading);
this.overlayLeft = (Button) findViewById(R.id.reading_overlay_left);
this.overlayRight = (Button) findViewById(R.id.reading_overlay_right);
this.overlayCount = (TextView) findViewById(R.id.reading_overlay_count);
fragmentManager = getSupportFragmentManager();
storiesToMarkAsRead = new HashSet<Story>();
storiesAlreadySeen = new HashSet<Story>();
passedPosition = getIntent().getIntExtra(EXTRA_POSITION, 0);
currentState = getIntent().getIntExtra(ItemsList.EXTRA_STATE, 0);
@ -94,6 +107,11 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
this.overlayRangeTopPx = (float) UIUtils.convertDPsToPixels(this, OVERLAY_RANGE_TOP_DP);
this.overlayRangeBotPx = (float) UIUtils.convertDPsToPixels(this, OVERLAY_RANGE_BOT_DP);
// the unread count overlay defaults to neutral colour. set it to positive if we are in focus mode
if (this.currentState == AppConstants.STATE_BEST) {
ViewUtils.setViewBackground(this.overlayCount, R.drawable.positive_count_rect);
}
}
protected void setupPager() {
@ -182,8 +200,12 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
@Override
public void onPageSelected(int position) {
this.setOverlayAlpha(1.0f);
this.enableOverlays();
if (readingAdapter.getStory(position) != null) {
addStoryToMarkAsRead(readingAdapter.getStory(position));
checkStoryCount(position);
}
}
// interface ScrollChangeListener
@ -194,7 +216,11 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
int posFromBot = (scrollMax - vPos);
float newAlpha = 0.0f;
if (vPos < this.overlayRangeTopPx) {
if ((vPos < this.overlayRangeTopPx) && (posFromBot < this.overlayRangeBotPx)) {
// if we have a super-tiny scroll window such that we never leave either top or bottom,
// just leave us at full alpha.
newAlpha = 1.0f;
} else if (vPos < this.overlayRangeTopPx) {
float delta = this.overlayRangeTopPx - ((float) vPos);
newAlpha = delta / this.overlayRangeTopPx;
} else if (posFromBot < this.overlayRangeBotPx) {
@ -208,13 +234,26 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
private void setOverlayAlpha(float a) {
UIUtils.setViewAlpha(this.overlayLeft, a);
UIUtils.setViewAlpha(this.overlayRight, a);
if (this.unreadCount > 0) {
UIUtils.setViewAlpha(this.overlayCount, a);
} else {
UIUtils.setViewAlpha(this.overlayCount, 0.0f);
}
}
/**
* Check and correct the display status of the overlays. Call this any time
* an event happens that might change out list position.
*/
private void enableOverlays() {
int page = this.pager.getCurrentItem();
this.overlayLeft.setEnabled(page > 0);
this.overlayRight.setEnabled(page < (this.readingAdapter.getCount()-1));
this.overlayRight.setText((page < (this.readingAdapter.getCount()-1)) ? R.string.overlay_next : R.string.overlay_done);
this.overlayCount.setText(Integer.toString(this.unreadCount));
this.setOverlayAlpha(1.0f);
}
@Override
@ -264,6 +303,11 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
if (this.storiesToMarkAsRead.size() >= AppConstants.MAX_MARK_READ_BATCH) {
flushStoriesMarkedRead();
}
if (this.storiesAlreadySeen.add(story)) {
// only decrement the cached story count if the story wasn't already read
this.unreadCount--;
}
this.enableOverlays();
}
private void flushStoriesMarkedRead() {
@ -284,6 +328,10 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
// operation, or it was read long before now.
FeedUtils.markStoryUnread(story, Reading.this, this.apiManager);
this.unreadCount++;
this.storiesAlreadySeen.remove(story);
this.enableOverlays();
}
@Override

View file

@ -29,12 +29,6 @@ public class SavedStoriesReading extends Reading {
setupPager();
}
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
checkStoryCount(position);
}
@Override
public void triggerRefresh() {
triggerRefresh(1);

View file

@ -14,6 +14,7 @@ import com.newsblur.database.MixedFeedsReadingAdapter;
import com.newsblur.domain.SocialFeed;
import com.newsblur.domain.Story;
import com.newsblur.service.SyncService;
import com.newsblur.util.FeedUtils;
public class SocialFeedReading extends Reading {
@ -40,6 +41,8 @@ public class SocialFeedReading extends Reading {
stories = contentResolver.query(storiesURI, null, DatabaseConstants.getStorySelectionFromState(currentState), null, null);
setTitle(getIntent().getStringExtra(EXTRA_USERNAME));
this.unreadCount = FeedUtils.getFeedUnreadCount(this.socialFeed, this.currentState);
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver(), stories);
setupPager();
@ -47,13 +50,6 @@ public class SocialFeedReading extends Reading {
addStoryToMarkAsRead(readingAdapter.getStory(passedPosition));
}
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
addStoryToMarkAsRead(readingAdapter.getStory(position));
checkStoryCount(position);
}
@Override
public void triggerRefresh() {
triggerRefresh(0);

View file

@ -80,27 +80,32 @@ public class FeedItemListFragment extends StoryItemListFragment implements Loade
Uri feedUri = FeedProvider.FEEDS_URI.buildUpon().appendPath(feedId).build();
Cursor feedCursor = contentResolver.query(feedUri, null, null, null, null);
if (feedCursor.getCount() > 0) {
feedCursor.moveToFirst();
Feed feed = Feed.fromCursor(feedCursor);
String[] groupFrom = new String[] { DatabaseConstants.STORY_TITLE, DatabaseConstants.STORY_AUTHORS, DatabaseConstants.STORY_READ, DatabaseConstants.STORY_SHORTDATE, DatabaseConstants.STORY_INTELLIGENCE_AUTHORS };
int[] groupTo = new int[] { R.id.row_item_title, R.id.row_item_author, R.id.row_item_title, R.id.row_item_date, R.id.row_item_sidebar };
// create the adapter before starting the loader, since the callback updates the adapter
adapter = new FeedItemsAdapter(getActivity(), feed, R.layout.row_item, storiesCursor, groupFrom, groupTo, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
getLoaderManager().initLoader(ITEMLIST_LOADER , null, this);
itemList.setOnScrollListener(this);
adapter.setViewBinder(new FeedItemViewBinder(getActivity()));
itemList.setAdapter(adapter);
itemList.setOnItemClickListener(this);
itemList.setOnCreateContextMenuListener(this);
} else {
Log.w(this.getClass().getName(), "Feed not found in DB, can't load.");
if (feedCursor.getCount() < 1) {
// This shouldn't happen, but crash reports indicate that it does (very rarely).
// If we are told to create an item list for a feed, but then can't find that feed ID in the DB,
// something is very wrong, and we won't be able to recover, so just force the user back to the
// feed list until we have a better understanding of how to prevent this.
Log.w(this.getClass().getName(), "Feed not found in DB, can't create item list.");
getActivity().finish();
}
feedCursor.moveToFirst();
Feed feed = Feed.fromCursor(feedCursor);
String[] groupFrom = new String[] { DatabaseConstants.STORY_TITLE, DatabaseConstants.STORY_AUTHORS, DatabaseConstants.STORY_READ, DatabaseConstants.STORY_SHORTDATE, DatabaseConstants.STORY_INTELLIGENCE_AUTHORS };
int[] groupTo = new int[] { R.id.row_item_title, R.id.row_item_author, R.id.row_item_title, R.id.row_item_date, R.id.row_item_sidebar };
// create the adapter before starting the loader, since the callback updates the adapter
adapter = new FeedItemsAdapter(getActivity(), feed, R.layout.row_item, storiesCursor, groupFrom, groupTo, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
getLoaderManager().initLoader(ITEMLIST_LOADER , null, this);
itemList.setOnScrollListener(this);
adapter.setViewBinder(new FeedItemViewBinder(getActivity()));
itemList.setAdapter(adapter);
itemList.setOnItemClickListener(this);
itemList.setOnCreateContextMenuListener(this);
return v;
}

View file

@ -241,9 +241,9 @@ public class FolderListFragment extends Fragment implements OnGroupClickListener
resolver.update(FeedProvider.FEEDS_URI.buildUpon().appendPath(feedId).build(), values, null, null);
}
folderAdapter.notifyDataSetChanged();
Toast.makeText(getActivity(), R.string.toast_marked_all_stories_as_read, Toast.LENGTH_SHORT).show();
UIUtils.safeToast(getActivity(), R.string.toast_marked_all_stories_as_read, Toast.LENGTH_SHORT);
} else {
Toast.makeText(getActivity(), R.string.toast_error_marking_feed_as_read, Toast.LENGTH_SHORT).show();
UIUtils.safeToast(getActivity(), R.string.toast_error_marking_feed_as_read, Toast.LENGTH_SHORT);
}
};
}.execute();

View file

@ -26,6 +26,7 @@ import com.newsblur.domain.Story;
import com.newsblur.domain.UserDetails;
import com.newsblur.network.APIManager;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.UIUtils;
public class ShareDialogFragment extends DialogFragment {
@ -132,10 +133,10 @@ public class ShareDialogFragment extends DialogFragment {
protected void onPostExecute(Boolean result) {
if (result) {
hasShared = true;
Toast.makeText(getActivity(), R.string.shared, Toast.LENGTH_LONG).show();
UIUtils.safeToast(getActivity(), R.string.shared, Toast.LENGTH_LONG);
callback.sharedCallback(shareComment, hasBeenShared);
} else {
Toast.makeText(getActivity(), R.string.error_sharing, Toast.LENGTH_LONG).show();
UIUtils.safeToast(getActivity(), R.string.error_sharing, Toast.LENGTH_LONG);
}
v.setEnabled(true);
ShareDialogFragment.this.dismiss();

View file

@ -5,7 +5,7 @@ public class AppConstants {
// Enables high-volume logging that may be useful for debugging. This should
// never be enabled for releases, as it not only slows down the app considerably,
// it will log sensitive info such as passwords!
public static final boolean VERBOSE_LOG = false;
public static final boolean VERBOSE_LOG = true;
public static final int STATE_ALL = 0;
public static final int STATE_SOME = 1;
@ -27,7 +27,8 @@ public class AppConstants {
public static final String LAST_APP_VERSION = "LAST_APP_VERSION";
// the max number of mark-as-read ops to batch up before flushing to the server
public static final int MAX_MARK_READ_BATCH = 5;
// set to 1 to effectively disable batching
public static final int MAX_MARK_READ_BATCH = 1;
// a pref for the time we completed the last full sync of the feed/fodler list
public static final String LAST_SYNC_TIME = "LAST_SYNC_TIME";

View file

@ -23,11 +23,14 @@ import com.newsblur.R;
import com.newsblur.database.DatabaseConstants;
import com.newsblur.database.FeedProvider;
import com.newsblur.domain.Classifier;
import com.newsblur.domain.Feed;
import com.newsblur.domain.SocialFeed;
import com.newsblur.domain.Story;
import com.newsblur.domain.ValueMultimap;
import com.newsblur.network.APIManager;
import com.newsblur.network.domain.NewsBlurResponse;
import com.newsblur.service.SyncService;
import com.newsblur.util.AppConstants;
public class FeedUtils {
@ -202,4 +205,34 @@ public class FeedUtils {
}
}
/**
* Gets the unread story count for a feed, filtered by view state.
*/
public static int getFeedUnreadCount(Feed feed, int currentState) {
if (feed == null ) return 0;
int count = 0;
count += feed.positiveCount;
if ((currentState == AppConstants.STATE_ALL) || (currentState == AppConstants.STATE_SOME)) {
count += feed.neutralCount;
}
if (currentState == AppConstants.STATE_ALL ) {
count += feed.negativeCount;
}
return count;
}
public static int getFeedUnreadCount(SocialFeed feed, int currentState) {
if (feed == null ) return 0;
int count = 0;
count += feed.positiveCount;
if ((currentState == AppConstants.STATE_ALL) || (currentState == AppConstants.STATE_SOME)) {
count += feed.neutralCount;
}
if (currentState == AppConstants.STATE_ALL ) {
count += feed.negativeCount;
}
return count;
}
}

View file

@ -45,7 +45,13 @@ public class ImageLoader {
Bitmap bitmap = memoryCache.get(url);
if (bitmap == null) {
File f = fileCache.getFile(url);
bitmap = BitmapFactory.decodeFile(f.getAbsolutePath());
try {
bitmap = BitmapFactory.decodeFile(f.getAbsolutePath());
} catch (Exception e) {
Log.e(this.getClass().getName(), "error decoding image, using default.", e);
// this can rarely happen if the device is low on memory and is not recoverable.
// just leave bitmap null and the default placeholder image will be used
}
}
if (bitmap != null) {
if (doRound) {

View file

@ -11,6 +11,7 @@ import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.os.Build;
import android.view.View;
import android.widget.Toast;
public class UIUtils {
@ -88,4 +89,16 @@ public class UIUtils {
v.setAlpha(alpha);
}
}
/**
* Shows a toast in a circumstance where the context might be null. This can very
* rarely happen when toasts are done from async tasks and the context is finished
* before the task completes, resulting in a crash. This prevents the crash at the
* cost of the toast not being shown.
*/
public static void safeToast(Context c, int rid, int duration) {
if (c != null) {
Toast.makeText(c, rid, duration).show();
}
}
}

View file

@ -84,27 +84,19 @@ public class ViewUtils {
TextView tagText = (TextView) v.findViewById(R.id.tag_text);
// due to a framework bug, the below modification of background resource also resets the declared
// padding on the view. save a copy of said padding so it can be re-applied after the change.
int oldPadL = v.getPaddingLeft();
int oldPadT = v.getPaddingTop();
int oldPadR = v.getPaddingRight();
int oldPadB = v.getPaddingBottom();
tagText.setText(tag);
if (classifier != null && classifier.tags.containsKey(tag)) {
switch (classifier.tags.get(tag)) {
case Classifier.LIKE:
tagText.setBackgroundResource(R.drawable.tag_background_positive);
setViewBackground(tagText, R.drawable.tag_background_positive);
tagText.setTextColor(tag_green_text);
break;
case Classifier.DISLIKE:
tagText.setBackgroundResource(R.drawable.tag_background_negative);
setViewBackground(tagText, R.drawable.tag_background_negative);
tagText.setTextColor(tag_red_text);
break;
}
v.setPadding(oldPadL, oldPadT, oldPadR, oldPadB);
}
v.setOnClickListener(new OnClickListener() {
@ -118,4 +110,21 @@ public class ViewUtils {
return v;
}
/**
* Sets the background resource of a view, working around a platform bug that causes the declared
* padding to get reset.
*/
public static void setViewBackground(View v, int resId) {
// due to a framework bug, the below modification of background resource also resets the declared
// padding on the view. save a copy of said padding so it can be re-applied after the change.
int oldPadL = v.getPaddingLeft();
int oldPadT = v.getPaddingTop();
int oldPadR = v.getPaddingRight();
int oldPadB = v.getPaddingBottom();
v.setBackgroundResource(resId);
v.setPadding(oldPadL, oldPadT, oldPadR, oldPadB);
}
}

8
fabfile.py vendored
View file

@ -14,6 +14,7 @@ import os
import time
import sys
import re
try:
import dop.client
except ImportError:
@ -32,9 +33,9 @@ except ImportError:
# = DEFAULTS =
# ============
env.NEWSBLUR_PATH = "~/projects/newsblur"
env.SECRETS_PATH = "~/projects/secrets-newsblur"
env.VENDOR_PATH = "~/projects/code"
env.NEWSBLUR_PATH = "/srv/newsblur"
env.SECRETS_PATH = "/srv/secrets-newsblur"
env.VENDOR_PATH = "/srv/code"
env.user = 'sclay'
env.key_filename = os.path.join(env.SECRETS_PATH, 'keys/newsblur.key')
@ -439,6 +440,7 @@ def setup_supervisor():
@parallel
def setup_hosts():
put(os.path.join(env.SECRETS_PATH, 'configs/hosts'), '/etc/hosts', use_sudo=True)
sudo('echo "\n\n127.0.0.1 `hostname`" >> /etc/hosts')
def config_pgbouncer():
put('config/pgbouncer.conf', '/etc/pgbouncer/pgbouncer.ini', use_sudo=True)

View file

@ -1244,6 +1244,8 @@ blockquote {
.NB-page-controls-end .NB-page-controls-text {
height: auto;
position: static;
color: rgba(255, 255, 255, 0.6);
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
}
.NB-page-controls-end:hover {
background-color: #3B3E49;

View file

@ -1168,10 +1168,12 @@ blockquote {
cursor: default;
height: auto;
margin-bottom: 0;
.NB-page-controls-text {
height: auto;
position: static;
color: rgba(255, 255, 255, .6);
text-shadow: 0 1px 0 rgba(0, 0, 0, .1);
}
&:hover {
background-color: #3B3E49;

View file

@ -1,7 +1,6 @@
NEWSBLUR.Router = Backbone.Router.extend({
routes: {
"": "index",
"add/?": "add_site",
"try/?": "try_site",
"site/:site_id/:slug": "site",
@ -15,11 +14,6 @@ NEWSBLUR.Router = Backbone.Router.extend({
"user/*user": "user"
},
index: function() {
// NEWSBLUR.log(["index"]);
NEWSBLUR.reader.show_splash_page();
},
add_site: function() {
NEWSBLUR.log(["add", window.location, $.getQueryString('url')]);
NEWSBLUR.reader.open_add_feed_modal({url: $.getQueryString('url')});

View file

@ -129,10 +129,20 @@ NEWSBLUR.Collections.Folders = Backbone.Collection.extend({
this.comparator = NEWSBLUR.Collections.Folders.comparator;
this.bind('change:feed_selected', this.propagate_feed_selected);
this.bind('change:counts', this.propagate_change_counts);
this.bind('reset', this.reset_folder_views);
},
model: NEWSBLUR.Models.FeedOrFolder,
reset_folder_views: function() {
this.each(function(item) {
if (item.is_feed()) {
item.feed.views = [];
item.feed.folders = [];
}
});
},
folders: function() {
return this.select(function(item) {
return item.is_folder();

View file

@ -2837,7 +2837,7 @@
$.make('div', { className: 'NB-menu-manage-confirm-position'}, [
$.make('div', { className: 'NB-menu-manage-move-save NB-menu-manage-feed-move-save NB-modal-submit-green NB-modal-submit-button' }, 'Save'),
$.make('div', { className: 'NB-menu-manage-image' }),
$.make('div', { className: 'NB-add-folders' }, NEWSBLUR.utils.make_folders(this.model))
$.make('div', { className: 'NB-add-folders' }, NEWSBLUR.utils.make_folders())
])
]),
$.make('li', { className: 'NB-menu-item NB-menu-manage-rename NB-menu-manage-feed-rename' }, [
@ -2939,7 +2939,7 @@
$.make('div', { className: 'NB-menu-manage-confirm-position'}, [
$.make('div', { className: 'NB-menu-manage-move-save NB-menu-manage-folder-move-save NB-modal-submit-green NB-modal-submit-button' }, 'Save'),
$.make('div', { className: 'NB-menu-manage-image' }),
$.make('div', { className: 'NB-add-folders' }, NEWSBLUR.utils.make_folders(this.model))
$.make('div', { className: 'NB-add-folders' }, NEWSBLUR.utils.make_folders())
])
]),
$.make('li', { className: 'NB-menu-item NB-menu-manage-rename NB-menu-manage-folder-rename' }, [
@ -5229,7 +5229,7 @@
});
$.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-story-previous' }, function($t, $p){
e.preventDefault();
self.show_previous_story();
self.show_next_story(-1);
});
$.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-layout-full' }, function($t, $p){
e.preventDefault();

View file

@ -62,7 +62,7 @@ NEWSBLUR.ReaderAddFeed = NEWSBLUR.ReaderPopover.extend({
$.make('input', { type: 'text', id: 'NB-add-url', className: 'NB-input NB-add-url', name: 'url', value: self.options.url })
]),
$.make('div', { className: 'NB-group NB-add-site' }, [
NEWSBLUR.utils.make_folders(this.model, this.options.folder_title),
NEWSBLUR.utils.make_folders(this.options.folder_title),
$.make('div', { className: 'NB-add-folder-icon' }),
$.make('div', { className: 'NB-modal-submit-button NB-modal-submit-green NB-add-url-submit' }, 'Add site'),
$.make('div', { className: 'NB-loading' })
@ -293,7 +293,7 @@ NEWSBLUR.ReaderAddFeed = NEWSBLUR.ReaderPopover.extend({
if (data.code > 0) {
$submit.text('Added!');
NEWSBLUR.assets.load_feeds(_.bind(function() {
var $folders = NEWSBLUR.utils.make_folders(this.model, $folder.val());
var $folders = NEWSBLUR.utils.make_folders($folder.val());
this.$(".NB-folders").replaceWith($folders);
this.open_add_folder();
$submit.text('Add Folder');

View file

@ -182,6 +182,25 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
'Window title'
])
]),
$.make('div', { className: 'NB-preference NB-preference-autoopenfolder' }, [
$.make('div', { className: 'NB-preference-options' }, [
$.make('div', [
$.make('input', { id: 'NB-preference-autoopenfolder-1', type: 'radio', name: 'autoopen_folder', value: 0 }),
$.make('label', { 'for': 'NB-preference-autoopenfolder-1' }, [
'Show the dashboard when loading NewsBlur'
])
]),
$.make('div', [
$.make('input', { id: 'NB-preference-autoopenfolder-2', type: 'radio', name: 'autoopen_folder', value: 1 }),
$.make('label', { 'for': 'NB-preference-autoopenfolder-2' }, [
this.make_autoopen_folders()
])
])
]),
$.make('div', { className: 'NB-preference-label'}, [
'Default folder'
])
]),
$.make('div', { className: 'NB-preference NB-preference-animations' }, [
$.make('div', { className: 'NB-preference-options' }, [
$.make('div', [
@ -739,6 +758,15 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
]);
},
make_autoopen_folders: function() {
var autoopen_folder = NEWSBLUR.Preferences.autoopen_folder;
var $folders = NEWSBLUR.utils.make_folders(autoopen_folder, {
name: 'default_folder',
toplevel: "All Site Stories"
});
return $folders;
},
resize_modal: function() {
var $scroll = $('.NB-tab.NB-active', this.$modal);
var $modal = this.$modal;
@ -763,6 +791,12 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
});
}
$('select[name=default_folder] option', $modal).each(function() {
if ($(this).val() == NEWSBLUR.Preferences.default_folder) {
$(this).attr('selected', true);
return false;
}
});
$('input[name=default_view]', $modal).each(function() {
if ($(this).val() == NEWSBLUR.Preferences.default_view) {
$(this).attr('checked', true);
@ -793,6 +827,12 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
return false;
}
});
$('input[name=autoopen_folder]', $modal).each(function() {
if ($(this).val() == NEWSBLUR.Preferences.autoopen_folder) {
$(this).attr('checked', true);
return false;
}
});
$('input[name=title_counts]', $modal).each(function() {
if (NEWSBLUR.Preferences.title_counts) {
$(this).attr('checked', true);

View file

@ -146,11 +146,12 @@ NEWSBLUR.utils = {
return this.dayNames[dayOfWeek] + ", " + this.monthNames[month] + " " + day + ", " + year;
},
make_folders: function(model, selected_folder_title) {
var folders = model.get_folders();
var $options = $.make('select', { className: 'NB-folders'});
make_folders: function(selected_folder_title, options) {
options = options || {};
var folders = NEWSBLUR.assets.get_folders();
var $options = $.make('select', { className: 'NB-folders', name: options.name });
var $option = $.make('option', { value: '' }, "Top Level");
var $option = $.make('option', { value: '' }, options.toplevel || "Top Level");
$options.append($option);
$options = this.make_folder_options($options, folders, '&nbsp;&nbsp;&nbsp;', selected_folder_title);

View file

@ -160,7 +160,10 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({
if (!NEWSBLUR.router) {
NEWSBLUR.router = new NEWSBLUR.Router;
var route_found = Backbone.history.start({pushState: true});
this.load_url_next_param(route_found);
var next = this.load_url_next_param(route_found);
if (!next && !route_found && NEWSBLUR.assets.preference("autoopen_folder")) {
this.load_default_folder();
}
}
},
@ -189,6 +192,21 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({
// In case this needs to be found again: window.location.href = BACKBONE
window.history.replaceState({}, null, '/');
}
return next;
},
load_default_folder: function() {
var default_folder = NEWSBLUR.assets.preference('default_folder');
if (!default_folder || default_folder == "") {
NEWSBLUR.reader.open_river_stories();
} else {
var folder = NEWSBLUR.assets.get_folder(default_folder);
if (folder) {
NEWSBLUR.reader.open_river_stories(folder.folder_view.$el, folder);
}
}
},
update_dashboard_count: function() {

View file

@ -43,10 +43,12 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({
},
destroy: function() {
console.log(["destroy", this]);
if (this.model) {
this.model.unbind(null, this);
}
this.$el.remove();
delete this.views;
},
render: function() {

View file

@ -50,7 +50,6 @@ HELLO_EMAIL = 'hello@newsblur.com'
NEWSBLUR_URL = 'http://www.newsblur.com'
SECRET_KEY = 'YOUR_SECRET_KEY'
# ===================
# = Global Settings =
# ===================
@ -71,7 +70,6 @@ MEDIA_URL = '/media/'
# trailing slash.
# Examples: "http://foo.com/media/", "/media/".
ADMIN_MEDIA_PREFIX = '/media/admin/'
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
CIPHER_USERNAMES = False
DEBUG_ASSETS = DEBUG
HOMEPAGE_USERNAME = 'popular'
@ -207,6 +205,11 @@ SESSION_COOKIE_AGE = 60*60*24*365*2 # 2 years
SESSION_COOKIE_DOMAIN = '.newsblur.com'
SENTRY_DSN = 'https://XXXNEWSBLURXXX@app.getsentry.com/99999999'
if not DEVELOPMENT:
EMAIL_BACKEND = 'django_mailgun.MailgunBackend'
else:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# ==============
# = Subdomains =
# ==============
@ -366,7 +369,7 @@ CELERYBEAT_SCHEDULE = {
},
'share-popular-stories': {
'task': 'share-popular-stories',
'schedule': datetime.timedelta(hours=1),
'schedule': datetime.timedelta(minutes=10),
'options': {'queue': 'beat_tasks'},
},
'clean-analytics': {
@ -381,7 +384,7 @@ CELERYBEAT_SCHEDULE = {
},
'activate-next-new-user': {
'task': 'activate-next-new-user',
'schedule': datetime.timedelta(minutes=3.5),
'schedule': datetime.timedelta(minutes=5),
'options': {'queue': 'beat_tasks'},
},
}
@ -489,9 +492,10 @@ if not DEVELOPMENT:
INSTALLED_APPS += (
'gunicorn',
'raven.contrib.django',
'django_ses',
'django_ses',
)
RAVEN_CLIENT = raven.Client(SENTRY_DSN)
COMPRESS = not DEBUG
TEMPLATE_DEBUG = DEBUG
@ -510,9 +514,6 @@ DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': custom_show_toolbar,
'HIDE_DJANGO_SQL': False,
}
if not DEVELOPMENT:
RAVEN_CLIENT = raven.Client(SENTRY_DSN)
EMAIL_BACKEND = 'django_ses.SESBackend'
if DEBUG:
TEMPLATE_LOADERS = (

View file

@ -71,6 +71,7 @@
'timezone' : "{{ user_profile.timezone }}",
'title_counts' : true,
'truncate_story' : 'social',
'autoopen_folder' : false,
'story_share_twitter' : true,
'story_share_facebook' : true,
'story_share_readitlater' : false,

View file

@ -42,7 +42,7 @@
<img src="{{ story.feed.favicon_url }}" />
</div>
<div class="NB-feed-title">
<a href="/site/{{ story.feed.id }}/">{{ story.feed.feed_title }}</a>
<a href="{{ story.feed.feed_link }}">{{ story.feed.feed_title }}</a>
</div>
{% endif %}
</div>

View file

@ -195,6 +195,10 @@
optional: true
default: "true"
example: "false"
tips:
- >
The story score (red, neutral, green) can be computed by following
the function <a href="https://github.com/samuelclay/NewsBlur/blob/master/media/js/newsblur/reader/reader_utils.js"> `compute_story_score` in reader_utils.js</a>.
- url: /reader/starred_stories
method: GET
@ -207,6 +211,10 @@
optional: true
default: 1
example: 2
- key: h
desc: "Pass up to 100 story_hashes. Use with starred_story_hashes."
optional: true
example: "h=a1b2c3&h=d4e5f6"
- url: /reader/starred_story_hashes
method: GET
@ -218,6 +226,8 @@
desc: "Including timestamps for starred_date"
optional: true
default: false
tips:
- "Use with /reader/starred_stories and pass up to 100 story_hashes as the `h` param."
- url: /reader/river_stories
method: GET

View file

@ -14,7 +14,7 @@ class NBMuninGraph(MuninGraph):
# 'feed_errors.label': 'Feed Errors',
'feed_success.label': 'Feed Success',
# 'page_errors.label': 'Page Errors',
'page_success.label': 'Page Success',
# 'page_success.label': 'Page Success',
}
def calculate_metrics(self):
@ -23,7 +23,6 @@ class NBMuninGraph(MuninGraph):
return {
'feed_success': statistics['feeds_fetched'],
'page_success': statistics['pages_fetched'],
}
if __name__ == '__main__':

View file

@ -20,4 +20,4 @@ WHITE='\033[01;37m'
ipaddr=`python /srv/newsblur/utils/hostname_ssh.py $1`
printf "\n ${BLUE}---> ${LBLUE}Connecting to ${LGREEN}$1${BLUE} / ${LRED}$ipaddr${BLUE} <--- ${RESTORE}\n\n"
ssh -i ~/projects/secrets-newsblur/keys/newsblur.key $ipaddr
ssh -l sclay -i /srv/secrets-newsblur/keys/newsblur.key $ipaddr

View file

@ -11,6 +11,7 @@ sys.path.insert(0, '/srv/newsblur')
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
import fabfile
NEWSBLUR_USERNAME = 'sclay'
IGNORE_HOSTS = [
'push',
]
@ -58,10 +59,15 @@ def create_streams_for_roles(role, role2, command=None, path=None):
if any(h in hostname for h in IGNORE_HOSTS): continue
if hostname in found: continue
if 'ec2' in hostname:
s = subprocess.Popen(["ssh", "-i", os.path.expanduser("~/.ec2/sclay.pem"),
s = subprocess.Popen(["ssh",
"-i", os.path.expanduser(os.path.join(fabfile.env.SECRETS_PATH,
"keys/ec2.pem")),
address, "%s %s" % (command, path)], stdout=subprocess.PIPE)
else:
s = subprocess.Popen(["ssh", address, "%s %s" % (command, path)], stdout=subprocess.PIPE)
s = subprocess.Popen(["ssh", "-l", NEWSBLUR_USERNAME,
"-i", os.path.expanduser(os.path.join(fabfile.env.SECRETS_PATH,
"keys/newsblur.key")),
address, "%s %s" % (command, path)], stdout=subprocess.PIPE)
s.name = hostname
streams.append(s)
found.add(hostname)

6
utils/tlnbw.py Executable file
View file

@ -0,0 +1,6 @@
#!/usr/bin/env python
import tlnb
if __name__ == "__main__":
tlnb.main(role="work", role2="work")