mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-27 20:11:14 +00:00
Feed templates
This commit is contained in:
parent
932acc961f
commit
b53ef73faf
13 changed files with 234 additions and 196 deletions
|
@ -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() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block content %}
|
||||
{% extends 'feed/feed_layout.html' %}
|
||||
{% block panel %}
|
||||
|
||||
<div class="block">
|
||||
<h1 class="title">Direct Messages</h1>
|
39
bookwyrm/templates/feed/feed.html
Normal file
39
bookwyrm/templates/feed/feed.html
Normal 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 %}
|
|
@ -3,6 +3,7 @@
|
|||
{% block content %}
|
||||
|
||||
<div class="columns">
|
||||
{% if user.is_authenticated %}
|
||||
<div class="column is-one-third">
|
||||
<h2 class="title is-5">Your books</h2>
|
||||
{% if not suggested_books %}
|
||||
|
@ -69,43 +70,15 @@
|
|||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="column is-two-thirds" id="feed">
|
||||
<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 %}
|
||||
{% block panel %}{% endblock %}
|
||||
|
||||
{% if activities %}
|
||||
{% include 'snippets/pagination.html' with page=activities path='/'|add:tab anchor="#feed" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
13
bookwyrm/templates/feed/status.html
Normal file
13
bookwyrm/templates/feed/status.html
Normal 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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -56,9 +56,11 @@ urlpatterns = [
|
|||
# landing pages
|
||||
re_path(r'^about/?$', views.About.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'^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()),
|
||||
|
||||
# search
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
156
bookwyrm/views/feed.py
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in a new issue