Skip to content

Commit 08cf498

Browse files
committed
Inbox counts: Add mark all messages as read
Conflicts: r2/r2/lib/js.py
1 parent f3f739a commit 08cf498

File tree

12 files changed

+194
-42
lines changed

12 files changed

+194
-42
lines changed

install-reddit.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,7 @@ function set_consumer_count {
613613
set_consumer_count log_q 0
614614
set_consumer_count cloudsearch_q 0
615615
set_consumer_count scraper_q 1
616+
set_consumer_count markread_q 1
616617
set_consumer_count commentstree_q 1
617618
set_consumer_count newcomments_q 1
618619
set_consumer_count vote_link_q 1

r2/r2/config/queues.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def declare_queues(g):
8383
"log_q": MessageQueue(bind_to_self=True),
8484
"cloudsearch_changes": MessageQueue(bind_to_self=True),
8585
"butler_q": MessageQueue(),
86+
"markread_q": MessageQueue(),
8687
})
8788

8889
if g.shard_link_vote_queues:
@@ -102,4 +103,6 @@ def declare_queues(g):
102103
queues.newcomments_q << "new_comment"
103104
queues.butler_q << ("new_comment",
104105
"usertext_edited")
106+
queues.markread_q << "mark_all_read"
107+
105108
return queues

r2/r2/controllers/api.py

Lines changed: 24 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2689,62 +2689,47 @@ def POST_collapse_message(self, things):
26892689
def POST_uncollapse_message(self, things):
26902690
self.collapse_handler(things, False)
26912691

2692-
def unread_handler(self, things, unread):
2692+
@require_oauth2_scope("privatemessages")
2693+
@noresponse(VUser(),
2694+
VModhash(),
2695+
things = VByName('id', multiple=True, limit=25))
2696+
@api_doc(api_section.messages)
2697+
def POST_unread_message(self, things):
26932698
if not things:
26942699
if (errors.TOO_MANY_THING_IDS, 'id') in c.errors:
26952700
return abort(413)
26962701
else:
26972702
return abort(400)
26982703

2699-
sr_messages = defaultdict(list)
2700-
comments = []
2701-
messages = []
2702-
# Group things by subreddit or type
2703-
for thing in things:
2704-
if isinstance(thing, Message):
2705-
if getattr(thing, 'sr_id', False):
2706-
sr_messages[thing.sr_id].append(thing)
2707-
else:
2708-
messages.append(thing)
2709-
else:
2710-
comments.append(thing)
2711-
2712-
if sr_messages:
2713-
mod_srs = Subreddit.reverse_moderator_ids(c.user)
2714-
srs = Subreddit._byID(sr_messages.keys())
2715-
else:
2716-
mod_srs = []
2717-
2718-
# Batch set items as unread
2719-
for sr_id, things in sr_messages.items():
2720-
# Remove the item(s) from the user's inbox
2721-
queries.set_unread(things, c.user, unread)
2722-
if sr_id in mod_srs:
2723-
# Only moderators can change the read status of that
2724-
# message in the modmail inbox
2725-
sr = srs[sr_id]
2726-
queries.set_unread(things, sr, unread)
2727-
if comments:
2728-
queries.set_unread(comments, c.user, unread)
2729-
if messages:
2730-
queries.set_unread(messages, c.user, unread)
2731-
2704+
queries.unread_handler(things, c.user, unread=True)
27322705

27332706
@require_oauth2_scope("privatemessages")
27342707
@noresponse(VUser(),
27352708
VModhash(),
27362709
things = VByName('id', multiple=True, limit=25))
27372710
@api_doc(api_section.messages)
2738-
def POST_unread_message(self, things):
2739-
self.unread_handler(things, True)
2711+
def POST_read_message(self, things):
2712+
if not things:
2713+
if (errors.TOO_MANY_THING_IDS, 'id') in c.errors:
2714+
return abort(413)
2715+
else:
2716+
return abort(400)
2717+
2718+
queries.unread_handler(things, c.user, unread=False)
27402719

27412720
@require_oauth2_scope("privatemessages")
27422721
@noresponse(VUser(),
27432722
VModhash(),
2744-
things = VByName('id', multiple=True, limit=25))
2723+
VRatelimit(rate_user=True, prefix="rate_read_all_"))
27452724
@api_doc(api_section.messages)
2746-
def POST_read_message(self, things):
2747-
self.unread_handler(things, False)
2725+
def POST_read_all_messages(self):
2726+
"""Queue up marking all messages for a user as read.
2727+
2728+
This may take some time, and returns 202 to acknowledge acceptance of
2729+
the request.
2730+
"""
2731+
amqp.add_item('mark_all_read', c.user._fullname)
2732+
return abort(202)
27482733

27492734
@require_oauth2_scope("report")
27502735
@noresponse(VUser(),

r2/r2/controllers/listingcontroller.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,6 +1137,9 @@ def GET_listing(self, where, mark, message, subwhere = None, **env):
11371137
self.mark = 'true'
11381138
if c.user_is_admin:
11391139
c.referrer_policy = "always"
1140+
if self.where == 'unread':
1141+
self.next_suggestions_cls = UnreadMessagesSuggestions
1142+
11401143
return ListingController.GET_listing(self, **env)
11411144

11421145
@validate(

r2/r2/lib/db/queries.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
UserQueryCache,
4444
)
4545
from r2.models.last_modified import LastModified
46-
from r2.lib.utils import SimpleSillyStub
46+
from r2.lib.utils import in_chunks, SimpleSillyStub
4747

4848
import cPickle as pickle
4949

@@ -1166,6 +1166,42 @@ def set_unread(messages, to, unread, mutator=None):
11661166
m.send()
11671167

11681168

1169+
def unread_handler(things, user, unread):
1170+
"""Given a user and Things of varying types, set their unread state."""
1171+
sr_messages = collections.defaultdict(list)
1172+
comments = []
1173+
messages = []
1174+
# Group things by subreddit or type
1175+
for thing in things:
1176+
if isinstance(thing, Message):
1177+
if getattr(thing, 'sr_id', False):
1178+
sr_messages[thing.sr_id].append(thing)
1179+
else:
1180+
messages.append(thing)
1181+
else:
1182+
comments.append(thing)
1183+
1184+
if sr_messages:
1185+
mod_srs = Subreddit.reverse_moderator_ids(user)
1186+
srs = Subreddit._byID(sr_messages.keys())
1187+
else:
1188+
mod_srs = []
1189+
1190+
# Batch set items as unread
1191+
for sr_id, things in sr_messages.items():
1192+
# Remove the item(s) from the user's inbox
1193+
set_unread(things, user, unread)
1194+
if sr_id in mod_srs:
1195+
# Only moderators can change the read status of that
1196+
# message in the modmail inbox
1197+
sr = srs[sr_id]
1198+
set_unread(things, sr, unread)
1199+
if comments:
1200+
set_unread(comments, user, unread)
1201+
if messages:
1202+
set_unread(messages, user, unread)
1203+
1204+
11691205
def unnotify(thing, possible_recipients=None):
11701206
"""Given a Thing, remove any notifications to possible recipients for it.
11711207
@@ -1762,3 +1798,15 @@ def _handle_vote(msg):
17621798
timer.flush()
17631799

17641800
amqp.consume_items(qname, _handle_vote, verbose = False)
1801+
1802+
1803+
def consume_mark_all_read():
1804+
@g.stats.amqp_processor('markread_q')
1805+
def process_mark_all_read(msg):
1806+
user = Account._by_fullname(msg.body)
1807+
inbox_fullnames = get_unread_inbox(user)
1808+
for inbox_chunk in in_chunks(inbox_fullnames, size=100):
1809+
things = Thing._by_fullname(inbox_chunk, return_dict=False)
1810+
unread_handler(things, user, unread=False)
1811+
1812+
amqp.consume_items('markread_q', process_mark_all_read)

r2/r2/lib/js.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,7 @@ def use(self):
473473
"recommender.js",
474474
"report.js",
475475
"saved.js",
476+
"messages.js",
476477
PermissionsDataSource({
477478
"moderator": ModeratorPermissionSet,
478479
"moderator_invite": ModeratorPermissionSet,

r2/r2/lib/pages/pages.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4723,6 +4723,11 @@ def __init__(self):
47234723
self.suggestion_type = "random"
47244724

47254725

4726+
class UnreadMessagesSuggestions(Templated):
4727+
"""Let a user mark all as read if they have > 1 page of unread messages."""
4728+
pass
4729+
4730+
47264731
class ExploreItem(Templated):
47274732
"""For managing recommended content."""
47284733

r2/r2/public/static/css/reddit.less

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
box-shadow: inset 0 1px 1px 1px fadeout(black, 90%);
5353
}
5454

55-
&:disabled {
55+
&:disabled, &.disabled {
5656
.linear-gradient(#e9edf1, #dce3ea) !important;
5757
color: #999999 !important;
5858
text-shadow: 0 1px 0 lighten(#dce3ea, 15%) !important;
@@ -1248,7 +1248,7 @@ body.with-listing-chooser.explore-page #header .pagename {
12481248
}
12491249

12501250
.next-suggestions {
1251-
margin-left: 1.5em;
1251+
margin-left: 0.75em;
12521252

12531253
a {
12541254
background: none;
@@ -1257,6 +1257,17 @@ body.with-listing-chooser.explore-page #header .pagename {
12571257
}
12581258
}
12591259

1260+
.next-suggestions .mark-all-read-container .throbber {
1261+
position: absolute;
1262+
margin-left: 5px;
1263+
margin-top: -2px;
1264+
padding-left: 22px;
1265+
min-width: 18px;
1266+
width: auto;
1267+
font-size: 10px;
1268+
line-height: 16px;
1269+
}
1270+
12601271
/* corner help */
12611272
.help a.help {
12621273
color: #808080;

r2/r2/public/static/js/base.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ $(function() {
122122
r.multi.init()
123123
r.recommend.init()
124124
r.saved.init()
125+
r.messages.init()
125126
r.resAdvisory.init()
126127
r.filter.init()
127128
} catch (err) {

r2/r2/public/static/js/messages.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
r.messages = {}
2+
3+
/**
4+
* After triggering a mark-as-read request, poll our account to see if our messages have been cleared.
5+
* If our messages have not been cleared after a number of iterations, it's likely we had a
6+
* message race or other issue, redirect back to stop polling and potentially display the new message.
7+
*/
8+
r.messages.pollUnread = _.debounce(function(count) {
9+
count = count + 1 || 1;
10+
11+
// Took too long, redirect in case of issue.
12+
if (count > 20) {
13+
document.location = "/message/unread";
14+
return;
15+
}
16+
17+
r.ajax({
18+
type: 'GET',
19+
url: '/api/me.json',
20+
success: function(response) {
21+
if (!response['data']['has_mail']) {
22+
document.location = "/message/unread";
23+
} else {
24+
r.messages.pollUnread(count);
25+
}
26+
},
27+
});
28+
}, 2000);
29+
30+
r.messages.init = function() {
31+
$('a.mark-all-read').on('click', function(e) {
32+
var $this = $(this);
33+
34+
e.preventDefault();
35+
e.stopPropagation();
36+
37+
if ($this.hasClass('disabled')) {
38+
return;
39+
}
40+
41+
$this.addClass('disabled');
42+
$this.parent().addClass('working');
43+
44+
r.ajax({
45+
type: 'POST',
46+
url: '/api/read_all_messages',
47+
data: {},
48+
success: r.messages.pollUnread,
49+
});
50+
});
51+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
## The contents of this file are subject to the Common Public Attribution
2+
## License Version 1.0. (the "License"); you may not use this file except in
3+
## compliance with the License. You may obtain a copy of the License at
4+
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
5+
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
6+
## software over a computer network and provide for limited attribution for the
7+
## Original Developer. In addition, Exhibit A has been modified to be
8+
## consistent with Exhibit B.
9+
##
10+
## Software distributed under the License is distributed on an "AS IS" basis,
11+
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
12+
## the specific language governing rights and limitations under the License.
13+
##
14+
## The Original Code is reddit.
15+
##
16+
## The Original Developer is the Initial Developer. The Initial Developer of
17+
## the Original Code is reddit Inc.
18+
##
19+
## All portions of the code written by reddit are Copyright (c) 2006-2014
20+
## reddit Inc. All Rights Reserved.
21+
###############################################################################
22+
23+
<span class="next-suggestions">
24+
${_('or')}
25+
<span class="mark-all-read-container">
26+
<a class="basic-button mark-all-read" href="#">${_('mark all as read')}</a>
27+
<span class="throbber">${_('(this may take a while)')}</span>
28+
</span>
29+
</span>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
description "mark all messages as read for a user"
2+
3+
instance $x
4+
5+
stop on reddit-stop or runlevel [016]
6+
7+
respawn
8+
respawn limit 10 5
9+
10+
nice 10
11+
script
12+
. /etc/default/reddit
13+
wrap-job paster run --proctitle markread_q$x $REDDIT_INI $REDDIT_ROOT/r2/lib/db/queries.py -c 'consume_mark_all_read()'
14+
end script

0 commit comments

Comments
 (0)