mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-01-28 08:17:58 -05:00
Multi frontend: listings, sidebar, and editing UI.
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -376,6 +376,7 @@ module["reddit"] = LocalizedModule("reddit.js",
|
||||
"wiki.js",
|
||||
"apps.js",
|
||||
"gold.js",
|
||||
"multi.js",
|
||||
)
|
||||
|
||||
module["admin"] = Module("admin.js",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
BIN
r2/r2/public/static/add.png
Normal file
BIN
r2/r2/public/static/add.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 269 B |
BIN
r2/r2/public/static/close-small.png
Normal file
BIN
r2/r2/public/static/close-small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 241 B |
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
240
r2/r2/public/static/js/multi.js
Normal file
240
r2/r2/public/static/js/multi.js
Normal file
@@ -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('<li data-name="<%= name %>"><a href="/r/<%= name %>">/r/<%= name %></a><button class="remove-sr">x</button></li>'),
|
||||
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('<div class="title"><strong><%= title %></strong><a class="sr" href="/r/<%= sr_name %>">/r/<%= sr_name %></a></div><div class="throbber"></div>'),
|
||||
itemTemplate: _.template('<label><input type="checkbox" data-path="<%= path %>" <%= checked %>><%= name %><a href="<%= path %>" target="_blank">›</a></label>'),
|
||||
|
||||
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 = $('<div class="multi-list">')
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -557,3 +557,40 @@ r.ui.scrollFixed.prototype = {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.ui.ConfirmButton = Backbone.View.extend({
|
||||
confirmTemplate: _.template('<span class="confirmation"><span class="prompt"><%= are_you_sure %></span><button class="yes"><%= yes %></button> / <button class="no"><%= no %></button></div>'),
|
||||
events: {
|
||||
'click': 'click'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
// wrap the specified element in a <span> and move its classes over to
|
||||
// the wrapper. this is intended for progressive enhancement of a bare
|
||||
// <button> element.
|
||||
this.$target = this.$el
|
||||
this.$target.wrap('<span>')
|
||||
this.setElement(this.$target.parent())
|
||||
this.$el
|
||||
.attr('class', this.$target.attr('class'))
|
||||
.addClass('confirm-button')
|
||||
this.$target.attr('class', null)
|
||||
},
|
||||
|
||||
click: function(ev) {
|
||||
var target = $(ev.target)
|
||||
if (this.$target.is(target)) {
|
||||
this.$target.hide()
|
||||
this.$el.append(this.confirmTemplate({
|
||||
are_you_sure: r.strings('are_you_sure'),
|
||||
yes: r.strings('yes'),
|
||||
no: r.strings('no')
|
||||
}))
|
||||
} else if (target.is('.no')) {
|
||||
this.$('.confirmation').remove()
|
||||
this.$target.show()
|
||||
} else if (target.is('.yes')) {
|
||||
this.$target.trigger('confirm')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -7,6 +7,15 @@ r.utils = {
|
||||
return r.config.static_root + '/' + item
|
||||
},
|
||||
|
||||
joinURLs: function(/* arguments */) {
|
||||
return _.map(arguments, function(url, idx) {
|
||||
if (idx > 0 && url && url[0] != '/') {
|
||||
url = '/' + url
|
||||
}
|
||||
return url
|
||||
}).join('')
|
||||
},
|
||||
|
||||
querySelectorFromEl: function(targetEl, selector) {
|
||||
return $(targetEl).parents().andSelf()
|
||||
.filter(selector || '*')
|
||||
|
||||
60
r2/r2/templates/listingchooser.html
Normal file
60
r2/r2/templates/listingchooser.html
Normal file
@@ -0,0 +1,60 @@
|
||||
## 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.
|
||||
###############################################################################
|
||||
|
||||
<%def name="section_items(itemlist)">
|
||||
%for item in itemlist:
|
||||
<li
|
||||
%if item['selected']:
|
||||
class="selected"
|
||||
%endif
|
||||
>
|
||||
<a href="${item['path']}">
|
||||
${item['name']}
|
||||
%if 'description' in item:
|
||||
<br><span class="description">${item['description']}</span>
|
||||
%endif
|
||||
</a>
|
||||
</li>
|
||||
%endfor
|
||||
</%def>
|
||||
|
||||
<div class="listing-chooser">
|
||||
<ul class="global">
|
||||
${section_items(thing.sections['global'])}
|
||||
</ul>
|
||||
|
||||
<h3>${_('multireddits')}</h3>
|
||||
<ul class="multis">
|
||||
${section_items(thing.sections['multi'])}
|
||||
<li class="create">
|
||||
<form>
|
||||
<input type="text" placeholder="${_('name')}"></input>
|
||||
<div class="error"></div>
|
||||
<button>${_('create')}</button><div class="throbber"></div>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="other">
|
||||
${section_items(thing.sections['other'])}
|
||||
</ul>
|
||||
</div>
|
||||
73
r2/r2/templates/multiinfobar.html
Normal file
73
r2/r2/templates/multiinfobar.html
Normal file
@@ -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" />
|
||||
|
||||
<div class="titlebox multi-details" data-path="${thing.multi.path}">
|
||||
<h1 class="hover redditname">
|
||||
<a href="${thing.multi.path}">${_('%s subreddits') % thing.multi.name}</a><div class="throbber"></div>
|
||||
</h1>
|
||||
<h2><a href="/user/${thing.multi.owner.name}">${_('curated by /u/%s') % thing.multi.owner.name}</a></h2>
|
||||
%if thing.can_edit:
|
||||
<div class="gray-buttons settings">
|
||||
<label><input type="radio" name="visibility" value="private" ${'checked' if thing.multi.visibility == 'private' else ''}>${_('private')}</label>
|
||||
<label><input type="radio" name="visibility" value="public" ${'checked' if thing.multi.visibility == 'public' else ''}>${_('public')}</label>
|
||||
<button class="delete">${_('delete')}</button>
|
||||
</div>
|
||||
%endif
|
||||
|
||||
<h3>${_('subreddits in this multi:')}</h3>
|
||||
<ul class="subreddits">
|
||||
%for sr in thing.srs:
|
||||
<li data-name="${sr.name}">
|
||||
<a href="/r/${sr.name}">/r/${sr.name}</a>
|
||||
%if thing.can_edit:
|
||||
<button class="remove-sr">${_('remove')}</button>
|
||||
%endif
|
||||
</li>
|
||||
%endfor
|
||||
</ul>
|
||||
|
||||
%if thing.can_edit:
|
||||
<form class="add-sr">
|
||||
<input type="text" class="sr-name" placeholder="${_('add subreddit')}"><button class="add">${_('add')}</button>
|
||||
<div class="error add-error"></div>
|
||||
</form>
|
||||
%endif
|
||||
|
||||
<div class="bottom">
|
||||
%if thing.multi.owner:
|
||||
${unsafe(_("created by %(user)s") % dict(user = unsafe(WrappedUser(thing.multi.owner).render())))}
|
||||
%endif
|
||||
|
||||
<span class="age">
|
||||
${_("a multireddit for")} ${thing_timestamp(thing.multi)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
<td class="prefright">
|
||||
${checkbox(_("allow subreddits to show me custom styles"), "show_stylesheets")}
|
||||
<br/>
|
||||
${checkbox(_("show left sidebar"), "show_left_bar")}
|
||||
<br/>
|
||||
${checkbox(_("show user flair"), "show_flair")}
|
||||
<br/>
|
||||
${checkbox(_("show link flair"), "show_link_flair")}
|
||||
|
||||
Reference in New Issue
Block a user