diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py
index 480479f95..c0d0c198b 100644
--- a/r2/r2/lib/pages/pages.py
+++ b/r2/r2/lib/pages/pages.py
@@ -45,6 +45,8 @@ from r2.models import (
LINK_FLAIR,
LabeledMulti,
Link,
+ ReadNextLink,
+ ReadNextListing,
Mod,
ModSR,
MultiReddit,
@@ -52,6 +54,7 @@ from r2.models import (
Printable,
PromoCampaign,
PromotionPrices,
+ QueryBuilder,
Random,
RandomNSFW,
RandomSubscription,
@@ -1728,6 +1731,27 @@ class LinkInfoPage(Reddit):
def rightbox(self):
rb = Reddit.rightbox(self)
+
+ if (c.site and not c.default_sr and c.render_style == 'html' and
+ feature.is_enabled('read_next')):
+ link = self.link
+
+ def wrapper_fn(thing):
+ w = Wrapped(thing)
+ w.render_class = ReadNextLink
+ return w
+
+ def keep_fn(thing):
+ return thing._fullname != link._fullname
+
+ query_obj = c.site.get_links('hot', 'all').query
+ builder = QueryBuilder(query_obj,
+ wrap=wrapper_fn, keep_fn=keep_fn,
+ skip=True, num=10)
+ listing = ReadNextListing(builder).listing()
+ if len(listing.things):
+ rb.append(ReadNext(c.site, listing.render()))
+
if not (self.link.promoted and not c.user_is_sponsor):
if c.user_is_admin:
from admin_pages import AdminLinkInfoBar
@@ -3446,6 +3470,13 @@ class Ads(Templated):
self.frame_id = "ad-frame"
+class ReadNext(Templated):
+ def __init__(self, sr, links):
+ Templated.__init__(self)
+ self.sr = sr
+ self.links = links
+
+
class Embed(Templated):
"""wrapper for embedding /help into reddit as if it were not on a separate wiki."""
def __init__(self,content = ''):
diff --git a/r2/r2/models/link.py b/r2/r2/models/link.py
index ed2035c54..fa4c2e3a2 100644
--- a/r2/r2/models/link.py
+++ b/r2/r2/models/link.py
@@ -932,6 +932,10 @@ class PromotedLink(Link):
Printable.add_props(user, wrapped)
+class ReadNextLink(Link):
+ _nodb = True
+
+
class Comment(Thing, Printable):
_data_int_props = Thing._data_int_props + ('reported', 'gildings')
_defaults = dict(reported=0,
diff --git a/r2/r2/models/listing.py b/r2/r2/models/listing.py
index ecd985a3e..0476ee883 100644
--- a/r2/r2/models/listing.py
+++ b/r2/r2/models/listing.py
@@ -301,6 +301,10 @@ class SearchListing(LinkListing):
return wrapped
+class ReadNextListing(Listing):
+ pass
+
+
class NestedListing(Listing):
def __init__(self, *a, **kw):
Listing.__init__(self, *a, **kw)
diff --git a/r2/r2/public/static/css/components/components.less b/r2/r2/public/static/css/components/components.less
index db2f61c74..5d837c5eb 100644
--- a/r2/r2/public/static/css/components/components.less
+++ b/r2/r2/public/static/css/components/components.less
@@ -11,3 +11,4 @@
@import "modal.less";
@import "close.less";
@import "toggles.less";
+@import "read-next.less";
diff --git a/r2/r2/public/static/css/components/read-next.less b/r2/r2/public/static/css/components/read-next.less
new file mode 100644
index 000000000..c8aa4f8e1
--- /dev/null
+++ b/r2/r2/public/static/css/components/read-next.less
@@ -0,0 +1,134 @@
+@read-next-color-background: @color-white;
+@read-next-color-header: #EFF7FF;
+@read-next-color-button-background: lighten(@color-ui-blue, 5%);
+@read-next-color-text: @color-semi-black;
+@read-next-color-text-light: @color-dark-grey;
+@read-next-height: 100px;
+@read-next-width: 300px;
+@read-next-link-height: 60px;
+@read-next-thumbnail-size: 50px;
+@zindex-read-next: @zindex-modal - 1;
+
+@read-next-font-base: @font-x-small;
+@read-next-font-small: @font-xx-small;
+@read-next-line-base: @line-x-small;
+@read-next-line-large: @line-small;
+
+.read-next-font-size(@font:@read-next-font-base, @line:@read-next-line-base) {
+ .md-font-size(@read-next-font-base, @font, @line);
+}
+
+.read-next-container {
+ font-size: @base-font-keyword;
+}
+
+.read-next {
+ .md-base-font-size(@read-next-font-base);
+ background: @read-next-color-background;
+ border: 1px solid darken(@read-next-color-background, 15%);
+ box-sizing: border-box;
+ color: @read-next-color-text;
+ height: @read-next-height;
+ position: relative;
+ -webkit-user-select: none;
+ width: @read-next-width;
+ z-index: @zindex-read-next;
+
+ &.fixed {
+ border-bottom-width: 0;
+ bottom: 0;
+ position: fixed;
+ }
+
+ .read-next-header {
+ background-color: @read-next-color-header;
+ border-bottom: 1px solid darken(@read-next-color-header, 5%);
+ color: @read-next-color-text-light;
+ padding-left: @margin-small * 1px;
+ padding-right: @margin-small * 1px;
+ padding-top: @margin-x-small * 1px;
+ }
+
+ .read-next-header-title {
+ .read-next-font-size(@read-next-font-base, @read-next-line-large);
+ }
+
+ .read-next-title {
+ .read-next-font-size();
+ display: block;
+ max-height: @read-next-line-base * 3px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .read-next-nav {
+ .read-next-font-size(@read-next-font-small);
+ position: absolute;
+ right: @margin-x-small * 1px;
+ top: @margin-x-small * 1px;
+ }
+
+ .read-next-dismiss,
+ .read-next-button {
+ .transform(scale(1, 1) translateY(0px));
+ .transition(all, 0.2s);
+ cursor: pointer;
+ display: inline-block;
+ height: @read-next-line-base * 1px;
+ margin-left: @margin-x-small * 1px;
+ position: relative;
+ text-align: center;
+ width: @read-next-line-base * 1px;
+
+ &:active {
+ .transform(scale(1.01, 1.01) translateY(1px));
+ }
+ }
+
+ .read-next-button {
+ background-color: @read-next-color-button-background;
+ border-radius: 50%;
+ color: @read-next-color-background;
+
+ &:active {
+ background-color: darken(@read-next-color-button-background, 5%);
+ }
+ }
+
+ .read-next-list {
+ padding: @margin-small * 1px;
+ padding-top: @margin-x-small * 1px;
+ }
+
+ .read-next-link {
+ display: none;
+ float: left;
+ height: @read-next-link-height;
+ overflow: hidden;
+ width: 100%;
+
+ &.active {
+ display: block;
+ }
+
+ .read-next-thumbnail {
+ display: block;
+ float: left;
+ height: @read-next-thumbnail-size;
+ margin-right: @margin-x-small * 1px;
+ // magic number, but it just looks better
+ margin-top: 3px;
+ width: @read-next-thumbnail-size;
+
+ img {
+ height: auto;
+ width: 100%;
+ }
+ }
+ }
+
+ .read-next-meta {
+ .read-next-font-size(@read-next-font-small);
+ color: @read-next-color-text-light;
+ }
+}
diff --git a/r2/r2/public/static/css/components/variables.less b/r2/r2/public/static/css/components/variables.less
index 68e2ce7b6..1b4614879 100644
--- a/r2/r2/public/static/css/components/variables.less
+++ b/r2/r2/public/static/css/components/variables.less
@@ -76,6 +76,7 @@
@base-font-keyword: small;
@base-font-keyword-size: 13; // small == 13px;
+@font-xx-small: 10;
@font-x-small: 12;
@font-small: 14;
@font-medium: 16;
diff --git a/r2/r2/public/static/css/reddit.less b/r2/r2/public/static/css/reddit.less
index 60133d09e..7fec03675 100644
--- a/r2/r2/public/static/css/reddit.less
+++ b/r2/r2/public/static/css/reddit.less
@@ -1,5 +1,5 @@
-@import "components/components.less";
@import "components/variables.less";
+@import "components/components.less";
@import "markdown.less";
.no-select {
diff --git a/r2/r2/public/static/js/ui.js b/r2/r2/public/static/js/ui.js
index 1fd1d0116..c9b213013 100644
--- a/r2/r2/public/static/js/ui.js
+++ b/r2/r2/public/static/js/ui.js
@@ -74,6 +74,8 @@ r.ui.init = function() {
r.ui.initNewCommentHighlighting()
+ r.ui.initReadNext();
+
r.ui.initTimings()
}
@@ -148,6 +150,95 @@ r.ui.highlightNewComments = function() {
});
}
+r.ui.initReadNext = function() {
+ var $readNextContainer = $('.read-next-container');
+
+ if ($readNextContainer.length) {
+ this.readNext = new r.ui.ReadNext({
+ el: $readNextContainer,
+ });
+ }
+};
+
+r.ui.ReadNext = Backbone.View.extend({
+ events: {
+ 'click .read-next-button.next': 'next',
+ 'click .read-next-button.prev': 'prev',
+ 'click .read-next-dismiss': 'dismiss',
+ },
+
+ initialize: function() {
+ this.$readNext = this.$el.find('.read-next');
+ this.$links = this.$readNext.find('.read-next-link');
+ this.numLinks = this.$links.length;
+
+ this.state = new Backbone.Model({
+ fixed: false,
+ index: -1,
+ });
+
+ this.updateScroll = this.updateScroll.bind(this);
+ window.addEventListener('scroll', this.updateScroll);
+ this.state.on('change', this.render.bind(this));
+
+ this.updateScroll();
+ this.state.set({
+ index: 0,
+ });
+ },
+
+ next: function() {
+ var currentIndex = this.state.get('index');
+ var numLinks = this.numLinks;
+ this.state.set({
+ index: currentIndex = (currentIndex + 1) % numLinks,
+ });
+ },
+
+ prev: function() {
+ var currentIndex = this.state.get('index');
+ var numLinks = this.numLinks;
+ this.state.set({
+ index: currentIndex = (currentIndex + numLinks - 1) % numLinks,
+ });
+ },
+
+ dismiss: function() {
+ this.$el.fadeOut();
+ window.removeEventListener('scroll', this.updateScroll);
+ },
+
+ updateScroll: function() {
+ var scrollPosition = window.scrollY;
+ var nodePosition = this.$el.position().top;
+
+ // stick to bottom
+ var scrollOffset = window.innerHeight;
+ var nodeOffset = this.$readNext.height();
+ scrollPosition += scrollOffset;
+ nodePosition += nodeOffset;
+
+ this.state.set({
+ fixed: scrollPosition >= nodePosition,
+ });
+ },
+
+ render: function() {
+ var currentIndex = this.state.get('index');
+ var fixedPosition = this.state.get('fixed');
+
+ this.$links.removeClass('active');
+ this.$links.eq(currentIndex).addClass('active');
+
+ if (fixedPosition) {
+ this.$readNext.addClass('fixed');
+ } else {
+ this.$readNext.removeClass('fixed');
+ }
+ },
+});
+
+
r.ui.initTimings = function() {
// return if we're not configured for sending stats
if (!r.config.pageInfo.actionName || !r.config.stats_domain) {
diff --git a/r2/r2/templates/link.html b/r2/r2/templates/link.html
index d8864aa23..fe0ecda74 100644
--- a/r2/r2/templates/link.html
+++ b/r2/r2/templates/link.html
@@ -32,7 +32,7 @@
%>
<%inherit file="printable.html"/>
-<%namespace file="utils.html" import="plain_link, thing_timestamp, edited, nsfw_stamp" />
+<%namespace file="utils.html" import="plain_link, thing_timestamp, edited, nsfw_stamp, thumbnail_img" />
<%namespace file="printablebuttons.html" import="toggle_button" />
<%def name="numcol()">
@@ -232,22 +232,7 @@ ${parent.thing_css_class(what)} ${"over18" if thing.over_18 else ""} ${thing.vis
<%def name="thumbnail()">
%if thing.thumbnail:
<%call expr="make_link('thumbnail', 'thumbnail ' + (thing.thumbnail if thing.thumbnail_sprited else ''))">
- % if not thing.thumbnail_sprited:
- <%
- if hasattr(thing, 'thumbnail_size'):
- scaling_factor = 1
- if thing.thumbnail_size[0] > g.thumbnail_size[0]:
- # hidpi scaling, calculate in case hidpi changes definition in the future and
- # we have multiple sets of image dimensions. Currently should always be 1 or 2.
- # Width is always the maximum allowed, so we don't need to check height.
- scaling_factor = thing.thumbnail_size[0] // g.thumbnail_size[0]
-
- size_str = "width='%d' height='%d'" % (thing.thumbnail_size[0] // scaling_factor, thing.thumbnail_size[1] // scaling_factor)
- else:
- size_str = ""
- %>
-
- % endif
+ ${thumbnail_img(thing)}
%call>
%endif
%def>
diff --git a/r2/r2/templates/readnext.html b/r2/r2/templates/readnext.html
new file mode 100644
index 000000000..e3e92afed
--- /dev/null
+++ b/r2/r2/templates/readnext.html
@@ -0,0 +1,42 @@
+## The contents of this file are subject to the Common Public Attribution
+## License Version 1.0. (the "License"); you may not use this file except in
+## compliance with the License. You may obtain a copy of the License at
+## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
+## License Version 1.1, but Sections 14 and 15 have been added to cover use of
+## software over a computer network and provide for limited attribution for the
+## Original Developer. In addition, Exhibit A has been modified to be
+## consistent with Exhibit B.
+##
+## Software distributed under the License is distributed on an "AS IS" basis,
+## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
+## the specific language governing rights and limitations under the License.
+##
+## The Original Code is reddit.
+##
+## The Original Developer is the Initial Developer. The Initial Developer of
+## the Original Code is reddit Inc.
+##
+## All portions of the code written by reddit are Copyright (c) 2006-2015
+## reddit Inc. All Rights Reserved.
+###############################################################################
+
+<%namespace file="utils.html" import="plain_link"/>
+
+