mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-26 01:50:34 +00:00
Merge pull request #1630 from joachimesque/add-feed-filters
Add feed filters
This commit is contained in:
commit
cb2a890c3b
11 changed files with 286 additions and 29 deletions
|
@ -22,6 +22,11 @@ class ActivityStream(RedisStore):
|
||||||
stream_id = self.stream_id(user)
|
stream_id = self.stream_id(user)
|
||||||
return f"{stream_id}-unread"
|
return f"{stream_id}-unread"
|
||||||
|
|
||||||
|
def unread_by_status_type_id(self, user):
|
||||||
|
"""the redis key for this user's unread count for this stream"""
|
||||||
|
stream_id = self.stream_id(user)
|
||||||
|
return f"{stream_id}-unread-by-type"
|
||||||
|
|
||||||
def get_rank(self, obj): # pylint: disable=no-self-use
|
def get_rank(self, obj): # pylint: disable=no-self-use
|
||||||
"""statuses are sorted by date published"""
|
"""statuses are sorted by date published"""
|
||||||
return obj.published_date.timestamp()
|
return obj.published_date.timestamp()
|
||||||
|
@ -35,6 +40,10 @@ class ActivityStream(RedisStore):
|
||||||
for user in self.get_audience(status):
|
for user in self.get_audience(status):
|
||||||
# add to the unread status count
|
# add to the unread status count
|
||||||
pipeline.incr(self.unread_id(user))
|
pipeline.incr(self.unread_id(user))
|
||||||
|
# add to the unread status count for status type
|
||||||
|
pipeline.hincrby(
|
||||||
|
self.unread_by_status_type_id(user), get_status_type(status), 1
|
||||||
|
)
|
||||||
|
|
||||||
# and go!
|
# and go!
|
||||||
pipeline.execute()
|
pipeline.execute()
|
||||||
|
@ -55,6 +64,7 @@ class ActivityStream(RedisStore):
|
||||||
"""load the statuses to be displayed"""
|
"""load the statuses to be displayed"""
|
||||||
# clear unreads for this feed
|
# clear unreads for this feed
|
||||||
r.set(self.unread_id(user), 0)
|
r.set(self.unread_id(user), 0)
|
||||||
|
r.delete(self.unread_by_status_type_id(user))
|
||||||
|
|
||||||
statuses = self.get_store(self.stream_id(user))
|
statuses = self.get_store(self.stream_id(user))
|
||||||
return (
|
return (
|
||||||
|
@ -75,6 +85,14 @@ class ActivityStream(RedisStore):
|
||||||
"""get the unread status count for this user's feed"""
|
"""get the unread status count for this user's feed"""
|
||||||
return int(r.get(self.unread_id(user)) or 0)
|
return int(r.get(self.unread_id(user)) or 0)
|
||||||
|
|
||||||
|
def get_unread_count_by_status_type(self, user):
|
||||||
|
"""get the unread status count for this user's feed's status types"""
|
||||||
|
status_types = r.hgetall(self.unread_by_status_type_id(user))
|
||||||
|
return {
|
||||||
|
str(key.decode("utf-8")): int(value) or 0
|
||||||
|
for key, value in status_types.items()
|
||||||
|
}
|
||||||
|
|
||||||
def populate_streams(self, user):
|
def populate_streams(self, user):
|
||||||
"""go from zero to a timeline"""
|
"""go from zero to a timeline"""
|
||||||
self.populate_store(self.stream_id(user))
|
self.populate_store(self.stream_id(user))
|
||||||
|
@ -460,7 +478,7 @@ def remove_status_task(status_ids):
|
||||||
@app.task(queue=HIGH)
|
@app.task(queue=HIGH)
|
||||||
def add_status_task(status_id, increment_unread=False):
|
def add_status_task(status_id, increment_unread=False):
|
||||||
"""add a status to any stream it should be in"""
|
"""add a status to any stream it should be in"""
|
||||||
status = models.Status.objects.get(id=status_id)
|
status = models.Status.objects.select_subclasses().get(id=status_id)
|
||||||
# we don't want to tick the unread count for csv import statuses, idk how better
|
# we don't want to tick the unread count for csv import statuses, idk how better
|
||||||
# to check than just to see if the states is more than a few days old
|
# to check than just to see if the states is more than a few days old
|
||||||
if status.created_date < timezone.now() - timedelta(days=2):
|
if status.created_date < timezone.now() - timedelta(days=2):
|
||||||
|
@ -507,3 +525,20 @@ def handle_boost_task(boost_id):
|
||||||
stream.remove_object_from_related_stores(boosted, stores=audience)
|
stream.remove_object_from_related_stores(boosted, stores=audience)
|
||||||
for status in old_versions:
|
for status in old_versions:
|
||||||
stream.remove_object_from_related_stores(status, stores=audience)
|
stream.remove_object_from_related_stores(status, stores=audience)
|
||||||
|
|
||||||
|
|
||||||
|
def get_status_type(status):
|
||||||
|
"""return status type even for boosted statuses"""
|
||||||
|
status_type = status.status_type.lower()
|
||||||
|
|
||||||
|
# Check if current status is a boost
|
||||||
|
if hasattr(status, "boost"):
|
||||||
|
# Act in accordance of your findings
|
||||||
|
if hasattr(status.boost.boosted_status, "review"):
|
||||||
|
status_type = "review"
|
||||||
|
if hasattr(status.boost.boosted_status, "comment"):
|
||||||
|
status_type = "comment"
|
||||||
|
if hasattr(status.boost.boosted_status, "quotation"):
|
||||||
|
status_type = "quotation"
|
||||||
|
|
||||||
|
return status_type
|
||||||
|
|
|
@ -9,6 +9,7 @@ from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
|
from bookwyrm.models.user import FeedFilterChoices
|
||||||
|
|
||||||
|
|
||||||
class CustomForm(ModelForm):
|
class CustomForm(ModelForm):
|
||||||
|
@ -174,6 +175,18 @@ class UserGroupForm(CustomForm):
|
||||||
fields = ["groups"]
|
fields = ["groups"]
|
||||||
|
|
||||||
|
|
||||||
|
class FeedStatusTypesForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.User
|
||||||
|
fields = ["feed_status_types"]
|
||||||
|
help_texts = {f: None for f in fields}
|
||||||
|
widgets = {
|
||||||
|
"feed_status_types": widgets.CheckboxSelectMultiple(
|
||||||
|
choices=FeedFilterChoices,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CoverForm(CustomForm):
|
class CoverForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Book
|
model = models.Book
|
||||||
|
|
32
bookwyrm/migrations/0119_user_feed_status_types.py
Normal file
32
bookwyrm/migrations/0119_user_feed_status_types.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 3.2.5 on 2021-11-24 10:15
|
||||||
|
|
||||||
|
import bookwyrm.models.user
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0118_alter_user_preferred_language"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="feed_status_types",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("review", "Reviews"),
|
||||||
|
("comment", "Comments"),
|
||||||
|
("quotation", "Quotations"),
|
||||||
|
("everything", "Everything else"),
|
||||||
|
],
|
||||||
|
max_length=10,
|
||||||
|
),
|
||||||
|
default=bookwyrm.models.user.get_feed_filter_choices,
|
||||||
|
size=8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -4,11 +4,12 @@ from urllib.parse import urlparse
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib.auth.models import AbstractUser, Group
|
from django.contrib.auth.models import AbstractUser, Group
|
||||||
from django.contrib.postgres.fields import CICharField
|
from django.contrib.postgres.fields import ArrayField, CICharField
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from model_utils import FieldTracker
|
from model_utils import FieldTracker
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
|
@ -27,6 +28,19 @@ from .federated_server import FederatedServer
|
||||||
from . import fields, Review
|
from . import fields, Review
|
||||||
|
|
||||||
|
|
||||||
|
FeedFilterChoices = [
|
||||||
|
("review", _("Reviews")),
|
||||||
|
("comment", _("Comments")),
|
||||||
|
("quotation", _("Quotations")),
|
||||||
|
("everything", _("Everything else")),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_feed_filter_choices():
|
||||||
|
"""return a list of filter choice keys"""
|
||||||
|
return [f[0] for f in FeedFilterChoices]
|
||||||
|
|
||||||
|
|
||||||
def site_link():
|
def site_link():
|
||||||
"""helper for generating links to the site"""
|
"""helper for generating links to the site"""
|
||||||
protocol = "https" if USE_HTTPS else "http"
|
protocol = "https" if USE_HTTPS else "http"
|
||||||
|
@ -128,6 +142,13 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
show_suggested_users = models.BooleanField(default=True)
|
show_suggested_users = models.BooleanField(default=True)
|
||||||
discoverable = fields.BooleanField(default=False)
|
discoverable = fields.BooleanField(default=False)
|
||||||
|
|
||||||
|
# feed options
|
||||||
|
feed_status_types = ArrayField(
|
||||||
|
models.CharField(max_length=10, blank=False, choices=FeedFilterChoices),
|
||||||
|
size=8,
|
||||||
|
default=get_feed_filter_choices,
|
||||||
|
)
|
||||||
|
|
||||||
preferred_timezone = models.CharField(
|
preferred_timezone = models.CharField(
|
||||||
choices=[(str(tz), str(tz)) for tz in pytz.all_timezones],
|
choices=[(str(tz), str(tz)) for tz in pytz.all_timezones],
|
||||||
default=str(pytz.utc),
|
default=str(pytz.utc),
|
||||||
|
|
|
@ -120,9 +120,44 @@ let BookWyrm = new class {
|
||||||
* @return {undefined}
|
* @return {undefined}
|
||||||
*/
|
*/
|
||||||
updateCountElement(counter, data) {
|
updateCountElement(counter, data) {
|
||||||
|
let count = data.count;
|
||||||
|
const count_by_type = data.count_by_type;
|
||||||
const currentCount = counter.innerText;
|
const currentCount = counter.innerText;
|
||||||
const count = data.count;
|
|
||||||
const hasMentions = data.has_mentions;
|
const hasMentions = data.has_mentions;
|
||||||
|
const allowedStatusTypesEl = document.getElementById('unread-notifications-wrapper');
|
||||||
|
|
||||||
|
// If we're on the right counter element
|
||||||
|
if (counter.closest('[data-poll-wrapper]').contains(allowedStatusTypesEl)) {
|
||||||
|
const allowedStatusTypes = JSON.parse(allowedStatusTypesEl.textContent);
|
||||||
|
|
||||||
|
// For keys in common between allowedStatusTypes and count_by_type
|
||||||
|
// This concerns 'review', 'quotation', 'comment'
|
||||||
|
count = allowedStatusTypes.reduce(function(prev, currentKey) {
|
||||||
|
const currentValue = count_by_type[currentKey] | 0;
|
||||||
|
|
||||||
|
return prev + currentValue;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Add all the "other" in count_by_type if 'everything' is allowed
|
||||||
|
if (allowedStatusTypes.includes('everything')) {
|
||||||
|
// Clone count_by_type with 0 for reviews/quotations/comments
|
||||||
|
const count_by_everything_else = Object.assign(
|
||||||
|
{},
|
||||||
|
count_by_type,
|
||||||
|
{review: 0, quotation: 0, comment: 0}
|
||||||
|
);
|
||||||
|
|
||||||
|
count = Object.keys(count_by_everything_else).reduce(
|
||||||
|
function(prev, currentKey) {
|
||||||
|
const currentValue =
|
||||||
|
count_by_everything_else[currentKey] | 0
|
||||||
|
|
||||||
|
return prev + currentValue;
|
||||||
|
},
|
||||||
|
count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (count != currentCount) {
|
if (count != currentCount) {
|
||||||
this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-hidden', count < 1);
|
this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-hidden', count < 1);
|
||||||
|
|
|
@ -16,10 +16,45 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# feed settings #}
|
||||||
|
<details class="mb-5" {% if settings_saved %}open{% endif %}>
|
||||||
|
<summary>
|
||||||
|
<span class="has-text-weight-bold">
|
||||||
|
{{ _("Feed settings") }}
|
||||||
|
</span>
|
||||||
|
{% if settings_saved %}
|
||||||
|
<span class="tag is-success is-light ml-2">{{ _("Saved!") }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</summary>
|
||||||
|
<form class="level is-align-items-flex-end" method="post" action="/{{ tab.key }}#feed">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<label class="label mt-2 mb-1">Status types</label>
|
||||||
|
{% for name, value in feed_status_types_options %}
|
||||||
|
<label class="mr-2">
|
||||||
|
<input type="checkbox" name="feed_status_types" value="{{ name }}" {% if name in user.feed_status_types %}checked=""{% endif %}/>
|
||||||
|
{{ value }}
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="level-right control">
|
||||||
|
<button class="button is-small is-primary is-outlined" type="submit">
|
||||||
|
{{ _("Save settings") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
|
||||||
{# announcements and system messages #}
|
{# announcements and system messages #}
|
||||||
{% if not activities.number > 1 %}
|
{% if not activities.number > 1 %}
|
||||||
<a href="{{ request.path }}" class="transition-y is-hidden notification is-primary is-block" data-poll-wrapper>
|
<a href="{{ request.path }}" class="transition-y is-hidden notification is-primary is-block" data-poll-wrapper>
|
||||||
{% blocktrans with tab_key=tab.key %}load <span data-poll="stream/{{ tab_key }}">0</span> unread status(es){% endblocktrans %}
|
{% blocktrans with tab_key=tab.key %}load <span data-poll="stream/{{ tab_key }}">0</span> unread status(es){% endblocktrans %}
|
||||||
|
{{ allowed_status_types|json_script:"unread-notifications-wrapper" }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% if request.user.show_goal and not goal and tab.key == 'home' %}
|
{% if request.user.show_goal and not goal and tab.key == 'home' %}
|
||||||
|
@ -36,6 +71,7 @@
|
||||||
{% if not activities %}
|
{% if not activities %}
|
||||||
<div class="block content">
|
<div class="block content">
|
||||||
<p>{% trans "There aren't any activities right now! Try following a user to get started" %}</p>
|
<p>{% trans "There aren't any activities right now! Try following a user to get started" %}</p>
|
||||||
|
<p>{% if user.feed_status_types|length < 4 %}{% trans "Alternatively, you can try enabling more status types" %}{% endif %}</p>
|
||||||
|
|
||||||
{% if request.user.show_suggested_users and suggested_users %}
|
{% if request.user.show_suggested_users and suggested_users %}
|
||||||
{# suggested users for when things are very lonely #}
|
{# suggested users for when things are very lonely #}
|
||||||
|
|
|
@ -50,10 +50,17 @@ class UpdateViews(TestCase):
|
||||||
request = self.factory.get("")
|
request = self.factory.get("")
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
|
|
||||||
with patch("bookwyrm.activitystreams.ActivityStream.get_unread_count") as mock:
|
with patch(
|
||||||
mock.return_value = 3
|
"bookwyrm.activitystreams.ActivityStream.get_unread_count"
|
||||||
|
) as mock_count:
|
||||||
|
with patch(
|
||||||
|
"bookwyrm.activitystreams.ActivityStream.get_unread_count_by_status_type"
|
||||||
|
) as mock_count_by_status:
|
||||||
|
mock_count.return_value = 3
|
||||||
|
mock_count_by_status.return_value = {"review": 5}
|
||||||
result = views.get_unread_status_count(request, "home")
|
result = views.get_unread_status_count(request, "home")
|
||||||
|
|
||||||
self.assertIsInstance(result, JsonResponse)
|
self.assertIsInstance(result, JsonResponse)
|
||||||
data = json.loads(result.getvalue())
|
data = json.loads(result.getvalue())
|
||||||
self.assertEqual(data["count"], 3)
|
self.assertEqual(data["count"], 3)
|
||||||
|
self.assertEqual(data["count_by_type"]["review"], 5)
|
||||||
|
|
|
@ -10,10 +10,11 @@ from django.utils.decorators import method_decorator
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
from bookwyrm import activitystreams, forms, models
|
from bookwyrm import activitystreams, forms, models
|
||||||
|
from bookwyrm.models.user import FeedFilterChoices
|
||||||
from bookwyrm.activitypub import ActivitypubResponse
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
from bookwyrm.settings import PAGE_LENGTH, STREAMS
|
from bookwyrm.settings import PAGE_LENGTH, STREAMS
|
||||||
from bookwyrm.suggested_users import suggested_users
|
from bookwyrm.suggested_users import suggested_users
|
||||||
from .helpers import get_user_from_username
|
from .helpers import filter_stream_by_status_type, get_user_from_username
|
||||||
from .helpers import is_api_request, is_bookwyrm_request
|
from .helpers import is_api_request, is_bookwyrm_request
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,7 +23,17 @@ from .helpers import is_api_request, is_bookwyrm_request
|
||||||
class Feed(View):
|
class Feed(View):
|
||||||
"""activity stream"""
|
"""activity stream"""
|
||||||
|
|
||||||
def get(self, request, tab):
|
def post(self, request, tab):
|
||||||
|
"""save feed settings form, with a silent validation fail"""
|
||||||
|
settings_saved = False
|
||||||
|
form = forms.FeedStatusTypesForm(request.POST, instance=request.user)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
settings_saved = True
|
||||||
|
|
||||||
|
return self.get(request, tab, settings_saved)
|
||||||
|
|
||||||
|
def get(self, request, tab, settings_saved=False):
|
||||||
"""user's homepage with activity feed"""
|
"""user's homepage with activity feed"""
|
||||||
tab = [s for s in STREAMS if s["key"] == tab]
|
tab = [s for s in STREAMS if s["key"] == tab]
|
||||||
tab = tab[0] if tab else STREAMS[0]
|
tab = tab[0] if tab else STREAMS[0]
|
||||||
|
@ -30,7 +41,11 @@ class Feed(View):
|
||||||
activities = activitystreams.streams[tab["key"]].get_activity_stream(
|
activities = activitystreams.streams[tab["key"]].get_activity_stream(
|
||||||
request.user
|
request.user
|
||||||
)
|
)
|
||||||
paginated = Paginator(activities, PAGE_LENGTH)
|
filtered_activities = filter_stream_by_status_type(
|
||||||
|
activities,
|
||||||
|
allowed_types=request.user.feed_status_types,
|
||||||
|
)
|
||||||
|
paginated = Paginator(filtered_activities, PAGE_LENGTH)
|
||||||
|
|
||||||
suggestions = suggested_users.get_suggestions(request.user)
|
suggestions = suggested_users.get_suggestions(request.user)
|
||||||
|
|
||||||
|
@ -43,6 +58,9 @@ class Feed(View):
|
||||||
"tab": tab,
|
"tab": tab,
|
||||||
"streams": STREAMS,
|
"streams": STREAMS,
|
||||||
"goal_form": forms.GoalForm(),
|
"goal_form": forms.GoalForm(),
|
||||||
|
"feed_status_types_options": FeedFilterChoices,
|
||||||
|
"allowed_status_types": request.user.feed_status_types,
|
||||||
|
"settings_saved": settings_saved,
|
||||||
"path": f"/{tab['key']}",
|
"path": f"/{tab['key']}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import dateutil.tz
|
||||||
from dateutil.parser import ParserError
|
from dateutil.parser import ParserError
|
||||||
|
|
||||||
from requests import HTTPError
|
from requests import HTTPError
|
||||||
|
from django.db.models import Q
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
|
|
||||||
|
@ -153,3 +154,29 @@ def set_language(user, response):
|
||||||
translation.activate(user.preferred_language)
|
translation.activate(user.preferred_language)
|
||||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user.preferred_language)
|
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user.preferred_language)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def filter_stream_by_status_type(activities, allowed_types=None):
|
||||||
|
"""filter out activities based on types"""
|
||||||
|
if not allowed_types:
|
||||||
|
allowed_types = []
|
||||||
|
|
||||||
|
if "review" not in allowed_types:
|
||||||
|
activities = activities.filter(
|
||||||
|
Q(review__isnull=True), Q(boost__boosted_status__review__isnull=True)
|
||||||
|
)
|
||||||
|
if "comment" not in allowed_types:
|
||||||
|
activities = activities.filter(
|
||||||
|
Q(comment__isnull=True), Q(boost__boosted_status__comment__isnull=True)
|
||||||
|
)
|
||||||
|
if "quotation" not in allowed_types:
|
||||||
|
activities = activities.filter(
|
||||||
|
Q(quotation__isnull=True), Q(boost__boosted_status__quotation__isnull=True)
|
||||||
|
)
|
||||||
|
if "everything" not in allowed_types:
|
||||||
|
activities = activities.filter(
|
||||||
|
Q(generatednote__isnull=True),
|
||||||
|
Q(boost__boosted_status__generatednote__isnull=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
return activities
|
||||||
|
|
|
@ -22,4 +22,9 @@ def get_unread_status_count(request, stream="home"):
|
||||||
stream = activitystreams.streams.get(stream)
|
stream = activitystreams.streams.get(stream)
|
||||||
if not stream:
|
if not stream:
|
||||||
return JsonResponse({})
|
return JsonResponse({})
|
||||||
return JsonResponse({"count": stream.get_unread_count(request.user)})
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"count": stream.get_unread_count(request.user),
|
||||||
|
"count_by_type": stream.get_unread_count_by_status_type(request.user),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -18,58 +18,58 @@ msgstr ""
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
#: bookwyrm/forms.py:248
|
#: bookwyrm/forms.py:262
|
||||||
msgid "A user with this email already exists."
|
msgid "A user with this email already exists."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/forms.py:262
|
#: bookwyrm/forms.py:276
|
||||||
msgid "One Day"
|
msgid "One Day"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/forms.py:263
|
#: bookwyrm/forms.py:277
|
||||||
msgid "One Week"
|
msgid "One Week"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/forms.py:264
|
#: bookwyrm/forms.py:278
|
||||||
msgid "One Month"
|
msgid "One Month"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/forms.py:265
|
#: bookwyrm/forms.py:279
|
||||||
msgid "Does Not Expire"
|
msgid "Does Not Expire"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/forms.py:269
|
#: bookwyrm/forms.py:283
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{i} uses"
|
msgid "{i} uses"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/forms.py:270
|
#: bookwyrm/forms.py:284
|
||||||
msgid "Unlimited"
|
msgid "Unlimited"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/forms.py:338
|
#: bookwyrm/forms.py:352
|
||||||
msgid "List Order"
|
msgid "List Order"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/forms.py:339
|
#: bookwyrm/forms.py:353
|
||||||
msgid "Book Title"
|
msgid "Book Title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/forms.py:340 bookwyrm/templates/shelf/shelf.html:149
|
#: bookwyrm/forms.py:354 bookwyrm/templates/shelf/shelf.html:149
|
||||||
#: bookwyrm/templates/shelf/shelf.html:181
|
#: bookwyrm/templates/shelf/shelf.html:181
|
||||||
#: bookwyrm/templates/snippets/create_status/review.html:33
|
#: bookwyrm/templates/snippets/create_status/review.html:33
|
||||||
msgid "Rating"
|
msgid "Rating"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/forms.py:342 bookwyrm/templates/lists/list.html:110
|
#: bookwyrm/forms.py:356 bookwyrm/templates/lists/list.html:110
|
||||||
msgid "Sort By"
|
msgid "Sort By"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/forms.py:346
|
#: bookwyrm/forms.py:360
|
||||||
msgid "Ascending"
|
msgid "Ascending"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/forms.py:347
|
#: bookwyrm/forms.py:361
|
||||||
msgid "Descending"
|
msgid "Descending"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -153,6 +153,22 @@ msgstr ""
|
||||||
msgid "A user with that username already exists."
|
msgid "A user with that username already exists."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/models/user.py:32 bookwyrm/templates/book/book.html:218
|
||||||
|
msgid "Reviews"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/models/user.py:33
|
||||||
|
msgid "Comments"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/models/user.py:34
|
||||||
|
msgid "Quotations"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/models/user.py:35
|
||||||
|
msgid "Everything else"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/settings.py:118
|
#: bookwyrm/settings.py:118
|
||||||
msgid "Home Timeline"
|
msgid "Home Timeline"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -449,10 +465,6 @@ msgstr ""
|
||||||
msgid "You don't have any reading activity for this book."
|
msgid "You don't have any reading activity for this book."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/templates/book/book.html:218
|
|
||||||
msgid "Reviews"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: bookwyrm/templates/book/book.html:223
|
#: bookwyrm/templates/book/book.html:223
|
||||||
msgid "Your reviews"
|
msgid "Your reviews"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1082,15 +1094,31 @@ msgstr ""
|
||||||
msgid "You have no messages right now."
|
msgid "You have no messages right now."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/templates/feed/feed.html:22
|
#: bookwyrm/templates/feed/feed.html:23
|
||||||
|
msgid "Feed settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/templates/feed/feed.html:26
|
||||||
|
msgid "Saved!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/templates/feed/feed.html:47
|
||||||
|
msgid "Save settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/templates/feed/feed.html:56
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "load <span data-poll=\"stream/%(tab_key)s\">0</span> unread status(es)"
|
msgid "load <span data-poll=\"stream/%(tab_key)s\">0</span> unread status(es)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/templates/feed/feed.html:38
|
#: bookwyrm/templates/feed/feed.html:72
|
||||||
msgid "There aren't any activities right now! Try following a user to get started"
|
msgid "There aren't any activities right now! Try following a user to get started"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/templates/feed/feed.html:73
|
||||||
|
msgid "Alternatively, you can try enabling more status types"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/templates/feed/goal_card.html:6
|
#: bookwyrm/templates/feed/goal_card.html:6
|
||||||
#: bookwyrm/templates/feed/layout.html:90
|
#: bookwyrm/templates/feed/layout.html:90
|
||||||
#: bookwyrm/templates/user/goal_form.html:6
|
#: bookwyrm/templates/user/goal_form.html:6
|
||||||
|
|
Loading…
Reference in a new issue