Feed templates

This commit is contained in:
Mouse Reeve 2021-01-29 10:25:31 -08:00
parent 932acc961f
commit b53ef73faf
13 changed files with 234 additions and 196 deletions

View file

@ -35,8 +35,17 @@ window.onload = function() {
// polling // polling
document.querySelectorAll('[data-poll]') document.querySelectorAll('[data-poll]')
.forEach(el => polling(el)); .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) { function polling(el) {
let delay = 10000 + (Math.random() * 1000); let delay = 10000 + (Math.random() * 1000);
setTimeout(function() { setTimeout(function() {

View file

@ -1,5 +1,5 @@
{% extends 'layout.html' %} {% extends 'feed/feed_layout.html' %}
{% block content %} {% block panel %}
<div class="block"> <div class="block">
<h1 class="title">Direct Messages</h1> <h1 class="title">Direct Messages</h1>

View file

@ -0,0 +1,39 @@
{% extends 'feed/feed_layout.html' %}
{% load bookwyrm_tags %}
{% block panel %}
<h1 class="title">{{ tab | title }} Timeline</h1>
<div class="tabs">
<ul>
<li class="{% if tab == 'home' %}is-active{% endif %}">
<a href="/#feed">Home</a>
</li>
<li class="{% if tab == 'local' %}is-active{% endif %}">
<a href="/local#feed">Local</a>
</li>
<li class="{% if tab == 'federated' %}is-active{% endif %}">
<a href="/federated#feed">Federated</a>
</li>
</ul>
</div>
{# announcements and system messages #}
{% if not goal and tab == 'home' %}
{% now 'Y' as year %}
<section class="block hidden" aria-title="Announcements" data-hide="hide-{{ year }}-reading-goal">
{% include 'snippets/goal_card.html' with year=year %}
<hr>
</section>
{% endif %}
{# activity feed #}
{% if not activities %}
<p>There aren't any activities right now! Try following a user to get started</p>
{% endif %}
{% for activity in activities %}
<div class="block">
{% include 'snippets/status.html' with status=activity %}
</div>
{% endfor %}
{% endblock %}

View file

@ -3,6 +3,7 @@
{% block content %} {% block content %}
<div class="columns"> <div class="columns">
{% if user.is_authenticated %}
<div class="column is-one-third"> <div class="column is-one-third">
<h2 class="title is-5">Your books</h2> <h2 class="title is-5">Your books</h2>
{% if not suggested_books %} {% if not suggested_books %}
@ -69,43 +70,15 @@
</section> </section>
{% endif %} {% endif %}
</div> </div>
{% endif %}
<div class="column is-two-thirds" id="feed"> <div class="column is-two-thirds" id="feed">
<h1 class="title">{{ tab | title }} Timeline</h1> {% block panel %}{% endblock %}
<div class="tabs">
<ul>
<li class="{% if tab == 'home' %}is-active{% endif %}">
<a href="/#feed">Home</a>
</li>
<li class="{% if tab == 'local' %}is-active{% endif %}">
<a href="/local#feed">Local</a>
</li>
<li class="{% if tab == 'federated' %}is-active{% endif %}">
<a href="/federated#feed">Federated</a>
</li>
</ul>
</div>
{# announcements and system messages #}
{% if not goal and tab == 'home' %}
{% now 'Y' as year %}
<section class="block hidden" aria-title="Announcements" data-hide="hide-{{ year }}-reading-goal">
{% include 'snippets/goal_card.html' with year=year %}
<hr>
</section>
{% endif %}
{# activity feed #}
{% if not activities %}
<p>There aren't any activities right now! Try following a user to get started</p>
{% endif %}
{% for activity in activities %}
<div class="block">
{% include 'snippets/status.html' with status=activity %}
</div>
{% endfor %}
{% if activities %}
{% include 'snippets/pagination.html' with page=activities path='/'|add:tab anchor="#feed" %} {% include 'snippets/pagination.html' with page=activities path='/'|add:tab anchor="#feed" %}
{% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,13 @@
{% extends 'feed/feed_layout.html' %}
{% block panel %}
<header class="block">
<a href="/#feed" class="button" data-back>
<span class="icon icon-arrow-left" aira-hidden="true"></span>
<span>Back</span>
</a>
</header>
{% include 'feed/thread.html' with status=status depth=0 max_depth=6 is_root=True direction=0 %}
{% endblock %}

View file

@ -1,9 +0,0 @@
{% extends 'layout.html' %}
{% block content %}
<div class="block">
{% include 'snippets/thread.html' with status=status depth=0 max_depth=6 is_root=True direction=0 %}
</div>
{% endblock %}

View file

@ -56,9 +56,11 @@ urlpatterns = [
# landing pages # landing pages
re_path(r'^about/?$', views.About.as_view()), re_path(r'^about/?$', views.About.as_view()),
path('', views.Home.as_view()), path('', views.Home.as_view()),
re_path(r'^(?P<tab>home|local|federated)/?$', views.Feed.as_view()),
re_path(r'^discover/?$', views.Discover.as_view()), re_path(r'^discover/?$', views.Discover.as_view()),
re_path(r'^notifications/?$', views.Notifications.as_view()), re_path(r'^notifications/?$', views.Notifications.as_view()),
# feeds
re_path(r'^(?P<tab>home|local|federated)/?$', views.Feed.as_view()),
re_path(r'^direct-messages/?$', views.DirectMessage.as_view()), re_path(r'^direct-messages/?$', views.DirectMessage.as_view()),
# search # search

View file

@ -4,15 +4,15 @@ from .author import Author, EditAuthor
from .block import Block, unblock from .block import Block, unblock
from .books import Book, EditBook, Editions from .books import Book, EditBook, Editions
from .books import upload_cover, add_description, switch_edition, resolve_book 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 .error import not_found_page, server_error_page
from .feed import DirectMessage, Feed, Replies, Status
from .follow import follow, unfollow from .follow import follow, unfollow
from .follow import accept_follow_request, delete_follow_request, handle_accept from .follow import accept_follow_request, delete_follow_request, handle_accept
from .goal import Goal from .goal import Goal
from .import_data import Import, ImportStatus from .import_data import Import, ImportStatus
from .interaction import Favorite, Unfavorite, Boost, Unboost from .interaction import Favorite, Unfavorite, Boost, Unboost
from .invite import ManageInvites, Invite from .invite import ManageInvites, Invite
from .landing import About, Home, Feed, Discover from .landing import About, Home, Discover
from .notifications import Notifications from .notifications import Notifications
from .outbox import Outbox from .outbox import Outbox
from .reading import edit_readthrough, create_readthrough, delete_readthrough from .reading import edit_readthrough, create_readthrough, delete_readthrough
@ -24,6 +24,6 @@ from .search import Search
from .shelf import Shelf from .shelf import Shelf
from .shelf import user_shelves_page, create_shelf, delete_shelf from .shelf import user_shelves_page, create_shelf, delete_shelf
from .shelf import shelve, unshelve from .shelf import shelve, unshelve
from .status import Status, Replies, CreateStatus, DeleteStatus from .status import CreateStatus, DeleteStatus
from .updates import Updates from .updates import Updates
from .user import User, EditUser, Followers, Following from .user import User, EditUser, Followers, Following

View file

@ -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)

156
bookwyrm/views/feed.py Normal file
View file

@ -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

View file

@ -1,14 +1,10 @@
''' non-interactive pages ''' ''' 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.db.models import Avg, Max
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from bookwyrm import forms, models from bookwyrm import forms, models
from bookwyrm.settings import PAGE_LENGTH from .feed import Feed
from .helpers import get_activity_feed from .helpers import get_activity_feed
@ -61,68 +57,3 @@ class Discover(View):
'ratings': ratings 'ratings': ratings
} }
return TemplateResponse(request, 'discover.html', data) 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

View file

@ -1,55 +1,22 @@
''' what are we here for if not for posting ''' ''' what are we here for if not for posting '''
import re import re
from django.contrib.auth.decorators import login_required 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.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from markdown import markdown from markdown import markdown
from bookwyrm import forms, models from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.broadcast import broadcast from bookwyrm.broadcast import broadcast
from bookwyrm.sanitize_html import InputHtmlParser from bookwyrm.sanitize_html import InputHtmlParser
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
from bookwyrm.status import create_notification, delete_status from bookwyrm.status import create_notification, delete_status
from bookwyrm.utils import regex from bookwyrm.utils import regex
from .helpers import get_user_from_username, handle_remote_webfinger from .helpers import handle_remote_webfinger
from .helpers import is_api_request, is_bookworm_request, object_visible_to_user
# pylint: disable= no-self-use # 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') @method_decorator(login_required, name='dispatch')
class CreateStatus(View): class CreateStatus(View):
''' the view for *posting* ''' ''' the view for *posting* '''
@ -144,23 +111,6 @@ class DeleteStatus(View):
broadcast(request.user, status.to_delete_activity(request.user)) broadcast(request.user, status.to_delete_activity(request.user))
return redirect(request.headers.get('Referer', '/')) 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): def find_mentions(content):
''' detect @mentions in raw status content ''' ''' detect @mentions in raw status content '''
for match in re.finditer(regex.strict_username, content): for match in re.finditer(regex.strict_username, content):