]> gitweb.mndrdr.org Git - arelpe.git/commitdiff
Omnibus update for arelpe:
authorAidan Cornelius-Bell <[email protected]>
Mon, 13 Jan 2025 03:02:40 +0000 (13:32 +1030)
committerAidan Cornelius-Bell <[email protected]>
Mon, 13 Jan 2025 03:02:40 +0000 (13:32 +1030)
- 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
app/controllers/users/registrations_controller.rb
app/jobs/weekly_bookmarks_digest_job.rb
app/mailers/digest_mailer.rb
app/models/user.rb
app/views/devise/registrations/edit.html.erb
app/views/digest_mailer/weekly_bookmarks_digest.html.erb
app/views/mailing_lists/index.html.erb
app/views/subscriptions/new.html.erb
db/migrate/20250113020623_add_digest_preferences_to_users.rb [new file with mode: 0644]
db/schema.rb

index e8e1611e8ee240c4c37127ac799d84e7768ce8c0..16b3c760e7ebfd9eb73a0c4b999836d0d6e3e036 100644 (file)
@@ -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;
 }
index 703d4f10810844310ae13bd5f1c19b1062d3dacd..a614e8d8eedb0bee8c39845a688460786bbdda7d 100644 (file)
@@ -25,7 +25,8 @@ class Users::RegistrationsController < Devise::RegistrationsController
       :password,
       :password_confirmation,
       :current_password,
-      :hide_ai_summaries
+      :hide_ai_summaries,
+      :digest_preference
     )
   end
 
index b38746fafb10aac96268157565130e697208185a..ea72a663174ff995801eb574239278d0f47949d8 100644 (file)
@@ -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
index 803c44d8671d63458d85a544a3c59c9ed2f8f8bc..b37f889b308b6dedb8932f8fe1a1fc463bb4e2b1 100644 (file)
@@ -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
index 1726c37d750cc82d146c64a7093f0f6401b1bf6e..938c2c1e83b80db4b8758d9f055faaf2b1e0f5d0 100644 (file)
@@ -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
index c23b518060885752611c5cb05d6eaee3916a74ed..f2f30b7d093e4240dd55784844a975536db95dc2 100644 (file)
     <p class="help-text">When enabled, AI-generated summaries will not be displayed on posts.</p>
     </div>
 
+    <% if (current_user.paid_user? == false) && (current_user.non_financial_supporter? == false) %>
+        <div class="field reg-form">
+        <%= 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" %>
+        <p class="help-text">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.</p>
+        </div>
+    <% else %>
+    <div class="field reg-form">
+    <%= f.label :digest_preference, "Digest email preferences" %>
+    <%= f.select :digest_preference,
+                    User.digest_preferences.keys.map { |k| [k.humanize, k] },
+                    {},
+                    class: "form-select" %>
+    <p class="help-text">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 %>.</p>
+    </div>
+    <% end %>
+
     <div class="reg-form">
         <div class="field">
         <%= f.label :current_password %><p class="help-text">We need your current password to confirm your changes:</p>
index 24a72219a12319b96239a2914a26bded00bad55b..748692560cf63dc518f28e44ef45b1f0705cf82c 100644 (file)
@@ -1,33 +1,82 @@
-<p>Dear <%= @user.first_name %>,</p>
-<p>Here's what's new from mind reader this week:</p>
-<% @bookmarks.group_by { |bookmark| bookmark.created_at.to_date }.each do |date, bookmarks| %>
-  <p span="dateline">On <%= date.strftime('%B %e, %Y') %>:</p>
-  <ul>
-    <% bookmarks.each do |bookmark| %>
-      <li>
-        <% url = if bookmark&.url&.present?
-                  bookmark.url
-                else
-                  year = bookmark&.created_at&.year || "2000"
-                  "https://mndrdr.org/#{year}/#{bookmark.slug}"
-                end %>
-        <strong><a href="<%= url %>" target="_blank"><%= bookmark.title %></a></strong>
-        <% if bookmark.content.present? %>
-          <br><em><%= truncate(bookmark.content, length: 255, separator: ' ', omission: '...') %></em>
+<!DOCTYPE html>
+<html>
+<head>
+  <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
+  <style type="text/css">
+    body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
+    .container { max-width: 600px; margin: 0 auto; padding: 20px; }
+    .header { font-size: 24px; margin-bottom: 10px; }
+    .subheading { color: #666; font-size: 14px; margin-bottom: 20px; }
+    ul { list-style-type: none; padding: 0; }
+    .date-heading { font-weight: bold; margin-top: 20px; color: #333; }
+    .post-link { display: block; margin-bottom: 5px; }
+    .post-link a { color: #0066cc; text-decoration: none; }
+    .post-link a:hover { text-decoration: underline; }
+    .meta { color: #666; font-size: 12px; }
+    .dash { color: #999; }
+    .bookmark-comment { color: #333; font-style: italic; }
+    blockquote {
+      margin: 10px 0;
+      padding: 10px;
+      background: #f5f5f5;
+      border-left: 3px solid #ddd;
+    }
+    .excerpt { color: #666; font-size: 14px; }
+    li { margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #eee; }
+    .dispatch-content { margin-top: 10px; }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <h1 class="header">mind reader</h1>
+    <p class="subheading">Your weekly dispatch and bookmark digest.</p>
+
+    <ul>
+      <% @bookmarks.group_by { |bookmark| bookmark.created_at.to_date }.each do |date, items| %>
+        <li class="date-heading">
+          From <%= date.strftime('%B %e, %Y') %>:
+        </li>
+        <% items.each do |item| %>
+          <li>
+            <span class="post-link">
+              <% url = if item&.url&.present?
+                        item.url
+                      else
+                        year = item&.created_at&.year || "2000"
+                        "https://mndrdr.org/#{year}/#{item.slug}"
+                      end %>
+              <strong><a href="<%= url %>" target="_blank"><%= item.title %></a></strong>
+            </span>
+
+            <% if item.dispatch? %>
+              <div class="dispatch-content">
+                <%= raw item.rendered_content %>
+              </div>
+            <% elsif item.content.present? %>
+              <div class="bookmark-comment">
+                <%= item.content %>
+              </div>
+            <% end %>
+
+            <% if item.summary? && (!@user&.hide_ai_summaries?) %>
+              <blockquote class="excerpt">
+                <strong>ML Summary:</strong> <%= item.summary %>
+              </blockquote>
+            <% end %>
+
+            <span class="meta">
+              Added <%= item.created_at.strftime('%l:%M%P') if item.created_at.present? %>
+            </span>
+          </li>
         <% end %>
-        <% if bookmark.created_at.present? %>
-          <br><small>Added at <%= bookmark.created_at.strftime('%l:%M%P') %></small>
-        <% end %>
-      </li>
-    <% end %>
-  </ul>
-<% end %>
-<p>Have a great weekend,<br>
-Aidan.</p>
-<style type="text/css">
-<!--
-    li { margin-bottom: 3px; }
-    .dateline:first-of-type { margin-top: 0 }
-    .dateline { margin-top: 5px; }
--->
-</style>
+      <% end %>
+    </ul>
+
+    <p style="margin-top: 30px; color: #666; font-size: 12px;">
+      Have a brilliant weekend,<br>Aidan.<br>
+      You're receiving this because you opted in to weekly digests.
+      You can change your preferences in your <a href="https://mndrdr.org/users/edit">account settings</a>.
+    </p>
+  </div>
+</body>
+</html>
index 99f1882dd6a528f7d67c473e88c1c89c2df48b69..3955bcde4e51a7dad997f9da925eeabcf820fb77 100644 (file)
@@ -1,12 +1,14 @@
 <% content_for :title do %>Your mailing list membership<% end %>
 <% content_for :form_content do %>
 
+<p>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 %>.</p>
+
 <% if @buttondown_status == 'unactivated' %>
   <p>You are not currently subscribed to our mailing list.</p>
   <%= button_to "Subscribe", subscribe_mailing_lists_path, method: :post, class: "button" %>
 <% else %>
   <p>Your current subscription status: <strong><%= @buttondown_status.titleize %></strong></p>
-  
+
   <% if @is_paid_user %>
     <p>You are a paid subscriber. Thank you for your support!</p>
     <% 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' %>
index e8711ab24380c57939a8d6535e574ddeffd355b0..a8ac223613135fede427c0b02b2c746f4bec679f 100644 (file)
@@ -9,43 +9,45 @@
          <li>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.</li>
          <li>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.</li>
   </ol>
-  
-  <% flash.each do |type, message| %>
-         <div class="flash <%= type %>">
-               <%= message %>
-         </div>
-       <% end %>
-  
-  <%= form_tag subscriptions_path, id: 'payment-form' do %>
-       <div class="field">
-         <%= 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' 
-         %>
-       </div>
-
-       <div id="payment-fields" style="display: none;">
-         <div class="field support-amount">
-               <%= label_tag :payment_amount, "Support amount" %>
-               <%= text_field_tag :payment_amount, nil, class: 'currency-input', placeholder: "AU$0.00" %>
-         </div>
-         
-         <div class="field">
-               <label for="card-element">Credit or debit card</label>
-               <div id="card-element"></div>
-               <div id="card-errors" role="alert"></div>
-         </div>
-       </div>
-
-       <div class="actions">
-         <%= submit_tag "Pay now", class: "button primary" %>
-       </div>
-  <% end %>
+
+  <div class="form reg-form">
+    <% flash.each do |type, message| %>
+         <div class="flash <%= type %>">
+               <%= message %>
+         </div>
+       <% end %>
+
+    <%= form_tag subscriptions_path, id: 'payment-form' do %>
+       <div class="field">
+         <%= 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'
+         %>
+       </div>
+
+       <div id="payment-fields" style="display: none;">
+         <div class="field support-amount">
+               <%= label_tag :payment_amount, "Support amount" %>
+               <%= text_field_tag :payment_amount, nil, class: 'currency-input', placeholder: "AU$0.00" %>
+         </div>
+
+         <div class="field">
+               <label for="card-element">Credit or debit card</label>
+               <div id="card-element"></div>
+               <div id="card-errors" role="alert"></div>
+         </div>
+       </div>
+
+       <div class="actions">
+         <%= submit_tag "Pay now", class: "button primary" %>
+       </div>
+    <% end %>
+  </div>
 <% end %>
 
 <% content_for :javascript do %>
                }
          };
          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) {
                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() {
                          }, 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, ",");
   </script>
 <% 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 (file)
index 0000000..f774c21
--- /dev/null
@@ -0,0 +1,5 @@
+class AddDigestPreferencesToUsers < ActiveRecord::Migration[8.0]
+  def change
+    add_column :users, :digest_preference, :integer, default: 0, null: false
+  end
+end
index 88661c2dc7d8fa8dd4b6de2b6bd3f40b7ab25a5f..582226db83bf41d53436dfd4ea509f7c1ea9e28d 100644 (file)
@@ -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