diff --git a/r2/r2/config/routing.py b/r2/r2/config/routing.py index 377f6e998..8013478d1 100644 --- a/r2/r2/config/routing.py +++ b/r2/r2/config/routing.py @@ -128,6 +128,11 @@ def make_map(): where='overview') mc('/u/:username', controller='redirect', action='user_redirect') + mc('/user/:username/m/:multi', controller='hot', action='listing') + mc('/user/:username/m/:multi/new', controller='new', action='listing') + mc('/user/:username/m/:multi/:sort', controller='browse', sort='top', + action='listing', requirements=dict(sort='top|controversial')) + # preserve timereddit URLs from 4/1/2012 mc('/t/:timereddit', controller='redirect', action='timereddit_redirect') mc('/t/:timereddit/*rest', controller='redirect', diff --git a/r2/r2/controllers/listingcontroller.py b/r2/r2/controllers/listingcontroller.py index 68a8f1feb..97a811ab3 100755 --- a/r2/r2/controllers/listingcontroller.py +++ b/r2/r2/controllers/listingcontroller.py @@ -77,6 +77,7 @@ class ListingController(RedditController, OAuth2ResourceController): # login box, subreddit box, submit box, etc, visible show_sidebar = True + show_chooser = False # class (probably a subclass of Reddit) to use to render the page. render_cls = Reddit @@ -112,14 +113,22 @@ class ListingController(RedditController, OAuth2ResourceController): if self.bare: return responsive(content.render()) - else: - return self.render_cls(content=content, - page_classes=self.extra_page_classes, - show_sidebar=self.show_sidebar, - nav_menus=self.menus, - title=self.title(), - robots=getattr(self, "robots", None), - **self.render_params).render() + + page_classes = self.extra_page_classes + + if (self.show_chooser and + c.user_is_loggedin and c.user.pref_show_left_bar and + isinstance(c.site, (DefaultSR, AllSR, LabeledMulti))): + page_classes = page_classes + ['with-listing-chooser'] + content = PaneStack([ListingChooser(), content]) + + return self.render_cls(content=content, + page_classes=page_classes, + show_sidebar=self.show_sidebar, + nav_menus=self.menus, + title=self.title(), + robots=getattr(self, "robots", None), + **self.render_params).render() def content(self): """Renderable object which will end up as content of the render_cls""" @@ -225,6 +234,7 @@ class FixListing(object): class HotController(FixListing, ListingController): where = 'hot' extra_page_classes = ListingController.extra_page_classes + ['hot-page'] + show_chooser = True def make_requested_ad(self): try: @@ -395,6 +405,7 @@ class NewController(ListingController): where = 'new' title_text = _('newest submissions') extra_page_classes = ListingController.extra_page_classes + ['new-page'] + show_chooser = True def keep_fn(self): def keep(item): @@ -443,6 +454,7 @@ class RisingController(NewController): class BrowseController(ListingController): where = 'browse' + show_chooser = True def keep_fn(self): """For merged time-listings, don't show items that are too old diff --git a/r2/r2/controllers/post.py b/r2/r2/controllers/post.py index 3e7fb5d4c..b21f3c7bd 100644 --- a/r2/r2/controllers/post.py +++ b/r2/r2/controllers/post.py @@ -101,6 +101,7 @@ class PostController(ApiController): pref_show_adbox = VBoolean("show_adbox"), pref_show_sponsors = VBoolean("show_sponsors"), pref_show_sponsorships = VBoolean("show_sponsorships"), + pref_show_left_bar = VBoolean("show_left_bar"), pref_highlight_new_comments = VBoolean("highlight_new_comments"), pref_monitor_mentions=VBoolean("monitor_mentions"), all_langs = VOneOf('all-langs', ('all', 'some'), default='all')) diff --git a/r2/r2/controllers/reddit_base.py b/r2/r2/controllers/reddit_base.py index b70581849..1585b8d42 100644 --- a/r2/r2/controllers/reddit_base.py +++ b/r2/r2/controllers/reddit_base.py @@ -91,6 +91,7 @@ from r2.models import ( FakeSubreddit, Friends, Frontpage, + LabeledMulti, Link, MultiReddit, NotFound, @@ -103,6 +104,7 @@ from r2.models import ( valid_feed, valid_otp_cookie, ) +from r2.lib.db import tdb_cassandra NEVER = datetime(2037, 12, 31, 23, 59, 59) @@ -386,6 +388,14 @@ def set_subreddit(): elif not c.error_page and not request.path.startswith("/api/login/") : abort(404) + routes_dict = request.environ["pylons.routes_dict"] + if "multi" in routes_dict and "username" in routes_dict: + try: + path = '/user/%s/m/%s' % (routes_dict["username"], routes_dict["multi"]) + c.site = LabeledMulti._byID(path) + except tdb_cassandra.NotFound: + abort(404) + #if we didn't find a subreddit, check for a domain listing if not sr_name and isinstance(c.site, DefaultSR) and domain: c.site = DomainSR(domain) @@ -1068,13 +1078,19 @@ class RedditController(MinimalController): # check if the user has access to this subreddit if not c.site.can_view(c.user) and not c.error_page: - public_description = c.site.public_description - errpage = pages.RedditError(strings.private_subreddit_title, - strings.private_subreddit_message, - image="subreddit-private.png", - sr_description=public_description) - request.environ['usable_error_content'] = errpage.render() - self.abort403() + if isinstance(c.site, LabeledMulti): + # do not leak the existence of multis via 403. + self.abort404() + else: + public_description = c.site.public_description + errpage = pages.RedditError( + strings.private_subreddit_title, + strings.private_subreddit_message, + image="subreddit-private.png", + sr_description=public_description, + ) + request.environ['usable_error_content'] = errpage.render() + self.abort403() #check over 18 if (c.site.over_18 and not c.over18 and diff --git a/r2/r2/lib/js.py b/r2/r2/lib/js.py index e5f07db8f..6df361324 100755 --- a/r2/r2/lib/js.py +++ b/r2/r2/lib/js.py @@ -376,6 +376,7 @@ module["reddit"] = LocalizedModule("reddit.js", "wiki.js", "apps.js", "gold.js", + "multi.js", ) module["admin"] = Module("admin.js", diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index 9f6f5d3a5..09be19c61 100755 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -25,7 +25,7 @@ from collections import OrderedDict from r2.lib.wrapped import Wrapped, Templated, CachedTemplate from r2.models import Account, FakeAccount, DefaultSR, make_feedurl from r2.models import FakeSubreddit, Subreddit, SubSR, AllMinus, AllSR -from r2.models import Friends, All, Sub, NotFound, DomainSR, Random, Mod, RandomNSFW, RandomSubscription, MultiReddit, ModSR, Frontpage +from r2.models import Friends, All, Sub, NotFound, DomainSR, Random, Mod, RandomNSFW, RandomSubscription, MultiReddit, ModSR, Frontpage, LabeledMulti from r2.models import Link, Printable, Trophy, bidding, PromoCampaign, PromotionWeights, Comment from r2.models import Flair, FlairTemplate, FlairTemplateBySubredditIndex from r2.models import USER_FLAIR, LINK_FLAIR @@ -91,7 +91,7 @@ except ImportError: def ips_by_account_id(account_id): return [] -from things import wrap_links, default_thing_wrapper +from things import wrap_links, wrap_things, default_thing_wrapper datefmt = _force_utf8(_('%d %b %Y')) @@ -350,19 +350,24 @@ class Reddit(Templated): if c.user.pref_show_sponsorships or not c.user.gold: ps.append(SponsorshipBox()) - if isinstance(c.site, (MultiReddit, ModSR)) and c.user_is_loggedin: + if isinstance(c.site, (MultiReddit, ModSR)): srs = Subreddit._byID(c.site.sr_ids, data=True, return_dict=False) - if c.user_is_admin or c.site.is_moderator(c.user): - ps.append(self.sr_admin_menu()) - if srs: + if isinstance(c.site, LabeledMulti): + ps.append(MultiInfoBar(c.site, srs, c.user)) + c.js_preload.set_wrapped( + '/api/multi/%s' % c.site.path.lstrip('/'), c.site) + elif srs: if isinstance(c.site, ModSR): box = SubscriptionBox(srs, multi_text=strings.mod_multi) else: box = SubscriptionBox(srs) ps.append(SideContentBox(_('these subreddits'), [box])) + if c.user_is_admin or c.site.is_moderator(c.user): + ps.append(self.sr_admin_menu()) + if isinstance(c.site, AllSR): ps.append(AllInfoBar(c.site, c.user)) @@ -1801,6 +1806,15 @@ class SubredditTopBar(CachedTemplate): return menus + +class MultiInfoBar(Templated): + def __init__(self, multi, srs, user): + Templated.__init__(self) + self.multi = wrap_things(multi)[0] + self.can_edit = multi.can_edit(user) + self.srs = srs + + class SubscriptionBox(Templated): """The list of reddits a user is currently subscribed to to go in the right pane.""" @@ -4062,6 +4076,38 @@ class ModeratorPermissions(Templated): Templated.__init__(self, permissions_type=permissions_type, editable=editable, embedded=embedded) +class ListingChooser(Templated): + def __init__(self): + Templated.__init__(self) + self.sections = defaultdict(list) + self.add_item("global", _("subscribed"), '/', + description=_("your front page")) + self.add_item("other", _("everything"), '/r/all', + description=_("from all subreddits")) + + if c.user_is_loggedin: + multis = LabeledMulti.by_owner(c.user) + multis.sort(key=lambda multi: multi.name) + for multi in multis: + self.add_item("multi", multi.name, multi.path) + self.selected_item = self.find_selected() + self.selected_item["selected"] = True + + def add_item(self, section, name, path, description=None): + self.sections[section].append({ + "name": name, + "description": description, + "path": path, + "selected": False, + }) + + def find_selected(self): + path = request.path + matching = [item for item in chain(*self.sections.values()) + if path.startswith(item["path"]) or + c.site.path.startswith(item["path"])] + matching.sort(key=lambda item: len(item["path"]), reverse=True) + return matching[0] class PolicyView(Templated): pass diff --git a/r2/r2/lib/strings.py b/r2/r2/lib/strings.py index 585e2615f..d5879eb24 100644 --- a/r2/r2/lib/strings.py +++ b/r2/r2/lib/strings.py @@ -209,6 +209,10 @@ Note: there are a couple of places outside of your subreddit where someone can c all_msg=_("full permissions"), none_msg=_("no permissions"), ), + categorize = _('categorize'), + are_you_sure = _('are you sure?'), + yes = _('yes'), + no = _('no'), ) class StringHandler(object): diff --git a/r2/r2/models/account.py b/r2/r2/models/account.py index 05bcc3b70..da6cd9642 100644 --- a/r2/r2/models/account.py +++ b/r2/r2/models/account.py @@ -88,6 +88,7 @@ class Account(Thing): pref_show_adbox = True, pref_show_sponsors = True, # sponsored links pref_show_sponsorships = True, + pref_show_left_bar = True, pref_highlight_new_comments = True, pref_monitor_mentions=True, mobile_compress = False, diff --git a/r2/r2/public/static/add.png b/r2/r2/public/static/add.png new file mode 100644 index 000000000..62f7948b0 Binary files /dev/null and b/r2/r2/public/static/add.png differ diff --git a/r2/r2/public/static/close-small.png b/r2/r2/public/static/close-small.png new file mode 100644 index 000000000..c12bb7a95 Binary files /dev/null and b/r2/r2/public/static/close-small.png differ diff --git a/r2/r2/public/static/css/reddit.less b/r2/r2/public/static/css/reddit.less index 5219d5437..3dc8139f2 100755 --- a/r2/r2/public/static/css/reddit.less +++ b/r2/r2/public/static/css/reddit.less @@ -1,3 +1,17 @@ +.box-sizing(@sizing) { + box-sizing: @sizing; + -webkit-box-sizing: @sizing; + -moz-box-sizing: @sizing; +} + +.no-select { + -webkit-user-select: none; + -moz-user-select: none; + -o-user-select: none; + -ms-user-select: none; + user-select: none; +} + html { height: 100%; /* Needed for toolbar's comments panel's pinstripe */ } @@ -1020,6 +1034,77 @@ a.author { margin-right: 0.5em; } } } +.hover-bubble.multi-selector { + @arrow-offset: 40px; + margin-top: -7px - @arrow-offset; + min-width: 130px; + min-height: @arrow-offset * 2; + padding: 8px 0; + .no-select; + + &:before, &:after { + top: 8px + @arrow-offset; + } + + strong, a.sr { + display: block; + margin: 3px 0; + text-align: center; + } + + strong { + font-size: 1.05em; + font-weight: bold; + color: #333; + } + + .throbber { + position: absolute; + top: 10px; + right: 8px; + } + + .multi-list { + margin-top: 5px; + } + + label { + font-size: 1.25em; + display: block; + padding: 5px 12px; + + &:hover { + background: #eee; + } + + input[type="checkbox"] { + margin-top: 0; + vertical-align: middle; + } + + a { + float: right; + margin-left: 7px; + width: 12px; + height: 12px; + line-height: 12px; + background: white; + border: 1px solid lighten(#369, 20%); + border-radius: 2px; + text-align: center; + opacity: .65; + + &:hover { + opacity: 1; + } + } + } + + input { + margin-right: 5px; + } +} + .infotext { border: 1px solid #369; background-color: #EFF7FF; @@ -4998,6 +5083,135 @@ table.calendar { margin-left: 5px; } +.confirm-button .confirmation { + color: red; + white-space: nowrap; + + .prompt { + margin-right: .5em; + } +} + +.gray-buttons { + button { + padding: 0; + margin: 0; + border: none; + background: none; + color: #888; + font-weight: bold; + + &:hover { + text-decoration: underline; + } + } +} + +.multi-details { + h1 { + margin-bottom: 0; + } + + h1 a, .throbber { + vertical-align: middle; + } + .throbber { + margin-left: 5px; + } + + h2 { + margin-top: 0; + margin-bottom: 3px; + } + + .settings { + margin-bottom: 5px; + + input[type="radio"] { + margin: 0; + margin-right: 3px; + vertical-align: middle; + } + + label { + margin-right: 10px; + } + + .delete { + margin-left: 5px; + } + } + + h3 { + font-weight: normal; + color: #777; + margin-bottom: 3px; + } + + ul, form.add-sr { + margin-left: 12px; + } + + button.remove-sr, button.add { + .box-sizing(content-box); + text-indent: -9999px; + margin-left: 3px; + background: none no-repeat; + border: 3px solid transparent; + padding: 0; + opacity: .3; + + &:hover { + opacity: 1; + } + + &:active { + position: relative; + top: 1px; + } + + &.remove-sr { + width: 9px; + height: 9px; + background-image: url(../close-small.png); /* SPRITE */ + } + + &.add { + width: 15px; + height: 15px; + background-image: url(../add.png); /* SPRITE */ + } + } + + form.add-sr { + .sr-name, button.add { + vertical-align: middle; + } + + .sr-name { + border: 1px solid #ccc; + padding: 3px; + } + + button.add { + border: 5px solid transparent; + } + } + + li { + font-size: 1.15em; + line-height: 1.5em; + + a, button { + vertical-align: middle; + } + } + + .bottom { + margin-top: 1em; + } +} + .sidecontentbox { font-size: normal; } @@ -6712,3 +6926,212 @@ body.gold .buttons li.comment-save-button { display: inline; } text-align: left; white-space: normal; } + +body.with-listing-chooser { + position: relative; + + & #header .tabmenu { + position: absolute; + margin-left: -2px; + left: 130px; + bottom: 0; + + li:first-child.selected { + margin-left: 2px; + } + } + + & #header .pagename { + position: absolute; + left: 130px; + bottom: 20px; + } + + & > .content, & .footer-parent { + margin-left: 140px + } +} + +.listing-chooser { + @width: 130px; + @shadow-width: 4px; + position: absolute; + top: 65px; + left: 0; + bottom: 0; + width: @width; + border-right: 1px solid #ccc; + background: #f7f7f7; + + &:after { + content: ''; + position: absolute; + right: 0; + top: 0; + bottom: 0; + left: @width - @shadow-width; + box-shadow: -@shadow-width 0 2*@shadow-width -@shadow-width rgba(0, 0, 0, .3) inset; + z-index: 90; + } + + h3 { + color: #777; + text-align: right; + padding: 4px; + } + + ul.global, ul.other { + padding: 8px 0; + li { + margin-left: 4px; + + a { + font-size: 1.3em; + padding: 1em 5px; + padding-left: 12px; + } + } + } + + ul.other { + margin-top: 10px; + } + + ul.multis { + li { + margin-left: 12px; + + a { + font-size: 1.2em; + padding: .8em 5px; + padding-left: 10px; + } + } + } + + li { + text-align: left; + margin-bottom: 3px; + background: #fff; + border: 1px solid #ccc; + border-bottom-width: 2px; + border-right: none; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + + a { + display: block; + position: relative; + overflow: hidden; + text-overflow: ellipsis; + margin-right: 5px; + + .description { + color: gray; + font-size: .8em; + font-weight: normal; + } + } + + &:last-child a { + border-bottom: none; + } + + &.selected { + position: relative; + background: lighten(#cee3f8, 6%); + border-color: lighten(#369, 40%); + z-index: 100; + + a { + font-weight: bold; + } + + &:after, + &:before { + position: absolute; + top: 50%; + margin-top: -15px; + display: block; + content: ''; + border: 15px solid transparent; + border-style: solid solid outset; // mitigates firefox drawing a thicker arrow + z-index: 100; + } + + &:before { + right: 0px; + border-right-color: #ccc; + } + + &:after { + right: -2px; + border-right-color: white; + } + } + } + + + .create { + padding: 5px; + + input[type="text"] { + width: 95px; + background: white; + border: 1px solid #ccc; + padding: 2px 5px; + margin-bottom: 3px; + display: none; + } + + .error { + margin: 4px 0; + } + + button { + display: inline; + text-align: center; + padding: 1px 4px; + margin: 0; + + background: none; + border: 1px solid #777; + border-radius: 3px; + opacity: .5; + + &:hover { + opacity: .90; + } + + &:active { + background: #e9e9e9; + } + } + + button, .throbber { + vertical-align: middle; + } + + .throbber { + float: right; + } + + &.expanded { + input[type="text"] { + display: block; + } + + button { + opacity: .75; + background: #fcfcfc; + box-shadow: 0 1px 1px rgba(0, 0, 0, .25); + + &:active { + position: relative; + top: 1px; + box-shadow: none; + } + } + } + } +} diff --git a/r2/r2/public/static/js/base.js b/r2/r2/public/static/js/base.js index 3302b1a16..f2902bc1e 100644 --- a/r2/r2/public/static/js/base.js +++ b/r2/r2/public/static/js/base.js @@ -19,12 +19,21 @@ r.setup = function(config) { r.setupBackbone = function() { Backbone.ajax = function(request) { - var preloaded = r.preload.read(request.url) + var url = request.url, + preloaded = r.preload.read(url) if (preloaded != null) { request.success(preloaded) return } + var isLocal = url && (url[0] == '/' || url.lastIndexOf(r.config.currentOrigin, 0) == 0) + if (isLocal) { + if (!request.headers) { + request.headers = {} + } + request.headers['X-Modhash'] = r.config.modhash + } + return Backbone.$.ajax(request) } } @@ -39,4 +48,5 @@ $(function() { r.apps.init() r.wiki.init() r.gold.init() + r.multi.init() }) diff --git a/r2/r2/public/static/js/multi.js b/r2/r2/public/static/js/multi.js new file mode 100644 index 000000000..7d1f0c6cf --- /dev/null +++ b/r2/r2/public/static/js/multi.js @@ -0,0 +1,240 @@ +r.multi = { + init: function() { + this.mine = new r.multi.MyMultiCollection() + + var detailsEl = $('.multi-details') + if (detailsEl.length) { + var multi = new r.multi.MultiReddit({ + path: detailsEl.data('path') + }) + multi.fetch() + new r.multi.MultiDetails({ + model: multi, + el: detailsEl + }) + } + + var subscribeBubbleGroup = {} + $('.subscribe-button').each(function(idx, el) { + new r.multi.SubscribeButton({ + el: el, + bubbleGroup: subscribeBubbleGroup + }) + }) + + $('.listing-chooser').each(function(idx, el) { + new r.multi.ListingChooser({el: el}) + }) + } +} + +r.multi.MultiRedditList = Backbone.Collection.extend({ + model: Backbone.Model.extend({ + idAttribute: 'name' + }) +}) + +r.multi.MultiReddit = Backbone.Model.extend({ + idAttribute: 'path', + url: function() { + return r.utils.joinURLs('/api/multi', this.id) + }, + + initialize: function() { + this.subreddits = new r.multi.MultiRedditList(this.get('subreddits')) + this.subreddits.url = this.url() + '/r/' + this.on('change:subreddits', function(model, value) { + this.subreddits.reset(value) + }, this) + this.subreddits.on('request', function(model, xhr, options) { + this.trigger('request', model, xhr, options) + }, this) + }, + + parse: function(response) { + return response.data + }, + + addSubreddit: function(name, options) { + this.subreddits.create({name: name}, options) + }, + + removeSubreddit: function(name, options) { + this.subreddits.get(name).destroy(options) + } +}) + +r.multi.MyMultiCollection = Backbone.Collection.extend({ + url: '/api/multi/mine', + model: r.multi.MultiReddit, + + create: function(attributes, options) { + if ('name' in attributes) { + attributes['path'] = '/user/' + r.config.logged + '/m/' + attributes['name'] + delete attributes['name'] + } + Backbone.Collection.prototype.create.call(this, attributes, options) + } +}) + +r.multi.MultiDetails = Backbone.View.extend({ + itemTemplate: _.template('
  • /r/<%= name %>
  • '), + events: { + 'submit .add-sr': 'addSubreddit', + 'click .remove-sr': 'removeSubreddit', + 'change [name="visibility"]': 'setVisibility', + 'confirm .delete': 'deleteMulti' + }, + + initialize: function() { + this.model.subreddits.on('add remove', this.render, this) + this.model.on('request', function(model, xhr) { + r.ui.showWorkingDeferred(this.$el, xhr) + }, this) + new r.ui.ConfirmButton({el: this.$('button.delete')}) + }, + + render: function() { + var srList = this.$('.subreddits') + srList.empty() + this.model.subreddits.each(function(sr) { + srList.append(this.itemTemplate({ + name: sr.get('name') + })) + }, this) + }, + + addSubreddit: function(ev) { + ev.preventDefault() + + var nameEl = this.$('.add-sr .sr-name'), + srName = $.trim(nameEl.val()) + if (!srName) { + return + } + + nameEl.val('') + this.$('.add-error').css('visibility', 'hidden') + this.model.addSubreddit(srName, { + wait: true, + success: _.bind(function() { + r.ui.refreshListing() + this.$('.add-error').hide() + }, this), + error: _.bind(function(model, xhr) { + var resp = JSON.parse(xhr.responseText) + this.$('.add-error') + .text(resp.explanation) + .css('visibility', 'visible') + .show() + }, this) + }) + }, + + removeSubreddit: function(ev) { + var srName = $(ev.target).parent().data('name') + this.model.removeSubreddit(srName, {success: r.ui.refreshListing}) + }, + + setVisibility: function() { + this.model.save({ + visibility: this.$('[name="visibility"]:checked').val() + }) + }, + + deleteMulti: function() { + this.model.destroy({ + success: function() { + window.location = '/' + } + }) + } +}) + +r.multi.SubscribeButton = Backbone.View.extend({ + initialize: function() { + this.bubble = new r.multi.MultiSubscribeBubble({ + parent: this.$el, + group: this.options.bubbleGroup, + sr_name: this.$el.data('sr_name') + }) + this.$el.append(this.bubble.el) + } +}) + +r.multi.MultiSubscribeBubble = r.ui.Bubble.extend({ + className: 'multi-selector hover-bubble anchor-right', + template: _.template('
    <%= title %>/r/<%= sr_name %>
    '), + itemTemplate: _.template(''), + + events: { + 'click input': 'toggleSubscribed' + }, + + initialize: function() { + this.on('show', this.load, this) + this.listenTo(r.multi.mine, 'reset', this.render) + r.ui.Bubble.prototype.initialize.apply(this) + }, + + load: function() { + r.ui.showWorkingDeferred(this.$el, r.multi.mine.fetch()) + }, + + render: function() { + this.$el.html(this.template({ + title: r.strings('categorize'), + sr_name: this.options.sr_name + })) + + var content = $('
    ') + r.multi.mine.each(function(multi) { + content.append(this.itemTemplate({ + name: multi.get('name'), + path: multi.get('path'), + checked: multi.subreddits.get(this.options.sr_name) + ? 'checked' : '' + })) + }, this) + this.$el.append(content) + }, + + toggleSubscribed: function(ev) { + var checkbox = $(ev.target), + multi = r.multi.mine.get(checkbox.data('path')) + if (checkbox.is(':checked')) { + multi.addSubreddit(this.options.sr_name) + } else { + multi.removeSubreddit(this.options.sr_name) + } + } +}) + +r.multi.ListingChooser = Backbone.View.extend({ + events: { + 'submit .create': 'createClick' + }, + + createClick: function(ev) { + ev.preventDefault() + if (!this.$('.create').is('.expanded')) { + this.$('.create').addClass('expanded') + this.$('.create input[type="text"]').focus() + } else { + var name = this.$('.create input[type="text"]').val() + name = $.trim(name) + if (name) { + r.multi.mine.create({name: name}, { + success: function(multi) { + window.location = multi.get('path') + }, + error: _.bind(function(multi, xhr) { + var resp = JSON.parse(xhr.responseText) + this.$('.error').text(resp.explanation).show() + }, this), + beforeSend: _.bind(r.ui.showWorkingDeferred, this, this.$el) + }) + } + } + } +}) diff --git a/r2/r2/public/static/js/ui.js b/r2/r2/public/static/js/ui.js index e5a8fde1e..7bb555d58 100644 --- a/r2/r2/public/static/js/ui.js +++ b/r2/r2/public/static/js/ui.js @@ -557,3 +557,40 @@ r.ui.scrollFixed.prototype = { } } } + +r.ui.ConfirmButton = Backbone.View.extend({ + confirmTemplate: _.template('<%= are_you_sure %> /
    '), + events: { + 'click': 'click' + }, + + initialize: function() { + // wrap the specified element in a and move its classes over to + // the wrapper. this is intended for progressive enhancement of a bare + //
    + + + + + + diff --git a/r2/r2/templates/multiinfobar.html b/r2/r2/templates/multiinfobar.html new file mode 100644 index 000000000..79204ffa6 --- /dev/null +++ b/r2/r2/templates/multiinfobar.html @@ -0,0 +1,73 @@ +## 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. +############################################################################### + +<%! + from r2.lib.strings import strings, Score + from r2.lib.pages import WrappedUser, ModList + %> + +<%namespace file="utils.html" import="plain_link, thing_timestamp, text_with_links"/> +<%namespace file="printablebuttons.html" import="ynbutton, state_button" /> + +
    +

    + ${_('%s subreddits') % thing.multi.name}
    +

    +

    ${_('curated by /u/%s') % thing.multi.owner.name}

    + %if thing.can_edit: +
    + + + +
    + %endif + +

    ${_('subreddits in this multi:')}

    + + + %if thing.can_edit: +
    + +
    +
    + %endif + +
    + %if thing.multi.owner: + ${unsafe(_("created by %(user)s") % dict(user = unsafe(WrappedUser(thing.multi.owner).render())))} + %endif + + + ${_("a multireddit for")} ${thing_timestamp(thing.multi)} + +
    +
    + diff --git a/r2/r2/templates/prefoptions.html b/r2/r2/templates/prefoptions.html index ee0c30f36..e3efd3512 100644 --- a/r2/r2/templates/prefoptions.html +++ b/r2/r2/templates/prefoptions.html @@ -232,6 +232,8 @@ ${checkbox(_("allow subreddits to show me custom styles"), "show_stylesheets")}
    + ${checkbox(_("show left sidebar"), "show_left_bar")} +
    ${checkbox(_("show user flair"), "show_flair")}
    ${checkbox(_("show link flair"), "show_link_flair")}