diff --git a/bookwyrm/static/js/shared.js b/bookwyrm/static/js/shared.js index a0c21bec6..c2fa8b541 100644 --- a/bookwyrm/static/js/shared.js +++ b/bookwyrm/static/js/shared.js @@ -35,8 +35,17 @@ window.onload = function() { // polling document.querySelectorAll('[data-poll]') .forEach(el => polling(el)); + + // browser back behavior + document.querySelectorAll('[data-back]') + .forEach(t => t.onclick = back); }; +function back(e) { + e.preventDefault(); + history.back(); +} + function polling(el) { let delay = 10000 + (Math.random() * 1000); setTimeout(function() { diff --git a/bookwyrm/templates/direct_messages.html b/bookwyrm/templates/feed/direct_messages.html similarity index 88% rename from bookwyrm/templates/direct_messages.html rename to bookwyrm/templates/feed/direct_messages.html index 666f52908..44a0cded7 100644 --- a/bookwyrm/templates/direct_messages.html +++ b/bookwyrm/templates/feed/direct_messages.html @@ -1,5 +1,5 @@ -{% extends 'layout.html' %} -{% block content %} +{% extends 'feed/feed_layout.html' %} +{% block panel %}

Direct Messages

diff --git a/bookwyrm/templates/feed/feed.html b/bookwyrm/templates/feed/feed.html new file mode 100644 index 000000000..8d6152e2c --- /dev/null +++ b/bookwyrm/templates/feed/feed.html @@ -0,0 +1,39 @@ +{% extends 'feed/feed_layout.html' %} +{% load bookwyrm_tags %} +{% block panel %} + +

{{ tab | title }} Timeline

+
+ +
+ +{# announcements and system messages #} +{% if not goal and tab == 'home' %} +{% now 'Y' as year %} + +{% endif %} + +{# activity feed #} +{% if not activities %} +

There aren't any activities right now! Try following a user to get started

+{% endif %} +{% for activity in activities %} +
+{% include 'snippets/status.html' with status=activity %} +
+{% endfor %} + +{% endblock %} diff --git a/bookwyrm/templates/feed.html b/bookwyrm/templates/feed/feed_layout.html similarity index 73% rename from bookwyrm/templates/feed.html rename to bookwyrm/templates/feed/feed_layout.html index 1368660bc..33123ca80 100644 --- a/bookwyrm/templates/feed.html +++ b/bookwyrm/templates/feed/feed_layout.html @@ -3,6 +3,7 @@ {% block content %}
+ {% if user.is_authenticated %}

Your books

{% if not suggested_books %} @@ -69,43 +70,15 @@ {% endif %}
+ {% endif %}
-

{{ tab | title }} Timeline

-
- -
- - {# announcements and system messages #} - {% if not goal and tab == 'home' %} - {% now 'Y' as year %} - - {% endif %} - - {# activity feed #} - {% if not activities %} -

There aren't any activities right now! Try following a user to get started

- {% endif %} - {% for activity in activities %} -
- {% include 'snippets/status.html' with status=activity %} -
- {% endfor %} + {% block panel %}{% endblock %} + {% if activities %} {% include 'snippets/pagination.html' with page=activities path='/'|add:tab anchor="#feed" %} + {% endif %}
{% endblock %} + diff --git a/bookwyrm/templates/feed/status.html b/bookwyrm/templates/feed/status.html new file mode 100644 index 000000000..eb8489934 --- /dev/null +++ b/bookwyrm/templates/feed/status.html @@ -0,0 +1,13 @@ +{% extends 'feed/feed_layout.html' %} +{% block panel %} +
+ + + Back + +
+ +{% include 'feed/thread.html' with status=status depth=0 max_depth=6 is_root=True direction=0 %} + +{% endblock %} + diff --git a/bookwyrm/templates/snippets/thread.html b/bookwyrm/templates/feed/thread.html similarity index 100% rename from bookwyrm/templates/snippets/thread.html rename to bookwyrm/templates/feed/thread.html diff --git a/bookwyrm/templates/status.html b/bookwyrm/templates/status.html deleted file mode 100644 index d38ed89ee..000000000 --- a/bookwyrm/templates/status.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends 'layout.html' %} -{% block content %} - -
- {% include 'snippets/thread.html' with status=status depth=0 max_depth=6 is_root=True direction=0 %} -
- -{% endblock %} - diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index e3b792fff..d232747f9 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -56,9 +56,11 @@ urlpatterns = [ # landing pages re_path(r'^about/?$', views.About.as_view()), path('', views.Home.as_view()), - re_path(r'^(?Phome|local|federated)/?$', views.Feed.as_view()), re_path(r'^discover/?$', views.Discover.as_view()), re_path(r'^notifications/?$', views.Notifications.as_view()), + + # feeds + re_path(r'^(?Phome|local|federated)/?$', views.Feed.as_view()), re_path(r'^direct-messages/?$', views.DirectMessage.as_view()), # search diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 36a4bed9a..80520eb38 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -4,15 +4,15 @@ from .author import Author, EditAuthor from .block import Block, unblock from .books import Book, EditBook, Editions from .books import upload_cover, add_description, switch_edition, resolve_book -from .direct_message import DirectMessage from .error import not_found_page, server_error_page +from .feed import DirectMessage, Feed, Replies, Status from .follow import follow, unfollow from .follow import accept_follow_request, delete_follow_request, handle_accept from .goal import Goal from .import_data import Import, ImportStatus from .interaction import Favorite, Unfavorite, Boost, Unboost from .invite import ManageInvites, Invite -from .landing import About, Home, Feed, Discover +from .landing import About, Home, Discover from .notifications import Notifications from .outbox import Outbox from .reading import edit_readthrough, create_readthrough, delete_readthrough @@ -24,6 +24,6 @@ from .search import Search from .shelf import Shelf from .shelf import user_shelves_page, create_shelf, delete_shelf from .shelf import shelve, unshelve -from .status import Status, Replies, CreateStatus, DeleteStatus +from .status import CreateStatus, DeleteStatus from .updates import Updates from .user import User, EditUser, Followers, Following diff --git a/bookwyrm/views/direct_message.py b/bookwyrm/views/direct_message.py deleted file mode 100644 index 1f6c4f192..000000000 --- a/bookwyrm/views/direct_message.py +++ /dev/null @@ -1,26 +0,0 @@ -''' non-interactive pages ''' -from django.contrib.auth.decorators import login_required -from django.core.paginator import Paginator -from django.template.response import TemplateResponse -from django.utils.decorators import method_decorator -from django.views import View - -from bookwyrm.settings import PAGE_LENGTH -from .helpers import get_activity_feed - - -# pylint: disable= no-self-use -@method_decorator(login_required, name='dispatch') -class DirectMessage(View): - ''' dm view ''' - def get(self, request, page=1): - ''' like a feed but for dms only ''' - activities = get_activity_feed(request.user, 'direct') - paginated = Paginator(activities, PAGE_LENGTH) - activity_page = paginated.page(page) - data = { - 'title': 'Direct Messages', - 'user': request.user, - 'activities': activity_page, - } - return TemplateResponse(request, 'direct_messages.html', data) diff --git a/bookwyrm/views/feed.py b/bookwyrm/views/feed.py new file mode 100644 index 000000000..931cf355c --- /dev/null +++ b/bookwyrm/views/feed.py @@ -0,0 +1,156 @@ +''' non-interactive pages ''' +from django.contrib.auth.decorators import login_required +from django.core.paginator import Paginator +from django.http import HttpResponseNotFound +from django.template.response import TemplateResponse +from django.utils import timezone +from django.utils.decorators import method_decorator +from django.views import View + +from bookwyrm import forms, models +from bookwyrm.activitypub import ActivitypubResponse +from bookwyrm.settings import PAGE_LENGTH +from .helpers import get_activity_feed +from .helpers import get_user_from_username +from .helpers import is_api_request, is_bookworm_request, object_visible_to_user + + +# pylint: disable= no-self-use +@method_decorator(login_required, name='dispatch') +class Feed(View): + ''' activity stream ''' + def get(self, request, tab): + ''' user's homepage with activity feed ''' + try: + page = int(request.GET.get('page', 1)) + except ValueError: + page = 1 + + if tab == 'home': + activities = get_activity_feed( + request.user, ['public', 'unlisted', 'followers'], + following_only=True) + elif tab == 'local': + activities = get_activity_feed( + request.user, ['public', 'followers'], local_only=True) + else: + activities = get_activity_feed( + request.user, ['public', 'followers']) + paginated = Paginator(activities, PAGE_LENGTH) + + data = {**feed_page_data(request.user), **{ + 'title': 'Updates Feed', + 'user': request.user, + 'activities': paginated.page(page), + 'tab': tab, + 'goal_form': forms.GoalForm(), + }} + return TemplateResponse(request, 'feed/feed.html', data) + + +@method_decorator(login_required, name='dispatch') +class DirectMessage(View): + ''' dm view ''' + def get(self, request): + ''' like a feed but for dms only ''' + try: + page = int(request.GET.get('page', 1)) + except ValueError: + page = 1 + + activities = get_activity_feed(request.user, 'direct') + paginated = Paginator(activities, PAGE_LENGTH) + activity_page = paginated.page(page) + data = {**feed_page_data(request.user), **{ + 'title': 'Direct Messages', + 'user': request.user, + 'activities': activity_page, + }} + return TemplateResponse(request, 'feed/direct_messages.html', data) + + +class Status(View): + ''' get posting ''' + def get(self, request, username, status_id): + ''' display a particular status (and replies, etc) ''' + try: + user = get_user_from_username(username) + status = models.Status.objects.select_subclasses().get( + id=status_id, deleted=False) + except ValueError: + return HttpResponseNotFound() + + # the url should have the poster's username in it + if user != status.user: + return HttpResponseNotFound() + + # make sure the user is authorized to see the status + if not object_visible_to_user(request.user, status): + return HttpResponseNotFound() + + if is_api_request(request): + return ActivitypubResponse( + status.to_activity(pure=not is_bookworm_request(request))) + + data = {**feed_page_data(request.user), **{ + 'title': 'Status by %s' % user.username, + 'status': status, + }} + return TemplateResponse(request, 'feed/status.html', data) + + +class Replies(View): + ''' replies page (a json view of status) ''' + def get(self, request, username, status_id): + ''' ordered collection of replies to a status ''' + # the html view is the same as Status + if not is_api_request(request): + status_view = Status.as_view() + return status_view(request, username, status_id) + + # the json view is different than Status + status = models.Status.objects.get(id=status_id) + if status.user.localname != username: + return HttpResponseNotFound() + + return ActivitypubResponse(status.to_replies(**request.GET)) + + +def feed_page_data(user): + ''' info we need for every feed page ''' + if not user.is_authenticated: + return {} + + goal = models.AnnualGoal.objects.filter( + user=user, year=timezone.now().year + ).first() + return { + 'suggested_books': get_suggested_books(user), + 'goal': goal, + 'goal_form': forms.GoalForm(), + } + +def get_suggested_books(user, max_books=5): + ''' helper to get a user's recent books ''' + book_count = 0 + preset_shelves = [ + ('reading', max_books), ('read', 2), ('to-read', max_books) + ] + suggested_books = [] + for (preset, shelf_max) in preset_shelves: + limit = shelf_max if shelf_max < (max_books - book_count) \ + else max_books - book_count + shelf = user.shelf_set.get(identifier=preset) + + shelf_books = shelf.shelfbook_set.order_by( + '-updated_date' + ).all()[:limit] + if not shelf_books: + continue + shelf_preview = { + 'name': shelf.name, + 'books': [s.book for s in shelf_books] + } + suggested_books.append(shelf_preview) + book_count += len(shelf_preview['books']) + return suggested_books diff --git a/bookwyrm/views/landing.py b/bookwyrm/views/landing.py index ec6cb3a9b..0d841ef04 100644 --- a/bookwyrm/views/landing.py +++ b/bookwyrm/views/landing.py @@ -1,14 +1,10 @@ ''' non-interactive pages ''' -from django.contrib.auth.decorators import login_required -from django.core.paginator import Paginator from django.db.models import Avg, Max from django.template.response import TemplateResponse -from django.utils import timezone -from django.utils.decorators import method_decorator from django.views import View from bookwyrm import forms, models -from bookwyrm.settings import PAGE_LENGTH +from .feed import Feed from .helpers import get_activity_feed @@ -61,68 +57,3 @@ class Discover(View): 'ratings': ratings } return TemplateResponse(request, 'discover.html', data) - - -@method_decorator(login_required, name='dispatch') -class Feed(View): - ''' activity stream ''' - def get(self, request, tab): - ''' user's homepage with activity feed ''' - try: - page = int(request.GET.get('page', 1)) - except ValueError: - page = 1 - - suggested_books = get_suggested_books(request.user) - - if tab == 'home': - activities = get_activity_feed( - request.user, ['public', 'unlisted', 'followers'], - following_only=True) - elif tab == 'local': - activities = get_activity_feed( - request.user, ['public', 'followers'], local_only=True) - else: - activities = get_activity_feed( - request.user, ['public', 'followers']) - paginated = Paginator(activities, PAGE_LENGTH) - - goal = models.AnnualGoal.objects.filter( - user=request.user, year=timezone.now().year - ).first() - data = { - 'title': 'Updates Feed', - 'user': request.user, - 'suggested_books': suggested_books, - 'activities': paginated.page(page), - 'tab': tab, - 'goal': goal, - 'goal_form': forms.GoalForm(), - } - return TemplateResponse(request, 'feed.html', data) - - -def get_suggested_books(user, max_books=5): - ''' helper to get a user's recent books ''' - book_count = 0 - preset_shelves = [ - ('reading', max_books), ('read', 2), ('to-read', max_books) - ] - suggested_books = [] - for (preset, shelf_max) in preset_shelves: - limit = shelf_max if shelf_max < (max_books - book_count) \ - else max_books - book_count - shelf = user.shelf_set.get(identifier=preset) - - shelf_books = shelf.shelfbook_set.order_by( - '-updated_date' - ).all()[:limit] - if not shelf_books: - continue - shelf_preview = { - 'name': shelf.name, - 'books': [s.book for s in shelf_books] - } - suggested_books.append(shelf_preview) - book_count += len(shelf_preview['books']) - return suggested_books diff --git a/bookwyrm/views/status.py b/bookwyrm/views/status.py index 834cf5837..4d342bfbc 100644 --- a/bookwyrm/views/status.py +++ b/bookwyrm/views/status.py @@ -1,55 +1,22 @@ ''' what are we here for if not for posting ''' import re from django.contrib.auth.decorators import login_required -from django.http import HttpResponseBadRequest, HttpResponseNotFound +from django.http import HttpResponseBadRequest from django.shortcuts import get_object_or_404, redirect -from django.template.response import TemplateResponse from django.utils.decorators import method_decorator from django.views import View from markdown import markdown from bookwyrm import forms, models -from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.broadcast import broadcast from bookwyrm.sanitize_html import InputHtmlParser from bookwyrm.settings import DOMAIN from bookwyrm.status import create_notification, delete_status from bookwyrm.utils import regex -from .helpers import get_user_from_username, handle_remote_webfinger -from .helpers import is_api_request, is_bookworm_request, object_visible_to_user +from .helpers import handle_remote_webfinger # pylint: disable= no-self-use -class Status(View): - ''' get posting ''' - def get(self, request, username, status_id): - ''' display a particular status (and replies, etc) ''' - try: - user = get_user_from_username(username) - status = models.Status.objects.select_subclasses().get( - id=status_id, deleted=False) - except ValueError: - return HttpResponseNotFound() - - # the url should have the poster's username in it - if user != status.user: - return HttpResponseNotFound() - - # make sure the user is authorized to see the status - if not object_visible_to_user(request.user, status): - return HttpResponseNotFound() - - if is_api_request(request): - return ActivitypubResponse( - status.to_activity(pure=not is_bookworm_request(request))) - - data = { - 'title': 'Status by %s' % user.username, - 'status': status, - } - return TemplateResponse(request, 'status.html', data) - - @method_decorator(login_required, name='dispatch') class CreateStatus(View): ''' the view for *posting* ''' @@ -144,23 +111,6 @@ class DeleteStatus(View): broadcast(request.user, status.to_delete_activity(request.user)) return redirect(request.headers.get('Referer', '/')) - -class Replies(View): - ''' replies page (a json view of status) ''' - def get(self, request, username, status_id): - ''' ordered collection of replies to a status ''' - # the html view is the same as Status - if not is_api_request(request): - status_view = Status.as_view() - return status_view(request, username, status_id) - - # the json view is different than Status - status = models.Status.objects.get(id=status_id) - if status.user.localname != username: - return HttpResponseNotFound() - - return ActivitypubResponse(status.to_replies(**request.GET)) - def find_mentions(content): ''' detect @mentions in raw status content ''' for match in re.finditer(regex.strict_username, content):