diff --git a/bookwyrm/activitystreams.py b/bookwyrm/activitystreams.py index e1a52d26..4cba9939 100644 --- a/bookwyrm/activitystreams.py +++ b/bookwyrm/activitystreams.py @@ -22,6 +22,11 @@ class ActivityStream(RedisStore): stream_id = self.stream_id(user) 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 """statuses are sorted by date published""" return obj.published_date.timestamp() @@ -35,6 +40,10 @@ class ActivityStream(RedisStore): for user in self.get_audience(status): # add to the unread status count 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! pipeline.execute() @@ -55,6 +64,7 @@ class ActivityStream(RedisStore): """load the statuses to be displayed""" # clear unreads for this feed r.set(self.unread_id(user), 0) + r.delete(self.unread_by_status_type_id(user)) statuses = self.get_store(self.stream_id(user)) return ( @@ -75,6 +85,14 @@ class ActivityStream(RedisStore): """get the unread status count for this user's feed""" 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): """go from zero to a timeline""" self.populate_store(self.stream_id(user)) @@ -460,7 +478,7 @@ def remove_status_task(status_ids): @app.task(queue=HIGH) def add_status_task(status_id, increment_unread=False): """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 # 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): @@ -507,3 +525,20 @@ def handle_boost_task(boost_id): stream.remove_object_from_related_stores(boosted, stores=audience) for status in old_versions: 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 diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index aff1e29c..2a4759fb 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _ from bookwyrm import models from bookwyrm.models.fields import ClearableFileInputWithWarning +from bookwyrm.models.user import FeedFilterChoices class CustomForm(ModelForm): @@ -196,6 +197,18 @@ class UserGroupForm(CustomForm): 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 Meta: model = models.Book diff --git a/bookwyrm/migrations/0119_user_feed_status_types.py b/bookwyrm/migrations/0119_user_feed_status_types.py new file mode 100644 index 00000000..64fa9169 --- /dev/null +++ b/bookwyrm/migrations/0119_user_feed_status_types.py @@ -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, + ), + ), + ] diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index d7945843..4d98f5c5 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -4,11 +4,12 @@ from urllib.parse import urlparse from django.apps import apps 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.dispatch import receiver from django.db import models, transaction from django.utils import timezone +from django.utils.translation import gettext_lazy as _ from model_utils import FieldTracker import pytz @@ -27,6 +28,19 @@ from .federated_server import FederatedServer 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(): """helper for generating links to the site""" protocol = "https" if USE_HTTPS else "http" @@ -128,6 +142,13 @@ class User(OrderedCollectionPageMixin, AbstractUser): show_suggested_users = models.BooleanField(default=True) 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( choices=[(str(tz), str(tz)) for tz in pytz.all_timezones], default=str(pytz.utc), diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index 2d5b88ad..d656ed18 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -45,6 +45,13 @@ let BookWyrm = new class { 'change', this.disableIfTooLarge.bind(this) )); + + document.querySelectorAll('[data-duplicate]') + .forEach(node => node.addEventListener( + 'click', + this.duplicateInput.bind(this) + + )) } /** @@ -113,9 +120,44 @@ let BookWyrm = new class { * @return {undefined} */ updateCountElement(counter, data) { + let count = data.count; + const count_by_type = data.count_by_type; const currentCount = counter.innerText; - const count = data.count; 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) { this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-hidden', count < 1); @@ -368,4 +410,24 @@ let BookWyrm = new class { ); } } + + duplicateInput (event ) { + const trigger = event.currentTarget; + const input_id = trigger.dataset['duplicate'] + const orig = document.getElementById(input_id); + const parent = orig.parentNode; + const new_count = parent.querySelectorAll("input").length + 1 + + let input = orig.cloneNode(); + + input.id += ("-" + (new_count)) + input.value = "" + + let label = parent.querySelector("label").cloneNode(); + + label.setAttribute("for", input.id) + + parent.appendChild(label) + parent.appendChild(input) + } }(); diff --git a/bookwyrm/static/js/status_cache.js b/bookwyrm/static/js/status_cache.js index 2a50bfcb..418b7dee 100644 --- a/bookwyrm/static/js/status_cache.js +++ b/bookwyrm/static/js/status_cache.js @@ -187,6 +187,7 @@ let StatusCache = new class { .forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", false)); // Remove existing disabled states + button.querySelectorAll("[data-shelf-dropdown-identifier] button") .forEach(item => item.disabled = false); diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index 36241ee2..713e7abe 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -153,12 +153,21 @@ {# user's relationship to the book #}
+ {% if user_shelfbooks.count > 0 %} +

+ {% trans "You have shelved this edition in:" %} +

+ + {% endif %} {% for shelf in other_edition_shelves %}

{% blocktrans with book_path=shelf.book.local_path shelf_path=shelf.shelf.local_path shelf_name=shelf.shelf.name %}A different edition of this book is on your {{ shelf_name }} shelf.{% endblocktrans %} diff --git a/bookwyrm/templates/book/edit/edit_book_form.html b/bookwyrm/templates/book/edit/edit_book_form.html index feebb803..fd2516a6 100644 --- a/bookwyrm/templates/book/edit/edit_book_form.html +++ b/bookwyrm/templates/book/edit/edit_book_form.html @@ -141,11 +141,15 @@ - - - {% trans "Separate multiple values with commas." %} - + {% for author in add_author %} + + + {% empty %} + + + {% endfor %}

+ diff --git a/bookwyrm/templates/directory/directory.html b/bookwyrm/templates/directory/directory.html index 9753c4c0..c3ddb3c5 100644 --- a/bookwyrm/templates/directory/directory.html +++ b/bookwyrm/templates/directory/directory.html @@ -18,7 +18,7 @@

{% csrf_token %} - +

{% url 'prefs-profile' as path %} {% blocktrans with path=path %}You can opt-out at any time in your profile settings.{% endblocktrans %} @@ -28,7 +28,7 @@

{% trans "Dismiss message" as button_text %}
diff --git a/bookwyrm/templates/feed/feed.html b/bookwyrm/templates/feed/feed.html index a6175199..1a2488af 100644 --- a/bookwyrm/templates/feed/feed.html +++ b/bookwyrm/templates/feed/feed.html @@ -16,10 +16,45 @@ +{# feed settings #} +
+ + + {{ _("Feed settings") }} + + {% if settings_saved %} + {{ _("Saved!") }} + {% endif %} + + + {% csrf_token %} + +
+
+
+ + {% for name, value in feed_status_types_options %} + + {% endfor %} +
+
+
+
+ +
+ +
+ {# announcements and system messages #} {% if not activities.number > 1 %} {% if request.user.show_goal and not goal and tab.key == 'home' %} @@ -36,6 +71,7 @@ {% if not activities %}

{% trans "There aren't any activities right now! Try following a user to get started" %}

+

{% if user.feed_status_types|length < 4 %}{% trans "Alternatively, you can try enabling more status types" %}{% endif %}

{% if request.user.show_suggested_users and suggested_users %} {# suggested users for when things are very lonely #} diff --git a/bookwyrm/templates/get_started/book_preview.html b/bookwyrm/templates/get_started/book_preview.html index 893e7593..8a20d0d7 100644 --- a/bookwyrm/templates/get_started/book_preview.html +++ b/bookwyrm/templates/get_started/book_preview.html @@ -4,9 +4,14 @@
diff --git a/bookwyrm/templates/search/book.html b/bookwyrm/templates/search/book.html index 704f055b..66adb8c8 100644 --- a/bookwyrm/templates/search/book.html +++ b/bookwyrm/templates/search/book.html @@ -39,7 +39,7 @@
diff --git a/bookwyrm/templates/shelf/shelf.html b/bookwyrm/templates/shelf/shelf.html index 01d41aa0..0184ab1d 100644 --- a/bookwyrm/templates/shelf/shelf.html +++ b/bookwyrm/templates/shelf/shelf.html @@ -80,7 +80,10 @@

- {{ shelf.name }} + {% if shelf.identifier == 'to-read' %}{% trans "To Read" %} + {% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %} + {% elif shelf.identifier == 'read' %}{% trans "Read" %} + {% else %}{{ shelf.name }}{% endif %} {% include 'snippets/privacy-icons.html' with item=shelf %} diff --git a/bookwyrm/templates/snippets/reading_modals/finish_reading_modal.html b/bookwyrm/templates/snippets/reading_modals/finish_reading_modal.html index 3c127160..a35ed9e0 100644 --- a/bookwyrm/templates/snippets/reading_modals/finish_reading_modal.html +++ b/bookwyrm/templates/snippets/reading_modals/finish_reading_modal.html @@ -9,10 +9,11 @@ Finish "{{ book_title }}" {% endblock %} {% block modal-form-open %} -
+ {% csrf_token %} + {% endblock %} {% block reading-dates %} diff --git a/bookwyrm/templates/snippets/reading_modals/start_reading_modal.html b/bookwyrm/templates/snippets/reading_modals/start_reading_modal.html index cd0b64f3..423f77eb 100644 --- a/bookwyrm/templates/snippets/reading_modals/start_reading_modal.html +++ b/bookwyrm/templates/snippets/reading_modals/start_reading_modal.html @@ -9,8 +9,9 @@ Start "{{ book_title }}" {% endblock %} {% block modal-form-open %} - + + {% csrf_token %} {% endblock %} diff --git a/bookwyrm/templates/snippets/reading_modals/want_to_read_modal.html b/bookwyrm/templates/snippets/reading_modals/want_to_read_modal.html index d1f06d8f..2fb976bf 100644 --- a/bookwyrm/templates/snippets/reading_modals/want_to_read_modal.html +++ b/bookwyrm/templates/snippets/reading_modals/want_to_read_modal.html @@ -9,8 +9,9 @@ Want to Read "{{ book_title }}" {% endblock %} {% block modal-form-open %} - + + {% csrf_token %} {% endblock %} diff --git a/bookwyrm/templates/snippets/shelf_selector.html b/bookwyrm/templates/snippets/shelf_selector.html index ca5a39f6..3ee6fa92 100644 --- a/bookwyrm/templates/snippets/shelf_selector.html +++ b/bookwyrm/templates/snippets/shelf_selector.html @@ -1,29 +1,96 @@ {% extends 'components/dropdown.html' %} {% load i18n %} +{% load bookwyrm_tags %} +{% load utilities %} + {% block dropdown-trigger %} {% trans "Move book" %} {% endblock %} {% block dropdown-list %} +{% with book.id|uuid as uuid %} +{% active_shelf book as active_shelf %} +{% latest_read_through book request.user as readthrough %} + {% for shelf in user_shelves %} + +{% if shelf.editable %}
+{% else%} +{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %} +{% with button_class="is-fullwidth is-small shelf-option is-radiusless is-white" %} + +{% endwith %} +{% endif %} {% endfor %} - + +{% if shelf.identifier == 'all' %} +{% for shelved_in in book.shelves.all %} + +{% endfor %} +{% else %} + + +{% endif %} + +{% include 'snippets/reading_modals/want_to_read_modal.html' with book=active_shelf.book controls_text="want_to_read" controls_uid=uuid move_from=current.id refresh=True %} + +{% include 'snippets/reading_modals/start_reading_modal.html' with book=active_shelf.book controls_text="start_reading" controls_uid=uuid move_from=current.id refresh=True %} + +{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book controls_text="finish_reading" controls_uid=uuid move_from=current.id readthrough=readthrough refresh=True %} + +{% endwith %} {% endblock %} diff --git a/bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown_options.html b/bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown_options.html index 32319f86..8c1881ce 100644 --- a/bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown_options.html +++ b/bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown_options.html @@ -32,7 +32,7 @@ {% elif shelf.editable %} -
+ {% csrf_token %}