From 63a74af117962854cd045ecad8b9e25ac8b31557 Mon Sep 17 00:00:00 2001 From: Aidan Cornelius-Bell Date: Mon, 13 Jan 2025 13:32:40 +1030 Subject: [PATCH] Omnibus update for arelpe: - Rewrote the system for sending digests to users - Changed a range of user interface elements for consistency - Digests are now controllable in user profile (registrations/edit) - Tweaked the subscription form Emergent bugs: - Safari (iOS particularly) ignores styling of button text - Additional button spacing on forms ** to be fixed in next release, shortly ** --- app/assets/stylesheets/application.css | 11 ++ .../users/registrations_controller.rb | 3 +- app/jobs/weekly_bookmarks_digest_job.rb | 40 ++++++- app/mailers/digest_mailer.rb | 5 +- app/models/user.rb | 7 ++ app/views/devise/registrations/edit.html.erb | 21 ++++ .../weekly_bookmarks_digest.html.erb | 113 +++++++++++++----- app/views/mailing_lists/index.html.erb | 8 +- app/views/subscriptions/new.html.erb | 98 +++++++-------- ...3020623_add_digest_preferences_to_users.rb | 5 + db/schema.rb | 3 +- 11 files changed, 221 insertions(+), 93 deletions(-) create mode 100644 db/migrate/20250113020623_add_digest_preferences_to_users.rb diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index e8e1611..16b3c76 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -276,6 +276,17 @@ label { color: var(--standout-form-text); } +.reg-form h1, +.reg-form h2, +.reg-form h3, +.reg-form h4 { + margin-top: 0; +} + +.reg-form hr { + margin: 1em 0; +} + .subheading { margin-top: -25px; } diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 703d4f1..a614e8d 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -25,7 +25,8 @@ class Users::RegistrationsController < Devise::RegistrationsController :password, :password_confirmation, :current_password, - :hide_ai_summaries + :hide_ai_summaries, + :digest_preference ) end diff --git a/app/jobs/weekly_bookmarks_digest_job.rb b/app/jobs/weekly_bookmarks_digest_job.rb index b38746f..ea72a66 100644 --- a/app/jobs/weekly_bookmarks_digest_job.rb +++ b/app/jobs/weekly_bookmarks_digest_job.rb @@ -1,15 +1,43 @@ class WeeklyBookmarksDigestJob < ApplicationJob queue_as :default + retry_on StandardError, attempts: 3, wait: 5.minutes def perform(user = nil) - start_date = 1.week.ago.beginning_of_week - end_date = Time.current.end_of_week - bookmarks = Post.where(created_at: start_date..end_date) - # or do User.find_each do |user| for everyone, but that'll break fastmail + end_date = Time.current.end_of_day + start_date = end_date - 1.week + + # Convert the relation to an array of IDs + bookmark_ids = Post.where(created_at: start_date..end_date).pluck(:id) + + if bookmark_ids.empty? + Rails.logger.info "No bookmarks found for period #{start_date} to #{end_date}" + return + end + if user.present? - DigestMailer.weekly_bookmarks_digest(user, bookmarks).deliver_now + process_single_user(user, bookmark_ids) else - DigestMailer.weekly_bookmarks_digest(User.first, bookmarks).deliver_now + process_all_users(bookmark_ids) + end + end + + private + + def process_single_user(user, bookmark_ids) + if user.digest_preference_weekly_summaries? + Rails.logger.info "Sending weekly digest to user #{user.id}" + DigestMailer.weekly_bookmarks_digest(user, bookmark_ids).deliver_later + else + Rails.logger.info "User #{user.id} has disabled weekly digests" + end + end + + def process_all_users(bookmark_ids) + user_count = User.digest_preference_weekly_summaries.count + Rails.logger.info "Starting weekly digest for #{user_count} users" + + User.digest_preference_weekly_summaries.find_each do |user| + DigestMailer.weekly_bookmarks_digest(user, bookmark_ids).deliver_later end end end diff --git a/app/mailers/digest_mailer.rb b/app/mailers/digest_mailer.rb index 803c44d..b37f889 100644 --- a/app/mailers/digest_mailer.rb +++ b/app/mailers/digest_mailer.rb @@ -4,9 +4,10 @@ class DigestMailer < ApplicationMailer # # en.digest_mailer.weekly_bookmarks_digest.subject # - def weekly_bookmarks_digest(user, bookmarks) + def weekly_bookmarks_digest(user, bookmark_ids) @user = user - @bookmarks = bookmarks + @bookmarks = Post.where(id: bookmark_ids).order(created_at: :desc) + mail(to: @user.email, subject: 'mind reader :: weekly digest') end end diff --git a/app/models/user.rb b/app/models/user.rb index 1726c37..938c2c1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -22,6 +22,13 @@ class User < ApplicationRecord attribute :buttondown_status, :string, default: 'unactivated' attribute :support_type, :string + # do they want digests? + attribute :digest_preference, :integer, default: 0 + enum :digest_preference, { + 'no_summaries' => 0, + 'weekly_summaries' => 1 + }, prefix: true + def one_time_donor? subscription_status == 'one_time' end diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index c23b518..f2f30b7 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -62,6 +62,27 @@

When enabled, AI-generated summaries will not be displayed on posts.

+ <% if (current_user.paid_user? == false) && (current_user.non_financial_supporter? == false) %> +
+ <%= f.label :digest_preference, "Digest email preferences" %> + <%= f.select :digest_preference, + User.digest_preferences.keys.map { |k| [k.humanize, k] }, + {}, + class: "form-select", + disabled: "disabled" %> +

Members have access to weekly summary emails which include full-text dispatches and bookmarks. Consider <%= link_to "becoming a member", new_subscription_path %> to use this feature.

+
+ <% else %> +
+ <%= f.label :digest_preference, "Digest email preferences" %> + <%= f.select :digest_preference, + User.digest_preferences.keys.map { |k| [k.humanize, k] }, + {}, + class: "form-select" %> +

Please note, this is a separate email including both dispatches and bookmarks sent weekly. If you are opted-in receive dispatches via email and want to change this, you can do so <%= link_to "on this page", subscriptions_path %>.

+
+ <% end %> +
<%= f.label :current_password %>

We need your current password to confirm your changes:

diff --git a/app/views/digest_mailer/weekly_bookmarks_digest.html.erb b/app/views/digest_mailer/weekly_bookmarks_digest.html.erb index 24a7221..7486925 100644 --- a/app/views/digest_mailer/weekly_bookmarks_digest.html.erb +++ b/app/views/digest_mailer/weekly_bookmarks_digest.html.erb @@ -1,33 +1,82 @@ -

Dear <%= @user.first_name %>,

-

Here's what's new from mind reader this week:

-<% @bookmarks.group_by { |bookmark| bookmark.created_at.to_date }.each do |date, bookmarks| %> -

On <%= date.strftime('%B %e, %Y') %>:

-
    - <% bookmarks.each do |bookmark| %> -
  • - <% url = if bookmark&.url&.present? - bookmark.url - else - year = bookmark&.created_at&.year || "2000" - "https://mndrdr.org/#{year}/#{bookmark.slug}" - end %> - <%= bookmark.title %> - <% if bookmark.content.present? %> -
    <%= truncate(bookmark.content, length: 255, separator: ' ', omission: '...') %> + + + + + + + +
    +

    mind reader

    +

    Your weekly dispatch and bookmark digest.

    + +
      + <% @bookmarks.group_by { |bookmark| bookmark.created_at.to_date }.each do |date, items| %> +
    • + From <%= date.strftime('%B %e, %Y') %>: +
    • + <% items.each do |item| %> +
    • + + <% url = if item&.url&.present? + item.url + else + year = item&.created_at&.year || "2000" + "https://mndrdr.org/#{year}/#{item.slug}" + end %> + <%= item.title %> + + + <% if item.dispatch? %> +
      + <%= raw item.rendered_content %> +
      + <% elsif item.content.present? %> +
      + <%= item.content %> +
      + <% end %> + + <% if item.summary? && (!@user&.hide_ai_summaries?) %> +
      + ML Summary: <%= item.summary %> +
      + <% end %> + + + Added <%= item.created_at.strftime('%l:%M%P') if item.created_at.present? %> + +
    • <% end %> - <% if bookmark.created_at.present? %> -
      Added at <%= bookmark.created_at.strftime('%l:%M%P') %> - <% end %> - - <% end %> -
    -<% end %> -

    Have a great weekend,
    -Aidan.

    - + <% end %> +
+ +

+ Have a brilliant weekend,
Aidan.
+ You're receiving this because you opted in to weekly digests. + You can change your preferences in your account settings. +

+
+ + diff --git a/app/views/mailing_lists/index.html.erb b/app/views/mailing_lists/index.html.erb index 99f1882..3955bcd 100644 --- a/app/views/mailing_lists/index.html.erb +++ b/app/views/mailing_lists/index.html.erb @@ -1,12 +1,14 @@ <% content_for :title do %>Your mailing list membership<% end %> <% content_for :form_content do %> +

Below you can change your settings for receiving new dispatches as they are published. This is sent via a service called Buttondown, and you can manage your subscription below and via the settings at the bottom of emails sent by the service. Note, <%= link_to "members", subscriptions_path %> may also receive a digest version of mind reader which includes full-text dispatches and bookmarks via the opt-in on your <%= link_to "profile page", edit_user_registration_path %>.

+ <% if @buttondown_status == 'unactivated' %>

You are not currently subscribed to our mailing list.

<%= button_to "Subscribe", subscribe_mailing_lists_path, method: :post, class: "button" %> <% else %>

Your current subscription status: <%= @buttondown_status.titleize %>

- + <% if @is_paid_user %>

You are a paid subscriber. Thank you for your support!

<% if @buttondown_status != 'gifted' %> @@ -14,7 +16,7 @@ <%= button_to "Sync Subscription Status", sync_status_mailing_lists_path, method: :post, class: "button" %> <% end %> <% end %> - + <%= button_to "Unsubscribe", unsubscribe_mailing_lists_path, method: :delete, class: "button danger", data: { confirm: "Are you sure you want to unsubscribe?" } %> <% end %> @@ -27,4 +29,4 @@ <% end %> -<%= render template: 'layouts/user_page_template' %> \ No newline at end of file +<%= render template: 'layouts/user_page_template' %> diff --git a/app/views/subscriptions/new.html.erb b/app/views/subscriptions/new.html.erb index e8711ab..a8ac223 100644 --- a/app/views/subscriptions/new.html.erb +++ b/app/views/subscriptions/new.html.erb @@ -9,43 +9,45 @@
  • One-time donation – or, as many times as you like, you like some of the work I’ve done here and want to let me know what I have done matters to you. You have my undying appreciation, and forever access to premium features.
  • Ongoing support – an annual subscription for as much as you can manage, you want me to commit more time to doing this work and are willing to become a patron of my work. You’ll have premium features forever, even if you end your subscription. This takes me one step closer to being able to craft daily change making theory.
  • - - <% flash.each do |type, message| %> -
    - <%= message %> -
    - <% end %> - - <%= form_tag subscriptions_path, id: 'payment-form' do %> -
    - <%= label_tag :support_type, "Support Type" %> - <%= select_tag :support_type, - options_for_select([ - ['Non-financial support', 'non_financial'], - ['One-time donation', 'one_time'], - ['Ongoing support (annual)', 'ongoing'] - ], 'non_financial'), - class: 'support-type-select' - %> -
    - - - -
    - <%= submit_tag "Pay now", class: "button primary" %> -
    - <% end %> + +
    + <% flash.each do |type, message| %> +
    + <%= message %> +
    + <% end %> + + <%= form_tag subscriptions_path, id: 'payment-form' do %> +
    + <%= label_tag :support_type, "Support Type" %> + <%= select_tag :support_type, + options_for_select([ + ['Non-financial support', 'non_financial'], + ['One-time donation', 'one_time'], + ['Ongoing support (annual)', 'ongoing'] + ], 'non_financial'), + class: 'support-type-select' + %> +
    + + + +
    + <%= submit_tag "Pay now", class: "button primary" %> +
    + <% end %> +
    <% end %> <% content_for :javascript do %> @@ -61,10 +63,10 @@ } }; var card = elements.create('card', {style: style, currency: 'aud'}); - + var supportTypeSelect = document.querySelector('.support-type-select'); var paymentFields = document.getElementById('payment-fields'); - + function togglePaymentFields() { if (supportTypeSelect.value === 'one_time' || supportTypeSelect.value === 'ongoing') { paymentFields.style.display = 'block'; @@ -77,7 +79,7 @@ // Initial toggle of payment fields togglePaymentFields(); - + supportTypeSelect.addEventListener('change', togglePaymentFields); card.addEventListener('change', function(event) { @@ -92,7 +94,7 @@ var form = document.getElementById('payment-form'); form.addEventListener('submit', function(event) { event.preventDefault(); - + if (supportTypeSelect.value === 'one_time' || supportTypeSelect.value === 'ongoing') { stripe.createToken(card, {currency: 'aud'}).then(function(result) { if (result.error) { @@ -121,24 +123,24 @@ if (paymentAmountInput) { // Set initial value paymentAmountInput.value = 'AU$0.00'; - + paymentAmountInput.addEventListener('input', function(e) { var cursorPosition = e.target.selectionStart; var value = e.target.value.replace(/[^0-9.]/g, ''); - + var floatValue = parseFloat(value) || 0; var formattedValue = 'AU$' + floatValue.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ","); - + var oldLength = e.target.value.length; e.target.value = formattedValue; var newLength = e.target.value.length; - + // Adjust cursor position var newPosition = cursorPosition + (newLength - oldLength); newPosition = Math.max(3, Math.min(newPosition, formattedValue.length)); e.target.setSelectionRange(newPosition, newPosition); }); - + paymentAmountInput.addEventListener('focus', function(e) { if (e.target.value === 'AU$0.00') { setTimeout(function() { @@ -146,7 +148,7 @@ }, 0); } }); - + paymentAmountInput.addEventListener('blur', function(e) { var value = parseFloat(e.target.value.replace(/[^0-9.]/g, '')) || 0; e.target.value = 'AU$' + value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ","); @@ -156,4 +158,4 @@ <% end %> -<%= render template: 'layouts/user_page_template' %> \ No newline at end of file +<%= render template: 'layouts/user_page_template' %> diff --git a/db/migrate/20250113020623_add_digest_preferences_to_users.rb b/db/migrate/20250113020623_add_digest_preferences_to_users.rb new file mode 100644 index 0000000..f774c21 --- /dev/null +++ b/db/migrate/20250113020623_add_digest_preferences_to_users.rb @@ -0,0 +1,5 @@ +class AddDigestPreferencesToUsers < ActiveRecord::Migration[8.0] + def change + add_column :users, :digest_preference, :integer, default: 0, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 88661c2..582226d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_01_12_054949) do +ActiveRecord::Schema[8.0].define(version: 2025_01_13_020623) do create_table "api_keys", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "key" t.datetime "created_at", null: false @@ -87,6 +87,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_12_054949) do t.text "otp_backup_codes" t.boolean "hide_ai_summaries", default: false t.integer "consumed_timestep" + t.integer "digest_preference", default: 0, null: false t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true -- 2.39.5