diff --git a/r2/example.ini b/r2/example.ini index ad941d275..d798bf950 100644 --- a/r2/example.ini +++ b/r2/example.ini @@ -498,3 +498,5 @@ beaker.session_secret = somesecret frontpage_dart = false # spotlight links for subreddit discovery sr_discovery_links = +spotlight_interest_sub_p = .05 +spotlight_interest_nosub_p = .1 diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index 4da80f06e..0a88a5c7c 100755 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -59,6 +59,7 @@ from r2.lib.log import log_text from r2.lib.filters import safemarkdown from r2.lib.scraper import str_to_image from r2.controllers.api_docs import api_doc, api_section +from r2.lib.cloudsearch import basic_query import csv from collections import defaultdict @@ -2783,3 +2784,37 @@ class ApiController(RedditController): c.user.otp_secret = "" c.user._commit() form.redirect("/prefs/otp") + + @json_validate(query=VPrintable("query", max_length=50)) + @api_doc(api_section.subreddits, extensions=["json"]) + def GET_subreddits_by_topic(self, responder, query): + if not g.CLOUDSEARCH_SEARCH_API: + return [] + + if not query or not query.strip(): + return [] + + exclude = Subreddit.default_subreddits() + + q = basic_query(query, + facets={"reddit":{"sort":"-sum(text_relevance)", "count":20}}, + record_stats=True) + if not q["facets"]: + return [] + + sr_facets = [f["value"] for f in q["facets"]["reddit"]["constraints"]] + srs = Subreddit._by_name(sr_facets) + + results = [] + for sr_name in sr_facets: + sr = srs.get(sr_name) + if (sr._id in exclude or (sr.over_18 and not c.over18) + or not sr.can_view(c.user) + or sr.type == "archived"): + continue + + results.append({ + "name": sr_name, + }) + + return results diff --git a/r2/r2/controllers/listingcontroller.py b/r2/r2/controllers/listingcontroller.py index 8c7015765..ad241c335 100755 --- a/r2/r2/controllers/listingcontroller.py +++ b/r2/r2/controllers/listingcontroller.py @@ -313,6 +313,17 @@ class HotController(FixListing, ListingController): max_num = self.listing_obj.max_num, max_score = self.listing_obj.max_score).listing() + has_subscribed = c.user.has_subscribed + promo_visible = promote.is_promo(s.lookup[vislink]) + if not promo_visible: + prob = g.live_config['spotlight_interest_sub_p' + if has_subscribed else + 'spotlight_interest_nosub_p'] + if random.random() < prob: + bar = InterestBar(has_subscribed) + s.spotlight_items.insert(pos, bar) + s.visible_item = bar + if len(s.things) > 0: # only pass through a listing if the links made it # through our builder diff --git a/r2/r2/lib/app_globals.py b/r2/r2/lib/app_globals.py index 2711d494b..069da9fc8 100755 --- a/r2/r2/lib/app_globals.py +++ b/r2/r2/lib/app_globals.py @@ -180,6 +180,10 @@ class Globals(object): ConfigValue.bool: [ 'frontpage_dart', ], + ConfigValue.float: [ + 'spotlight_interest_sub_p', + 'spotlight_interest_nosub_p', + ], ConfigValue.tuple: [ 'sr_discovery_links', ], diff --git a/r2/r2/lib/js.py b/r2/r2/lib/js.py index 781745d2f..e60528c23 100755 --- a/r2/r2/lib/js.py +++ b/r2/r2/lib/js.py @@ -275,6 +275,7 @@ module["reddit"] = LocalizedModule("reddit.js", "login.js", "analytics.js", "flair.js", + "interestbar.js", "reddit.js", ) diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index 824c9ed63..883cf135f 100755 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -3619,3 +3619,8 @@ class TimeSeriesChart(Templated): self.classes = " ".join(classes) Templated.__init__(self) + +class InterestBar(Templated): + def __init__(self, has_subscribed): + self.has_subscribed = has_subscribed + Templated.__init__(self) diff --git a/r2/r2/lib/strings.py b/r2/r2/lib/strings.py index 2ac91d31e..40db1d15b 100644 --- a/r2/r2/lib/strings.py +++ b/r2/r2/lib/strings.py @@ -189,6 +189,7 @@ Note: there are a couple of places outside of your subreddit where someone can c view_subreddit_traffic = _("view subreddit traffic"), an_error_occurred = _("an error occurred"), + an_error_occurred_friendly = _("an error occurred. please try again later!"), ) class StringHandler(object): diff --git a/r2/r2/public/static/css/reddit.css b/r2/r2/public/static/css/reddit.css index caf4e2cab..093efb5b7 100755 --- a/r2/r2/public/static/css/reddit.css +++ b/r2/r2/public/static/css/reddit.css @@ -5709,3 +5709,118 @@ tr.gold-accent + tr > td { .users-online .word, .users-online .number:after { cursor: help; } + +.sr-interest-bar { + position: relative; + background: #cee3f8 url(../snoo-upside-down.png) 15px top no-repeat; + padding: 5px; + overflow: hidden; + border: 1px solid #336699; + margin-bottom: 10px; +} + +.organic-listing .sr-interest-bar { + border: none; + margin: 0; +} + +.sr-interest-bar .bubble { + position: relative; + margin-left: 85px; + margin-right: 68px; + max-width: 700px; + font-size: 13px; + background: white; + padding: 6px; + border-radius: 8px; +} + +.sr-interest-bar .bubble:after { + position: absolute; + display: block; + content: ''; + border: 10px solid; + border-style: solid solid outset; + border-color: transparent; + border-right-color: white; + left: -20px; + top: 15px; +} + +.sr-interest-bar .bubble p { + margin: 6px 3px; + margin-top: 0; +} + +.sr-interest-bar .subscribe { + background-image: url(../bg-button-add.png); /* SPRITE stretch-x */ + border: 1px solid #444; + border-radius: 3px; + padding: 0 6px; + color: white; + font-weight: bold; +} + +.sr-interest-bar .query-box { + position: relative; + padding: 2px 4px; + border: 2px solid #979797; + border-radius: 5px; +} + +.sr-interest-bar.focus .query-box { + border-color: #5f99cf; +} + +.sr-interest-bar.error .query-box { + border-color: #cf5e5e; +} + +.sr-interest-bar .error-caption, .sr-interest-bar.error .caption { + display: none; +} + +.sr-interest-bar.error .error-caption { + display: block; +} + +.sr-interest-bar .query { + width: 100%; + font-size: 20px; + margin: 0; + padding: 0; + border: none; + outline: none; +} + +.sr-interest-bar .throbber { + position: absolute; + right: 3px; + top: 5px; +} + +.sr-interest-bar ul.results { + margin: 0; + margin-top: 6px; + padding-top: 2px; + border-top: 1px dotted #bbb; + display: none; +} + +.sr-interest-bar li { + display: inline-block; + margin: 6px 3px; +} + +.sr-interest-bar a { + padding: 1px 2px; +} + +.sr-interest-bar a:hover { + text-decoration: underline; +} + +.sr-interest-bar .results .random { + color: gray; + font-weight: bold; +} diff --git a/r2/r2/public/static/js/analytics.js b/r2/r2/public/static/js/analytics.js index be6144a31..ec7adf851 100644 --- a/r2/r2/public/static/js/analytics.js +++ b/r2/r2/public/static/js/analytics.js @@ -113,7 +113,7 @@ r.analytics = { } r.analytics.breadcrumbs = { - selector: '.thing, .side, .sr-list, .srdrop, .tagline, .md, .organic-listing, .gadget, a, button, input', + selector: '.thing, .side, .sr-list, .srdrop, .tagline, .md, .organic-listing, .gadget, .sr-interest-bar, a, button, input', init: function() { this.hasSessionStorage = this._checkSessionStorage() diff --git a/r2/r2/public/static/js/base.js b/r2/r2/public/static/js/base.js index 00637a2c5..3f4c4682b 100644 --- a/r2/r2/public/static/js/base.js +++ b/r2/r2/public/static/js/base.js @@ -13,4 +13,5 @@ $(function() { r.login.ui.init() r.analytics.init() r.ui.HelpBubble.init() + r.interestbar.init() }) diff --git a/r2/r2/public/static/js/interestbar.js b/r2/r2/public/static/js/interestbar.js new file mode 100644 index 000000000..aea12e42f --- /dev/null +++ b/r2/r2/public/static/js/interestbar.js @@ -0,0 +1,81 @@ +r.interestbar = { + init: function() { + new r.ui.InterestBar($('.sr-interest-bar')) + } +} + +r.ui.InterestBar = function() { + r.ui.Base.apply(this, arguments) + this.$query = this.$el.find('.query') + this.queryChangedDebounced = _.debounce($.proxy(this, 'queryChanged'), 500) + this.$query.on('keyup', $.proxy(this, 'keyPressed')) + + this.$query + .on('focus', $.proxy(function() { + this.$el.addClass('focus') + }, this)) + .on('blur', $.proxy(function() { + this.$el.removeClass('focus') + }, this)) +} +r.ui.InterestBar.prototype = { + keyPressed: function() { + var query = this.$query.val() + query = $.trim(query) + if (query != this._lastQuery) { + this._lastQuery = query + this.$el.addClass('working') + this.queryChangedDebounced(query) + } + + if (!query) { + this.$el.removeClass('working error') + this.hideResults() + } + }, + + queryChanged: function(query) { + if (query) { + $.ajax({ + url: '/api/subreddits_by_topic.json', + data: {'query': query}, + success: $.proxy(this, 'displayResults'), + error: $.proxy(this, 'displayError') + }) + } + }, + + displayResults: function(results) { + this.$el.removeClass('working error') + + var first = this.$el.find('.results li:first'), + last = this.$el.find('.results li:last') + + var item = _.template( + '
  • ' + +'/r/<%= name %>' + +'
  • ' + ) + + this.$el.find('.results') + .empty() + .append(first) + .append(_.map(results, item).join('')) + .append(last) + .slideDown(150) + }, + + hideResults: function() { + this.$el.find('.results').slideUp(150) + }, + + displayError: function(xhr) { + this.$el + .removeClass('working') + .addClass('error') + .find('.error-caption') + .text(r.strings.an_error_occurred_friendly + ' (' + xhr.status + ')') + + this.hideResults() + } +} diff --git a/r2/r2/public/static/js/reddit.js b/r2/r2/public/static/js/reddit.js index 63191dbaf..16e590d8a 100644 --- a/r2/r2/public/static/js/reddit.js +++ b/r2/r2/public/static/js/reddit.js @@ -334,6 +334,8 @@ function organic_help(listing, thing) { help.find('.help-section').hide() if (thing.hasClass('promoted')) { help.find('.help-promoted').show() + } else if (thing.hasClass('interestbar')) { + help.find('.help-interestbar').show() } else { help.find('.help-organic').show() } diff --git a/r2/r2/public/static/snoo-upside-down.png b/r2/r2/public/static/snoo-upside-down.png new file mode 100644 index 000000000..e86f17f53 Binary files /dev/null and b/r2/r2/public/static/snoo-upside-down.png differ diff --git a/r2/r2/templates/interestbar.html b/r2/r2/templates/interestbar.html new file mode 100644 index 000000000..e1693b4ee --- /dev/null +++ b/r2/r2/templates/interestbar.html @@ -0,0 +1,45 @@ +## 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-2012 +## reddit Inc. All Rights Reserved. +############################################################################### + +
    +
    + <% + if thing.has_subscribed: + msg = _("ready for something new? %s subscribe %s to some new subreddits.") + else: + msg = _("it looks like you haven't %s subscribed %s to any subreddits yet. want some ideas?") + + pre, sub, post = msg.split("%s") + %> +

    ${pre} ${post}

    +

    +
    + +
    +
    diff --git a/r2/r2/templates/spotlightlisting.html b/r2/r2/templates/spotlightlisting.html index d9d8a4842..47dd87b9d 100644 --- a/r2/r2/templates/spotlightlisting.html +++ b/r2/r2/templates/spotlightlisting.html @@ -21,9 +21,10 @@ ############################################################################### <%namespace file="printablebuttons.html" import="ynbutton"/> -<%namespace file="utils.html" import="text_with_links"/> +<%namespace file="utils.html" import="tags, text_with_links"/> <% from r2.lib.template_helpers import static + from r2.lib.wrapped import Templated %>
    @@ -31,7 +32,11 @@ seen = set([]) %> %for name in thing.spotlight_items: - %if name in seen: + %if isinstance(name, Templated): +
    + ${unsafe(name.render())} +
    + %elif name in seen: <% pass %> %elif name in thing.lookup: <% seen.add(name) %> @@ -77,6 +82,15 @@ hidden_data = dict(id="organic"))} %endif
    +
    +

    ${_("Enter a keyword or topic to discover new subreddits around your interests. Be specific!")}

    +

    + ${text_with_links( + _("You can access this tool at any time on the %(reddits)s page."), + reddits=dict(link_text="/reddits/", path="/reddits/") + )} +

    +