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 %}
+
+ {% include 'snippets/goal_card.html' with year=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 %}
-
- {% include 'snippets/goal_card.html' with year=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 %}
+
+
+{% 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'^(?P
home|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):