Large commit to add support for browsing the inbox

This commit is contained in:
Michael Lazar
2019-02-27 02:04:45 -05:00
parent 3f7c9410a6
commit 7a71023a40
34 changed files with 23150 additions and 363 deletions

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
import os
import sys
import time
@@ -43,7 +44,6 @@ class Page(object):
FOOTER = None
def __init__(self, reddit, term, config, oauth):
self.reddit = reddit
self.term = term
self.config = config
@@ -54,6 +54,7 @@ class Page(object):
self.copy_to_clipboard = copy
self.active = True
self.selected_page = None
self._row = 0
self._subwindows = None
@@ -64,6 +65,9 @@ class Page(object):
raise NotImplementedError
def get_selected_item(self):
"""
Return the content dictionary that is currently selected by the cursor.
"""
return self.content.get(self.nav.absolute_index)
def loop(self):
@@ -72,34 +76,91 @@ class Page(object):
1. Re-draw the screen
2. Wait for user to press a key (includes terminal resizing)
3. Trigger the method registered to the input key
4. Check if there are any nested pages that need to be looped over
The loop will run until self.active is set to False from within one of
the methods.
"""
self.active = True
# This needs to be called once before the main loop, in case a subpage
# was pre-selected before the loop started. This happens in __main__.py
# with ``page.open_submission(url=url)``
while self.selected_page and self.active:
self.handle_selected_page()
while self.active:
self.draw()
ch = self.term.stdscr.getch()
self.controller.trigger(ch)
while self.selected_page and self.active:
self.handle_selected_page()
return self.selected_page
def handle_selected_page(self):
"""
Some commands will result in an action that causes a new page to open.
Examples include selecting a submission, viewing subscribed subreddits,
or opening the user's inbox. With these commands, the newly selected
page will be pre-loaded and stored in ``self.selected_page`` variable.
It's up to each page type to determine what to do when another page is
selected.
- It can start a nested page.loop(). This would allow the user to
return to their previous screen after exiting the sub-page. For
example, this is what happens when opening an individual submission
from within a subreddit page. When the submission is closed, the
user resumes the subreddit that they were previously viewing.
- It can close the current self.loop() and bubble the selected page up
one level in the loop stack. For example, this is what happens when
the user opens their subscriptions and selects a subreddit. The
subscription page loop is closed and the selected subreddit is
bubbled up to the root level loop.
Care should be taken to ensure the user can never enter an infinite
nested loop, as this could lead to memory leaks and recursion errors.
# Example of an unsafe nested loop
subreddit_page.loop()
-> submission_page.loop()
-> subreddit_page.loop()
-> submission_page.loop()
...
"""
raise NotImplementedError
@PageController.register(Command('REFRESH'))
def reload_page(self):
"""
Clear the PRAW cache to force the page the re-fetch content from reddit.
"""
self.reddit.handler.clear_cache()
self.refresh_content()
@PageController.register(Command('EXIT'))
def exit(self):
"""
Prompt and exit the application.
"""
if self.term.prompt_y_or_n('Do you really want to quit? (y/n): '):
sys.exit()
@PageController.register(Command('FORCE_EXIT'))
def force_exit(self):
"""
Immediately exit the application.
"""
sys.exit()
@PageController.register(Command('PREVIOUS_THEME'))
def previous_theme(self):
"""
Cycle to preview the previous theme from the internal list of themes.
"""
theme = self.term.theme_list.previous(self.term.theme)
while not self.term.check_theme(theme):
theme = self.term.theme_list.previous(theme)
@@ -111,7 +172,9 @@ class Page(object):
@PageController.register(Command('NEXT_THEME'))
def next_theme(self):
"""
Cycle to preview the next theme from the internal list of themes.
"""
theme = self.term.theme_list.next(self.term.theme)
while not self.term.check_theme(theme):
theme = self.term.theme_list.next(theme)
@@ -123,36 +186,57 @@ class Page(object):
@PageController.register(Command('HELP'))
def show_help(self):
"""
Open the help documentation in the system pager.
"""
self.term.open_pager(docs.HELP.strip())
@PageController.register(Command('MOVE_UP'))
def move_cursor_up(self):
"""
Move the cursor up one selection.
"""
self._move_cursor(-1)
self.clear_input_queue()
@PageController.register(Command('MOVE_DOWN'))
def move_cursor_down(self):
"""
Move the cursor down one selection.
"""
self._move_cursor(1)
self.clear_input_queue()
@PageController.register(Command('PAGE_UP'))
def move_page_up(self):
"""
Move the cursor up approximately the number of entries on the page.
"""
self._move_page(-1)
self.clear_input_queue()
@PageController.register(Command('PAGE_DOWN'))
def move_page_down(self):
"""
Move the cursor down approximately the number of entries on the page.
"""
self._move_page(1)
self.clear_input_queue()
@PageController.register(Command('PAGE_TOP'))
def move_page_top(self):
"""
Move the cursor to the first item on the page.
"""
self.nav.page_index = self.content.range[0]
self.nav.cursor_index = 0
self.nav.inverted = False
@PageController.register(Command('PAGE_BOTTOM'))
def move_page_bottom(self):
"""
Move the cursor to the last item on the page.
"""
self.nav.page_index = self.content.range[1]
self.nav.cursor_index = 0
self.nav.inverted = True
@@ -160,6 +244,9 @@ class Page(object):
@PageController.register(Command('UPVOTE'))
@logged_in
def upvote(self):
"""
Upvote the currently selected item.
"""
data = self.get_selected_item()
if 'likes' not in data:
self.term.flash()
@@ -179,6 +266,9 @@ class Page(object):
@PageController.register(Command('DOWNVOTE'))
@logged_in
def downvote(self):
"""
Downvote the currently selected item.
"""
data = self.get_selected_item()
if 'likes' not in data:
self.term.flash()
@@ -198,6 +288,9 @@ class Page(object):
@PageController.register(Command('SAVE'))
@logged_in
def save(self):
"""
Mark the currently selected item as saved through the reddit API.
"""
data = self.get_selected_item()
if 'saved' not in data:
self.term.flash()
@@ -218,7 +311,6 @@ class Page(object):
Prompt to log into the user's account, or log out of the current
account.
"""
if self.reddit.is_oauth_session():
ch = self.term.show_notification('Log out? (y/n)')
if ch in (ord('y'), ord('Y')):
@@ -227,13 +319,64 @@ class Page(object):
else:
self.oauth.authorize()
def reply(self):
"""
Reply to the selected item. This is a utility method and should not
be bound to a key directly.
Item type:
Submission - add a top level comment
Comment - add a comment reply
Message - reply to a private message
"""
data = self.get_selected_item()
if data['type'] == 'Submission':
body = data['text']
description = 'submission'
reply = data['object'].add_comment
elif data['type'] in ('Comment', 'InboxComment'):
body = data['body']
description = 'comment'
reply = data['object'].reply
elif data['type'] == 'Message':
body = data['body']
description = 'private message'
reply = data['object'].reply
else:
self.term.flash()
return
# Construct the text that will be displayed in the editor file.
# The post body will be commented out and added for reference
lines = [' |' + line for line in body.split('\n')]
content = '\n'.join(lines)
comment_info = docs.REPLY_FILE.format(
author=data['author'],
type=description,
content=content)
with self.term.open_editor(comment_info) as comment:
if not comment:
self.term.show_notification('Canceled')
return
with self.term.loader('Posting {}'.format(description), delay=0):
reply(comment)
# Give reddit time to process the submission
time.sleep(2.0)
if self.term.loader.exception is None:
self.reload_page()
else:
raise TemporaryFileError()
@PageController.register(Command('DELETE'))
@logged_in
def delete_item(self):
"""
Delete a submission or comment.
"""
data = self.get_selected_item()
if data.get('author') != self.reddit.user.name:
self.term.flash()
@@ -248,6 +391,7 @@ class Page(object):
data['object'].delete()
# Give reddit time to process the request
time.sleep(2.0)
if self.term.loader.exception is None:
self.reload_page()
@@ -257,7 +401,6 @@ class Page(object):
"""
Edit a submission or comment.
"""
data = self.get_selected_item()
if data.get('author') != self.reddit.user.name:
self.term.flash()
@@ -289,20 +432,52 @@ class Page(object):
else:
raise TemporaryFileError()
@PageController.register(Command('INBOX'))
@PageController.register(Command('PRIVATE_MESSAGE'))
@logged_in
def get_inbox(self):
def send_private_message(self):
"""
Checks the inbox for unread messages and displays a notification.
Send a new private message to another user.
"""
message_info = docs.MESSAGE_FILE
with self.term.open_editor(message_info) as text:
if not text:
self.term.show_notification('Canceled')
return
with self.term.loader('Loading'):
messages = self.reddit.get_unread(limit=1)
inbox = len(list(messages))
parts = text.split('\n', 2)
if len(parts) == 1:
self.term.show_notification('Missing message subject')
return
elif len(parts) == 2:
self.term.show_notification('Missing message body')
return
if self.term.loader.exception is None:
message = 'New Messages' if inbox > 0 else 'No New Messages'
self.term.show_notification(message)
recipient, subject, message = parts
recipient = recipient.strip()
subject = subject.strip()
message = message.rstrip()
if not recipient:
self.term.show_notification('Missing recipient')
return
elif not subject:
self.term.show_notification('Missing message subject')
return
elif not message:
self.term.show_notification('Missing message body')
return
with self.term.loader('Sending message', delay=0):
self.reddit.send_message(
recipient, subject, message, raise_captcha_exception=True)
# Give reddit time to process the message
time.sleep(2.0)
if self.term.loader.exception:
raise TemporaryFileError()
else:
self.term.show_notification('Message sent!')
self.selected_page = self.open_inbox_page('sent')
def prompt_and_select_link(self):
"""
@@ -340,31 +515,23 @@ class Page(object):
@PageController.register(Command('COPY_PERMALINK'))
def copy_permalink(self):
"""
Copies submission permalink to OS clipboard
Copy the submission permalink to OS clipboard
"""
data = self.get_selected_item()
url = data.get('permalink')
if url is None:
self.term.flash()
return
try:
self.copy_to_clipboard(url)
except (ProgramError, OSError) as e:
_logger.exception(e)
self.term.show_notification(
'Failed to copy permalink: {0}'.format(e))
else:
self.term.show_notification(
'Copied permalink to clipboard', timeout=1)
url = self.get_selected_item().get('permalink')
self.copy_to_clipboard(url)
@PageController.register(Command('COPY_URL'))
def copy_url(self):
"""
Copies link to OS clipboard
Copy a link to OS clipboard
"""
url = self.prompt_and_select_link()
self.copy_to_clipboard(url)
def copy_to_clipboard(self, url):
"""
Attempt to copy the selected URL to the user's clipboard
"""
if url is None:
self.term.flash()
return
@@ -379,11 +546,104 @@ class Page(object):
self.term.show_notification(
['Copied to clipboard:', url], timeout=1)
@PageController.register(Command('SUBSCRIPTIONS'))
@logged_in
def subscriptions(self):
"""
View a list of the user's subscribed subreddits
"""
self.selected_page = self.open_subscription_page('subreddit')
@PageController.register(Command('MULTIREDDITS'))
@logged_in
def multireddits(self):
"""
View a list of the user's subscribed multireddits
"""
self.selected_page = self.open_subscription_page('multireddit')
@PageController.register(Command('PROMPT'))
def prompt(self):
"""
Open a prompt to navigate to a different subreddit or comment"
"""
name = self.term.prompt_input('Enter page: /')
if name:
# Check if opening a submission url or a subreddit url
# Example patterns for submissions:
# comments/571dw3
# /comments/571dw3
# /r/pics/comments/571dw3/
# https://www.reddit.com/r/pics/comments/571dw3/at_disneyland
submission_pattern = re.compile(r'(^|/)comments/(?P<id>.+?)($|/)')
match = submission_pattern.search(name)
if match:
url = 'https://www.reddit.com/comments/{0}'.format(match.group('id'))
self.selected_page = self.open_submission_page(url)
else:
self.selected_page = self.open_subreddit_page(name)
@PageController.register(Command('INBOX'))
@logged_in
def inbox(self):
"""
View the user's inbox.
"""
self.selected_page = self.open_inbox_page('all')
def open_inbox_page(self, content_type):
"""
Open an instance of the inbox page for the logged in user.
"""
from .inbox_page import InboxPage
with self.term.loader('Loading inbox'):
page = InboxPage(self.reddit, self.term, self.config, self.oauth,
content_type=content_type)
if not self.term.loader.exception:
return page
def open_subscription_page(self, content_type):
"""
Open an instance of the subscriptions page with the selected content.
"""
from .subscription_page import SubscriptionPage
with self.term.loader('Loading {0}s'.format(content_type)):
page = SubscriptionPage(self.reddit, self.term, self.config,
self.oauth, content_type=content_type)
if not self.term.loader.exception:
return page
def open_submission_page(self, url=None, submission=None):
"""
Open an instance of the submission page for the given submission URL.
"""
from .submission_page import SubmissionPage
with self.term.loader('Loading submission'):
page = SubmissionPage(self.reddit, self.term, self.config,
self.oauth, url=url, submission=submission)
if not self.term.loader.exception:
return page
def open_subreddit_page(self, name):
"""
Open an instance of the subreddit page for the given subreddit name.
"""
from .subreddit_page import SubredditPage
with self.term.loader('Loading subreddit'):
page = SubredditPage(self.reddit, self.term, self.config,
self.oauth, name)
if not self.term.loader.exception:
return page
def clear_input_queue(self):
"""
Clear excessive input caused by the scroll wheel or holding down a key
"""
with self.term.no_delay():
while self.term.getch() != -1:
continue