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)} %endif 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"/> + + diff --git a/r2/r2/templates/readnextlink.html b/r2/r2/templates/readnextlink.html new file mode 100644 index 000000000..3418ee8af --- /dev/null +++ b/r2/r2/templates/readnextlink.html @@ -0,0 +1,47 @@ +## 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="thumbnail_img" /> + + + + %if thing.thumbnail: + ${self.thumbnail()} + %endif + + + +<%def name="thumbnail()"> + %if thing.thumbnail and not thing.thumbnail_sprited: + + %endif + diff --git a/r2/r2/templates/readnextlisting.html b/r2/r2/templates/readnextlisting.html new file mode 100644 index 000000000..139fe0ea2 --- /dev/null +++ b/r2/r2/templates/readnextlisting.html @@ -0,0 +1,29 @@ +## 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. +############################################################################### + + diff --git a/r2/r2/templates/utils.html b/r2/r2/templates/utils.html index 468580643..2ca9dd24f 100644 --- a/r2/r2/templates/utils.html +++ b/r2/r2/templates/utils.html @@ -637,3 +637,22 @@ ${unsafe(txt)} ${_("NSFW")} + +<%def name="thumbnail_img(thing)"> + %if thing.thumbnail and 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 +