bible270 1.4.1 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bcb59bf731c08118644e9303ed749397492c6c7fea523a20cf4eb3c17b84281f
4
- data.tar.gz: e1f6b1be1b245e859370ae745eb05f8adf47df953f89ad7cb28c700ad705830c
3
+ metadata.gz: 8792007d09956f4b53cbe9842007baea209a5eb0010758ef35e7d6f1977928f0
4
+ data.tar.gz: 0c5d16299136af62a6e1466210e962ef6e8d48065d759d9b5b57450215471c31
5
5
  SHA512:
6
- metadata.gz: 312efc5cd9bfcb503430d1adc601b21a93658c9d378060182d3453b747d43674a736a83b753accaf0f0d738b0da005656c87490f17a51814800b3798632c09ab
7
- data.tar.gz: c45129d6950f6b40f8bb0477b9de95ed9e98bb5ad3c1f9fa36bfb1b44eb5c1b26e337fc9d31e7dc9554115f2c4ecde14427127333bf2287925a0c0a33457b470
6
+ metadata.gz: 89cbba3ff6adfc107efacddb9b2d80903452488384983b488ecf733aa6e16d55451a0137626cda5a446591b2c28585201f49b612927a6ba80ad0b54a5889c377
7
+ data.tar.gz: d2d501c3f9935405bf0b850124f2329b3c5355ccfdf5d7403688329a65f1bfdc4a779fc3d65d957eba242363393e6a057519ef509b337986f6426bb2a3c6ffdc
data/CHANGELOG.md CHANGED
@@ -4,9 +4,18 @@ All notable changes to bible270. Format follows [Keep a Changelog](https://keepa
4
4
 
5
5
  ## Unreleased Changes
6
6
 
7
+ ### Added
8
+
9
+ - let administrators disable the day-completion dove animation for individual readers
10
+
7
11
  ### Changed
8
12
 
9
13
  - simplify day completion to one continuous server-confirmed dove flight from the lower center, eliminating the stagger between a pre-submit animation and the Turbo or HTML response
14
+ - keep the dove bright in dark mode, grow it to an 80%-of-viewport fit, and finish with a quick fade instead of veering offscreen
15
+
16
+ ### Fixed
17
+
18
+ - show the dove only once for each persisted day-completion event, even when Turbo replays a response or retries a stale-session request
10
19
 
11
20
  **[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.4.0...main)**
12
21
 
@@ -13,8 +13,8 @@ module Bible270
13
13
 
14
14
  before_action :require_admin!
15
15
  before_action :load_reader,
16
- only: %i[show destroy update_start update_profile update_notifications remove_avatar complete_through
17
- toggle_day]
16
+ only: %i[show destroy update_start update_profile update_notifications update_completion_animation
17
+ remove_avatar complete_through toggle_day]
18
18
  before_action :load_comment, only: %i[hide_comment unhide_comment destroy_comment]
19
19
 
20
20
  # ---- enrolment --------------------------------------------------------
@@ -74,6 +74,7 @@ module Bible270
74
74
 
75
75
  def show
76
76
  @comment_notifications_available = comment_notifications_available_for?(@reader)
77
+ @completion_dove_preference_available = completion_dove_available_for(@reader)
77
78
  @days_completed = @reader.days_completed
78
79
  @comments = @reader.comments.order(created_at: :desc).limit(50)
79
80
  end
@@ -236,6 +237,16 @@ module Bible270
236
237
  redirect_to admin_reader_path(@reader), notice: 'Reflection email setting updated.'
237
238
  end
238
239
 
240
+ def update_completion_animation
241
+ unless completion_dove_available_for(@reader)
242
+ alert = 'Run the pending Bible270 database migration before changing the completion animation.'
243
+ return redirect_to(admin_reader_path(@reader), alert: alert)
244
+ end
245
+
246
+ @reader.update!(completion_dove_disabled: params[:completion_dove_disabled] == '1')
247
+ redirect_to admin_reader_path(@reader), notice: 'Completion animation setting updated.'
248
+ end
249
+
239
250
  def remove_avatar
240
251
  @reader.remove_avatar!
241
252
  redirect_to admin_reader_path(@reader), notice: "Removed #{@reader.display_name}'s picture."
@@ -298,6 +309,12 @@ module Bible270
298
309
  true
299
310
  end
300
311
 
312
+ def completion_dove_available_for(reader)
313
+ available = Reader.completion_dove_column?
314
+ reader.reload if available && !reader.has_attribute?('completion_dove_disabled')
315
+ available
316
+ end
317
+
301
318
  def require_admin!
302
319
  return if Bible270.config.admin_configured? && Bible270.config.admin?(current_reader)
303
320
 
@@ -18,25 +18,27 @@ module Bible270
18
18
  requested_state = params[:checked] if params.key?(:checked)
19
19
  head :bad_request and return if params.key?(:checked) && !%w[0 1].include?(requested_state)
20
20
 
21
- marked_read, checkoff_changed = apply_checkoff(requested_state)
22
-
23
- @reader = current_reader.reload.reload_progress
24
- @day_just_completed = marked_read && checkoff_changed && @reader.day_complete?(@day)
21
+ # Serialize this reader's transitions so two different final portions
22
+ # submitted concurrently cannot both announce the same day completion.
23
+ marked_read = nil
24
+ current_reader.with_lock do
25
+ marked_read, @completion_event_id = apply_checkoff(requested_state)
26
+ @reader = current_reader.reload.reload_progress
27
+ @day_just_completed = marked_read && @completion_event_id && @reader.day_complete?(@day)
28
+ end
25
29
  @reader_tracks = @reader.read_tracks_for(@day)
26
30
  @readings = Plan.readings_for(@day)
27
31
 
28
- required = Plan.total_parts(@day)
29
- completer_ids = Checkoff.where(day: @day)
30
- .group(:reader_id)
31
- .having('COUNT(*) >= ?', required)
32
- .pluck(:reader_id)
33
- @completers = Reader.where(id: completer_ids).order(:display_name)
32
+ @completers = Reader.completers_for(@day)
34
33
 
35
34
  respond_to do |format|
36
35
  format.turbo_stream
37
36
  format.html do
38
37
  if @day_just_completed
39
- redirect_to day_path(@day), flash: { b270_day_just_completed: @day }
38
+ redirect_to day_path(@day), flash: {
39
+ b270_day_just_completed: @day,
40
+ b270_completion_event_id: @completion_event_id
41
+ }
40
42
  else
41
43
  message = marked_read ? 'Reading marked read.' : 'Reading marked unread.'
42
44
  redirect_to day_path(@day), flash: { b270_interaction_status: message }
@@ -51,9 +53,10 @@ module Bible270
51
53
  existing = find_checkoff
52
54
  if requested_state == '1'
53
55
  current_reader.ensure_started!
54
- [true, existing.nil? && create_checkoff]
56
+ [true, existing.nil? ? create_checkoff : nil]
55
57
  elsif requested_state == '0' || existing
56
- [false, existing ? existing.destroy.present? : false]
58
+ existing&.destroy
59
+ [false, nil]
57
60
  else
58
61
  current_reader.ensure_started!
59
62
  [true, create_checkoff]
@@ -66,15 +69,16 @@ module Bible270
66
69
  end
67
70
  end
68
71
 
72
+ # The created id is stable if Turbo replays this response, while a later
73
+ # re-completion creates a new id and is correctly treated as a new event.
69
74
  def create_checkoff
70
- Checkoff.create!(reader: current_reader, day: @day, track: @track, part: @part)
71
- true
75
+ Checkoff.create!(reader: current_reader, day: @day, track: @track, part: @part).id
72
76
  rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
73
77
  # A repeated or concurrent desired-state request is already successful. Do
74
78
  # not hide any other validation failure under the same recovery.
75
79
  raise unless find_checkoff
76
80
 
77
- false
81
+ nil
78
82
  end
79
83
  end
80
84
  end
@@ -21,16 +21,9 @@ module Bible270
21
21
  @new_comment = Comment.new(day: @day, parent_id: reply_parent&.id)
22
22
  @reader_tracks = current_reader ? current_reader.read_tracks_for(@day) : []
23
23
  @day_just_completed = flash[:b270_day_just_completed].to_i == @day
24
+ @completion_event_id = flash[:b270_completion_event_id] if @day_just_completed
24
25
 
25
- # Every box on the day, not every track: an Old Testament reading of three
26
- # chapters is three rows, so counting tracks would call someone finished
27
- # who had only read the Old Testament.
28
- required = Plan.total_parts(@day)
29
- completer_ids = Checkoff.where(day: @day)
30
- .group(:reader_id)
31
- .having('COUNT(*) >= ?', required)
32
- .pluck(:reader_id)
33
- @completers = Reader.where(id: completer_ids).order(:display_name)
26
+ @completers = Reader.completers_for(@day)
34
27
  end
35
28
 
36
29
  private
@@ -33,6 +33,7 @@ module Bible270
33
33
  REFLECTIONS_SEEN_COLUMN = 'reflections_seen_at'
34
34
  COMMENT_NOTIFICATION_LEVELS = %w[all personal none].freeze
35
35
  COMMENT_NOTIFICATION_COLUMNS = %w[notify_on_mention notify_on_all_comments].freeze
36
+ COMPLETION_DOVE_COLUMN = 'completion_dove_disabled'
36
37
  MENTION_SUGGESTION_LIMIT = 5
37
38
 
38
39
  validates :display_name, presence: true
@@ -64,6 +65,18 @@ module Bible270
64
65
  end
65
66
  end
66
67
 
68
+ # A day is completed when its final required checkoff is created, so MAX(created_at)
69
+ # is the completion moment. Keep reader_id as a deterministic tie-breaker.
70
+ def self.completers_for(day)
71
+ reader_ids = Checkoff.where(day: day)
72
+ .group(:reader_id)
73
+ .having('COUNT(*) >= ?', Plan.total_parts(day))
74
+ .order(Checkoff.arel_table[:created_at].maximum.asc, :reader_id)
75
+ .pluck(:reader_id)
76
+ readers_by_id = where(id: reader_ids).index_by(&:id)
77
+ reader_ids.filter_map { |reader_id| readers_by_id[reader_id] }
78
+ end
79
+
67
80
  def self.reflections_seen_column?
68
81
  column_names.include?(REFLECTIONS_SEEN_COLUMN)
69
82
  rescue ActiveRecord::StatementInvalid
@@ -289,6 +302,22 @@ module Bible270
289
302
  where(notify_on_all_comments: true).where.not(email: [nil, ''])
290
303
  end
291
304
 
305
+ def self.completion_dove_column?
306
+ return true if column_names.include?(COMPLETION_DOVE_COLUMN)
307
+
308
+ reset_column_information
309
+ column_names.include?(COMPLETION_DOVE_COLUMN)
310
+ rescue ActiveRecord::StatementInvalid
311
+ false
312
+ end
313
+
314
+ # Keep day completion usable while an upgraded host is waiting to run the
315
+ # preference migration. Historically the dove was enabled, so a missing
316
+ # column preserves that behavior.
317
+ def completion_dove_disabled?
318
+ has_attribute?(COMPLETION_DOVE_COLUMN) && self[COMPLETION_DOVE_COLUMN] == true
319
+ end
320
+
292
321
  # Small, deterministic suggestions for the reflection composer. Full-handle
293
322
  # collisions are omitted because the server must never guess which reader a
294
323
  # mention intended.
@@ -70,6 +70,29 @@
70
70
  <% end %>
71
71
  </div>
72
72
 
73
+ <div class="b270-panel">
74
+ <h2 class="b270-subhead">Completion animation</h2>
75
+ <% unless @completion_dove_preference_available %>
76
+ <p class="b270-flash alert" style="margin:0">
77
+ Run the pending Bible270 database migration before changing the completion animation.
78
+ </p>
79
+ <% else %>
80
+ <%= form_with url: admin_reader_completion_animation_path(@reader), method: :patch,
81
+ class: "b270-emailform" do %>
82
+ <div class="b270-field">
83
+ <label class="b270-reminder-optin">
84
+ <%= check_box_tag :completion_dove_disabled, "1", @reader.completion_dove_disabled? %>
85
+ <span>Turn off the dove animation for this reader</span>
86
+ </label>
87
+ </div>
88
+ <p class="b270-hint" style="margin:0">
89
+ Day completion and progress tracking still work normally when the animation is off.
90
+ </p>
91
+ <%= submit_tag 'Save', class: 'b270-btn' %>
92
+ <% end %>
93
+ <% end %>
94
+ </div>
95
+
73
96
  <div class="b270-panel">
74
97
  <h2 class="b270-subhead">Where they are in the plan</h2>
75
98
  <%= form_with url: admin_reader_start_path(@reader), method: :patch, class: "b270-startform" do %>
@@ -5,7 +5,7 @@
5
5
 
6
6
  <%= turbo_stream.replace "day_progress_#{@day}" do %>
7
7
  <%= render "bible270/days/day_progress", day: @day, reader: @reader,
8
- just_completed: @day_just_completed %>
8
+ just_completed: @day_just_completed, completion_event_id: @completion_event_id %>
9
9
  <% end %>
10
10
 
11
11
  <%= turbo_stream.replace "completers_#{@day}" do %>
@@ -2,9 +2,8 @@
2
2
  <div class="b270-completers" id="completers_<%= day %>">
3
3
  <% if completers.any? %>
4
4
  <span>Finished this day:</span>
5
- <% completers.first(12).each do |reader| %>
6
- <%= link_to b270_avatar(reader), reader_path(reader), title: reader.display_name %>
5
+ <% completers.each do |reader| %>
6
+ <%= link_to b270_avatar(reader, size: 56), reader_path(reader), title: reader.display_name %>
7
7
  <% end %>
8
- <% if completers.size > 12 %><span><%= completers.size - 12 %> more</span><% end %>
9
8
  <% end %>
10
9
  </div>
@@ -1,4 +1,4 @@
1
- <%# locals: (day:, reader:, just_completed: false) %>
1
+ <%# locals: (day:, reader:, just_completed: false, completion_event_id: nil) %>
2
2
  <%
3
3
  previous_day = day > 1 ? day - 1 : nil
4
4
  next_day = day < Bible270::Plan::DAYS ? day + 1 : nil
@@ -33,7 +33,9 @@
33
33
 
34
34
 
35
35
  <% if complete %>
36
- <%= render "bible270/shared/day_completion_dove", day: day if just_completed %>
36
+ <% if just_completed && completion_event_id && !reader&.completion_dove_disabled? %>
37
+ <%= render "bible270/shared/day_completion_dove", day: day, completion_event_id: completion_event_id %>
38
+ <% end %>
37
39
  <div class="b270-day-complete" role="status" aria-live="polite"
38
40
  <%= tag.attributes(data: completion_data) %>>
39
41
  <div>
@@ -2,7 +2,7 @@
2
2
 
3
3
  <div class="b270-day-page">
4
4
  <%= render "bible270/days/day_progress", day: @day, reader: current_reader,
5
- just_completed: @day_just_completed %>
5
+ just_completed: @day_just_completed, completion_event_id: @completion_event_id %>
6
6
 
7
7
  <%= render "bible270/shared/interaction_status" %>
8
8
 
@@ -16,8 +16,6 @@
16
16
  <% end %>
17
17
 
18
18
  <div class="b270-day-community">
19
- <%= render "bible270/days/completers", day: @day, completers: @completers %>
20
-
21
19
  <h2 class="b270-section-h">Reflections</h2>
22
20
  <div id="comments">
23
21
  <%= render partial: "bible270/comments/comment", collection: @comments, as: :comment %>
@@ -30,6 +28,8 @@
30
28
  <%= render "bible270/shared/comment_drafts" %>
31
29
  <%= render "bible270/shared/mention_typeahead" %>
32
30
  <% end %>
31
+
32
+ <%= render "bible270/days/completers", day: @day, completers: @completers %>
33
33
  </div>
34
34
  </div>
35
35
 
@@ -1,17 +1,19 @@
1
- <%# locals: (day:) %>
1
+ <%# locals: (day:, completion_event_id:) %>
2
2
  <svg class="b270-completion-dove" data-b270-completion-dove data-b270-day="<%= day %>"
3
+ data-b270-completion-event="<%= completion_event_id %>" data-turbo-temporary
3
4
  viewBox="0 0 260 200" aria-hidden="true" focusable="false">
4
5
  <defs>
5
6
  <radialGradient id="b270-dove-light" cx="50%" cy="43%" r="68%">
6
- <stop offset="0%" stop-color="#fff" />
7
- <stop offset="58%" stop-color="#fffef8" />
8
- <stop offset="100%" stop-color="#fff9e2" />
7
+ <stop class="b270-dove-light-center" offset="0%" stop-color="#fff" />
8
+ <stop class="b270-dove-light-middle" offset="58%" stop-color="#fffef8" />
9
+ <stop class="b270-dove-light-edge" offset="100%" stop-color="#fff9e2" />
9
10
  </radialGradient>
10
11
  </defs>
11
12
  <g class="b270-dove-silhouette">
12
13
  <path d="M122 94C103 82 87 69 72 53L39 17l11 28-23-23 15 32-23-20 19 30-24-16 22 26-25-12 27 23-24-7 30 19-22-3 31 17c20 11 42 16 59 8 5-9 8-18 10-25Z" />
13
14
  <path d="M138 94c19-12 35-25 50-41l33-36-11 28 23-23-15 32 23-20-19 30 24-16-22 26 25-12-27 23 24-7-30 19 22-3-31 17c-20 11-42 16-59 8-5-9-8-18-10-25Z" />
14
15
  <path d="M148 140c6 7 16 14 24 19 2 2 1 5-2 6-5 2-10-1-14-5 4 8 7 17 4 22-2 3-5 1-7-2l-6-11c3 10 2 19-3 24-2 3-5 1-6-3l-4-15c0 11-3 21-8 24-3 2-5-1-6-5l-1-19c-3 10-6 17-11 20-3 2-5-1-5-5l2-19c-4 8-9 13-14 14-3 1-5-2-4-6l6-18c-5 5-11 7-16 4-3-1-3-4 0-7 7-7 17-13 26-19Z" />
15
- <path d="M108 116c5-10 9-17 14-17-1-11-2-23 4-29 5-4 12-3 15 1 2 3 2 6 1 9 3 .2 6 .8 8 1.5-2.5 1.3-5.5 2.1-8 2.5-1 5-3 10-5 15 6 3 11 9 15 17-1 15-6 28-16 39-3 4-7 7-10 9-4-2-8-5-11-9-9-11-14-25-11-37 1-1 2-2 4-2Z" />
16
+ <path class="b270-dove-beak" d="M140 80C144 80 148 80.8 151 82 152 82.4 152 83.2 151 83.8 148 85.1 144 85.8 140 85 138.5 83.5 138.5 81.2 140 80Z" />
17
+ <path d="M108 116c5-10 9-17 14-17-1-11-2-23 4-29 5-4 12-3 15 1 2 3 2 6 1 9 1 1 1 3 0 4-1 5-3 10-5 15 6 3 11 9 15 17-1 15-6 28-16 39-3 4-7 7-10 9-4-2-8-5-11-9-9-11-14-25-11-37 1-1 2-2 4-2Z" />
16
18
  </g>
17
19
  </svg>
@@ -18,6 +18,53 @@
18
18
  region.dataset.state = state;
19
19
  }
20
20
 
21
+ const shownCompletionDoves = new Set();
22
+
23
+ function claimCompletionDove(dove) {
24
+ const eventId = dove.dataset.b270CompletionEvent;
25
+ if (!eventId) return false;
26
+
27
+ const key = `b270-completion-dove:${eventId}`;
28
+ if (shownCompletionDoves.has(key)) return false;
29
+
30
+ try {
31
+ if (window.sessionStorage.getItem(key) === "shown") return false;
32
+ window.sessionStorage.setItem(key, "shown");
33
+ } catch (_error) {
34
+ // Some privacy modes disable session storage. The in-memory set still
35
+ // prevents duplicate Turbo rendering within the current page.
36
+ }
37
+ shownCompletionDoves.add(key);
38
+ return true;
39
+ }
40
+
41
+ function initializeCompletionDove(dove) {
42
+ if (dove.dataset.b270DoveInitialized === "true") return;
43
+ dove.dataset.b270DoveInitialized = "true";
44
+
45
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
46
+ dove.remove();
47
+ return;
48
+ }
49
+
50
+ if (!claimCompletionDove(dove)) {
51
+ dove.remove();
52
+ return;
53
+ }
54
+
55
+ // Keep an active flight outside the Turbo-replaced progress panel so a
56
+ // replayed response cannot restart or cut short the same completion event.
57
+ if (dove.parentElement !== document.body) document.body.appendChild(dove);
58
+ requestAnimationFrame(() => {
59
+ if (dove.isConnected) dove.classList.add("b270-completion-dove-active");
60
+ });
61
+ }
62
+
63
+ function initializeCompletionDoves(root = document) {
64
+ if (root.matches && root.matches("[data-b270-completion-dove]")) initializeCompletionDove(root);
65
+ root.querySelectorAll?.("[data-b270-completion-dove]").forEach(initializeCompletionDove);
66
+ }
67
+
21
68
  function resetForm(form) {
22
69
  form.removeAttribute("aria-busy");
23
70
  form.dataset.b270Submitting = "false";
@@ -29,8 +76,18 @@
29
76
 
30
77
  function reset() {
31
78
  document.querySelectorAll("form[data-b270-submit='true']").forEach(resetForm);
79
+ initializeCompletionDoves();
32
80
  }
33
81
 
82
+ const completionDoveObserver = new MutationObserver((mutations) => {
83
+ mutations.forEach((mutation) => {
84
+ mutation.addedNodes.forEach((node) => {
85
+ if (node.nodeType === Node.ELEMENT_NODE) initializeCompletionDoves(node);
86
+ });
87
+ });
88
+ });
89
+ completionDoveObserver.observe(document.documentElement, { childList: true, subtree: true });
90
+
34
91
  const SESSION_REFRESH_AFTER = 5 * 60 * 1000;
35
92
  let sessionRefreshedAt = Date.now();
36
93
  let sessionRefresh = null;
@@ -201,8 +258,12 @@
201
258
  }
202
259
  });
203
260
  document.addEventListener("turbo:load", reset);
261
+ document.addEventListener("turbo:before-cache", () => {
262
+ document.querySelectorAll("[data-b270-completion-dove]").forEach((dove) => dove.remove());
263
+ });
204
264
  document.addEventListener("animationend", (event) => {
205
- if (event.target.matches("[data-b270-completion-dove]")) event.target.remove();
265
+ if (event.animationName === "b270-dove-flight" &&
266
+ event.target.matches("[data-b270-completion-dove]")) event.target.remove();
206
267
  });
207
268
 
208
269
  window.Bible270InteractionUI = { reset };
@@ -148,16 +148,20 @@
148
148
  .b270-day-complete span{font-size:12px;color:var(--muted)}
149
149
  .b270 .b270-day-complete .b270-btn{flex:none;margin:0 0 0 auto}
150
150
  .b270-completion-dove{position:fixed;z-index:1000;top:72vh;left:50vw;
151
- width:min(75vw,97.5vh);height:auto;overflow:visible;pointer-events:none;opacity:0;
151
+ width:min(80vw,104vh);height:auto;overflow:visible;pointer-events:none;opacity:0;
152
+ color-scheme:only light;forced-color-adjust:none;
152
153
  filter:drop-shadow(0 0 8px rgba(255,255,255,1)) drop-shadow(0 0 24px rgba(255,252,220,1)) drop-shadow(0 0 52px rgba(225,191,92,.82));
153
- will-change:top,left,transform,opacity;animation:b270-dove-flight 1.6s cubic-bezier(.18,.72,.28,1) both}
154
- .b270-dove-silhouette{fill:url(#b270-dove-light);stroke:none}
154
+ will-change:top,left,transform,opacity}
155
+ .b270-completion-dove-active{animation:b270-dove-flight 1.7s cubic-bezier(.18,.72,.28,1) both}
156
+ .b270-dove-silhouette{fill:url(#b270-dove-light)!important;stroke:none}
157
+ .b270-dove-light-center{stop-color:#fff!important}
158
+ .b270-dove-light-middle{stop-color:#fffef8!important}
159
+ .b270-dove-light-edge{stop-color:#fff9e2!important}
155
160
  @keyframes b270-dove-flight{
156
161
  0%{top:72vh;left:50vw;opacity:.22;visibility:visible;transform:translate(-50%,-50%) scale(.02) rotate(-8deg)}
157
162
  8%{top:calc(72vh - 10px);left:50vw;opacity:.78;transform:translate(-50%,-50%) scale(.08) rotate(-5deg)}
158
- 64%{top:38vh;left:52vw;opacity:.92;transform:translate(-50%,-50%) scale(1) rotate(4deg)}
159
- 99%{visibility:visible}
160
- 100%{top:-25vh;left:115vw;opacity:0;visibility:hidden;transform:translate(-50%,-50%) scale(1.55) rotate(12deg)}
163
+ 33%{top:50vh;left:50vw;opacity:1;visibility:visible;transform:translate(-50%,-50%) scale(1) rotate(4deg)}
164
+ 100%{top:50vh;left:50vw;opacity:0;visibility:hidden;transform:translate(-50%,-50%) scale(1) rotate(4deg)}
161
165
  }
162
166
 
163
167
  /* readings */
data/config/routes.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Rails routes are intentionally kept in one cohesive DSL block.
3
4
  Bible270::Engine.routes.draw do
4
5
  root to: 'days#index'
5
6
 
@@ -22,6 +23,8 @@ Bible270::Engine.routes.draw do
22
23
  delete 'admin/readers/:id', to: 'admin#destroy'
23
24
  patch 'admin/readers/:id/profile', to: 'admin#update_profile', as: :admin_reader_profile
24
25
  patch 'admin/readers/:id/notifications', to: 'admin#update_notifications', as: :admin_reader_notifications
26
+ patch 'admin/readers/:id/completion-animation', to: 'admin#update_completion_animation',
27
+ as: :admin_reader_completion_animation
25
28
  delete 'admin/readers/:id/avatar', to: 'admin#remove_avatar', as: :admin_reader_avatar
26
29
  patch 'admin/readers/:id/version', to: 'admin#update_bible_version', as: :admin_reader_version
27
30
  patch 'admin/readers/:id/start', to: 'admin#update_start', as: :admin_reader_start
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddCompletionDoveDisabledToBible270Readers < ActiveRecord::Migration[7.0]
4
+ def change
5
+ add_column :bible270_readers, :completion_dove_disabled, :boolean, null: false, default: false
6
+ end
7
+ end
@@ -29,6 +29,7 @@ module Bible270
29
29
  'AddReflectionsSeenAtToBible270Readers' => :check_reflections_seen_at,
30
30
  'AddAllCommentNoticesToBible270Readers' => :check_all_comment_notices,
31
31
  'ChangePassageSourceDefaultToBibleCom' => :check_passage_source_default,
32
+ 'AddCompletionDoveDisabledToBible270Readers' => :check_completion_dove_disabled,
32
33
  'CreateActiveStorageTables' => :check_active_storage
33
34
  }.freeze
34
35
 
@@ -204,6 +205,10 @@ module Bible270
204
205
  column&.default == 'bible_com' ? [] : ['column bible270_readers.passage_source default bible_com']
205
206
  end
206
207
 
208
+ def check_completion_dove_disabled
209
+ table_missing(:bible270_readers, columns: %i[completion_dove_disabled])
210
+ end
211
+
207
212
  # The Bible270 installer can install Active Storage for reader avatars. A
208
213
  # copied database can therefore have the same timestamp mismatch for Rails'
209
214
  # generated Active Storage migration as it has for the engine migrations.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bible270
4
- VERSION = '1.4.1'
4
+ VERSION = '1.4.2'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bible270
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew vonderLuft
@@ -175,6 +175,7 @@ files:
175
175
  - db/migrate/20260101000017_add_reflections_seen_at_to_bible270_readers.rb
176
176
  - db/migrate/20260101000018_add_all_comment_notices_to_bible270_readers.rb
177
177
  - db/migrate/20260101000019_change_passage_source_default_to_bible_com.rb
178
+ - db/migrate/20260101000020_add_completion_dove_disabled_to_bible270_readers.rb
178
179
  - lib/bible270.rb
179
180
  - lib/bible270/avatars.rb
180
181
  - lib/bible270/configuration.rb