bible270 1.4.1 → 1.5.0
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 +4 -4
- data/CHANGELOG.md +45 -3
- data/README.md +11 -3
- data/app/controllers/bible270/admin_controller.rb +19 -2
- data/app/controllers/bible270/checkoffs_controller.rb +20 -16
- data/app/controllers/bible270/comments_controller.rb +11 -1
- data/app/controllers/bible270/days_controller.rb +3 -10
- data/app/helpers/bible270/plan_helper.rb +33 -27
- data/app/mailers/bible270/application_mailer.rb +1 -0
- data/app/models/bible270/comment.rb +20 -0
- data/app/models/bible270/reader.rb +29 -0
- data/app/views/bible270/admin/comments.html.erb +1 -1
- data/app/views/bible270/admin/show.html.erb +23 -0
- data/app/views/bible270/checkoffs/toggle.turbo_stream.erb +1 -1
- data/app/views/bible270/comments/_body.html.erb +6 -0
- data/app/views/bible270/comments/_comment.html.erb +1 -1
- data/app/views/bible270/comments/_edit_fields.html.erb +2 -1
- data/app/views/bible270/comments/_editor.html.erb +57 -0
- data/app/views/bible270/comments/_form.html.erb +3 -12
- data/app/views/bible270/comments/create.turbo_stream.erb +2 -1
- data/app/views/bible270/days/_completers.html.erb +2 -3
- data/app/views/bible270/days/_day_progress.html.erb +4 -2
- data/app/views/bible270/days/show.html.erb +4 -3
- data/app/views/bible270/notice_mailer/comment_notice.html.erb +1 -1
- data/app/views/bible270/notice_mailer/comment_notice.text.erb +1 -1
- data/app/views/bible270/shared/_comment_drafts.html.erb +9 -1
- data/app/views/bible270/shared/_day_completion_dove.html.erb +7 -5
- data/app/views/bible270/shared/_interaction_ui.html.erb +62 -1
- data/app/views/bible270/shared/_markdown_toolbar.html.erb +239 -0
- data/app/views/bible270/shared/_reflections.html.erb +1 -1
- data/app/views/bible270/shared/_styles.html.erb +40 -7
- data/config/routes.rb +4 -0
- data/db/migrate/20260101000020_add_completion_dove_disabled_to_bible270_readers.rb +7 -0
- data/db/migrate/20260101000021_add_body_format_to_bible270_comments.rb +11 -0
- data/lib/bible270/comment_formatter.rb +135 -0
- data/lib/bible270/migration_reconciler.rb +17 -0
- data/lib/bible270/version.rb +1 -1
- data/lib/bible270.rb +1 -0
- metadata +41 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7867bb4de49fe177447a5f7b1cfa9fa031e1926cdb8c4c73f2071eb8a7620c82
|
|
4
|
+
data.tar.gz: '0499e0931abdd3890f07945f7fc82f6de8dc5da1a156fbb8147e1b5b2be79596'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 422848afb4e96f1a94cbfc26078f7c343afdb519e431c25aa6f2de10be0578d51fa65aa6d04683490b792685d5888474b8f711f0e1839ccd6ccaeb45c52c26d0
|
|
7
|
+
data.tar.gz: 81cd1f5df1028fa4064bc77ec5ee20866406d21c1ae5d4d992e8137bde59e5c6d6c505c7fc92b85111c931b7450da60b125692cb0bacbe63652e63012f9257da
|
data/CHANGELOG.md
CHANGED
|
@@ -2,13 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to bible270. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html), treating pre-1.0 minor bumps as the place breaking changes may land.
|
|
4
4
|
|
|
5
|
-
## Unreleased
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- add an on-demand Markdown formatting drawer and separate server-rendered Preview control for reflections and replies, with safe rendering, formatted notification emails, mention and browser-draft compatibility, and literal display of existing plain-text reflections; upgrading hosts must copy and run the new `body_format` comment migration
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- keep `json` below 3.0 while supported Rails releases still pass positional parse options, preventing session-cookie decoding from crashing every request under `json` 3.0
|
|
14
|
+
|
|
15
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.4.2...main)**
|
|
16
|
+
|
|
17
|
+
## [1.4.2] - 2026-09-03
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- let administrators disable the day-completion dove animation for individual readers without affecting progress tracking; upgrading hosts must copy and run the new `completion_dove_disabled` reader migration before changing this setting
|
|
6
22
|
|
|
7
23
|
### Changed
|
|
8
24
|
|
|
9
|
-
-
|
|
25
|
+
- keep the completion dove bright in dark mode, grow it to an 80%-of-viewport fit with a smoother rounded silhouette, and finish with a centered fade instead of veering offscreen
|
|
26
|
+
- show every reader who finished a day after the Reflections section, using compact 56px avatars ordered from earliest to latest completion
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- show the dove only once for each persisted day completion, including during concurrent final checkoffs, Turbo response replays, and stale-session retries
|
|
31
|
+
|
|
32
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.4.1...v1.4.2)**
|
|
33
|
+
|
|
34
|
+
## [1.4.1] - 2026-09-02
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- run the day-completion dove as one continuous, server-confirmed animation from the lower center, avoiding a stagger between submission and the Turbo or HTML response
|
|
39
|
+
- improve installer guidance for OmniAuth providers, deferred bundling, Active Storage, pending migrations, and OAuth callback setup
|
|
40
|
+
- detect existing engine mounts more reliably and move them above catch-all routes when necessary
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- reject invalid enrollment states and malformed or out-of-range admin day values instead of silently interpreting them
|
|
45
|
+
- apply admin profile name and avatar changes atomically, preventing partial updates when validation fails
|
|
46
|
+
- report broadcast queueing, delivery failures, and partial success accurately without recording an entirely failed broadcast as sent
|
|
47
|
+
- restrict reflection return redirects to safe application URLs
|
|
48
|
+
- honor the install generator's `--mailer-from` option, reject unsafe provider identifiers, and safely quote generated configuration values
|
|
10
49
|
|
|
11
|
-
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.4.0...
|
|
50
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.4.0...v1.4.1)**
|
|
12
51
|
|
|
13
52
|
## [1.4.0] - 2026-09-02
|
|
14
53
|
|
|
@@ -622,6 +661,9 @@ This is the initial public release.
|
|
|
622
661
|
|
|
623
662
|
* Email sign-in requires working Action Mailer delivery in the host application. Set `config.mailer_from`; delivery is inline by default, or set `email_sign_in_deliver_later` when a queue backend is available.
|
|
624
663
|
|
|
664
|
+
[Unreleased]: https://github.com/avonderluft/bible270/compare/v1.4.2...HEAD
|
|
665
|
+
[1.4.2]: https://github.com/avonderluft/bible270/releases/tag/v1.4.2
|
|
666
|
+
[1.4.1]: https://github.com/avonderluft/bible270/releases/tag/v1.4.1
|
|
625
667
|
[1.4.0]: https://github.com/avonderluft/bible270/releases/tag/v1.4.0
|
|
626
668
|
[1.3.2]: https://github.com/avonderluft/bible270/releases/tag/v1.3.2
|
|
627
669
|
[1.3.1]: https://github.com/avonderluft/bible270/releases/tag/v1.3.1
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Bible 270
|
|
2
2
|
|
|
3
|
-
[](https://rubygems.org/gems/bible270) [](https://rubygems.org/gems/bible270) [](https://coveralls.io/github/avonderluft/bible270?branch=main) [](https://rubygems.org/gems/bible270) [](https://github.com/avonderluft/bible270/releases)
|
|
4
4
|
|
|
5
5
|
A mountable **Rails engine** that drops a 270-day (9 month) interactive Bible reading plan into any Rails app, built with [ComfortableMediaSurfer](https://github.com/shakacode/comfortable-media-surfer) CMS in mind. Readers tick off each day's readings, post reflections, and see how everyone else is getting on, much like the social plans on Bible.com.
|
|
6
6
|
|
|
@@ -283,7 +283,14 @@ filters, and paginates older threads. Signed-in readers see which conversations
|
|
|
283
283
|
their previous visit. Reflection and reply drafts are retained for up to 30 days in that browser and
|
|
284
284
|
cleared after a successful post. On a shared device, clearing browser storage also removes saved drafts.
|
|
285
285
|
Typing `@` in the composer offers up to five unambiguous reader handles; manual mentions continue to
|
|
286
|
-
work without JavaScript.
|
|
286
|
+
work without JavaScript. A quiet **Formatting** disclosure opens a toolbar for bold, italic,
|
|
287
|
+
quotations, bulleted and numbered lists, and links without requiring readers to know Markdown syntax.
|
|
288
|
+
A separate **Preview** control checks the current draft through the same server-side sanitizer used
|
|
289
|
+
after posting; formatting tools stay out of sight until requested. The source remains
|
|
290
|
+
editable in the ordinary textarea, drafts preserve its format, and all rendered HTML is restricted to that small safe
|
|
291
|
+
set. Existing reflections remain literal plain text until a reader intentionally formats or converts
|
|
292
|
+
them. Upgrading hosts must copy and run migration `20260101000021`, which records whether each body is
|
|
293
|
+
plain text or Markdown; until then, the composer safely falls back to plain text.
|
|
287
294
|
|
|
288
295
|
When `config.mention_notifications` is enabled, each reader can request email for every new reflection
|
|
289
296
|
and reply, only replies to their reflections and mentions of them, or none. Administrators can change
|
|
@@ -483,7 +490,7 @@ The CMS serves your content; this is a separate mounted app. Either **just link
|
|
|
483
490
|
|-------|-------|
|
|
484
491
|
| `bible270_readers` | identity, avatar, provider/uid or polymorphic `owner`, start date, translation, passage-reader, and reminder preferences |
|
|
485
492
|
| `bible270_checkoffs` | one row per reader per day per track (`ot`/`nt`/`pp`), unique-indexed |
|
|
486
|
-
| `bible270_comments` | a reflection on a day, optionally scoped to a track, public to all |
|
|
493
|
+
| `bible270_comments` | a plain-text or safely rendered Markdown reflection on a day, optionally scoped to a track, public to all |
|
|
487
494
|
| `bible270_sign_in_tokens` | magic-link tokens: email, **digest only**, expiry, consumed-at |
|
|
488
495
|
|
|
489
496
|
A day is "complete" once all three tracks are ticked. Progress and comments are **public** by
|
|
@@ -496,6 +503,7 @@ GET / days#index overview + community + calendar
|
|
|
496
503
|
GET /day/:day days#show readings, who's finished, reflections
|
|
497
504
|
POST /day/:day/toggle/:track checkoffs#toggle
|
|
498
505
|
POST /day/:day/comments comments#create
|
|
506
|
+
POST /comments/preview comments#preview
|
|
499
507
|
DELETE /comments/:id comments#destroy
|
|
500
508
|
GET /community readers#index reader directory
|
|
501
509
|
GET /mention-suggestions readers#mention_suggestions (signed-in JSON)
|
|
@@ -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
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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: {
|
|
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?
|
|
56
|
+
[true, existing.nil? ? create_checkoff : nil]
|
|
55
57
|
elsif requested_state == '0' || existing
|
|
56
|
-
|
|
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
|
-
|
|
81
|
+
nil
|
|
78
82
|
end
|
|
79
83
|
end
|
|
80
84
|
end
|
|
@@ -11,6 +11,14 @@ module Bible270
|
|
|
11
11
|
current_reader&.mark_reflections_seen!(visited_at)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def preview
|
|
15
|
+
return unless require_reader!
|
|
16
|
+
|
|
17
|
+
attributes = { body: params.dig(:comment, :body).to_s }
|
|
18
|
+
attributes[:body_format] = CommentFormatter::MARKDOWN if Comment.body_format_column?
|
|
19
|
+
render partial: 'bible270/comments/body', locals: { comment: Comment.new(attributes) }
|
|
20
|
+
end
|
|
21
|
+
|
|
14
22
|
def create
|
|
15
23
|
return unless require_reader!
|
|
16
24
|
|
|
@@ -134,7 +142,9 @@ module Bible270
|
|
|
134
142
|
end
|
|
135
143
|
|
|
136
144
|
def comment_params
|
|
137
|
-
|
|
145
|
+
permitted = %i[body track parent_id]
|
|
146
|
+
permitted << :body_format if Comment.body_format_column?
|
|
147
|
+
params.require(:comment).permit(*permitted)
|
|
138
148
|
end
|
|
139
149
|
|
|
140
150
|
# Scoped to the reader, so someone else's reflection is simply not found —
|
|
@@ -18,19 +18,12 @@ module Bible270
|
|
|
18
18
|
@readings = Plan.readings_for(@day)
|
|
19
19
|
@comments = Comment.threads_for_day(@day)
|
|
20
20
|
@editing_comment_id = editing_comment_id
|
|
21
|
-
@new_comment = Comment.
|
|
21
|
+
@new_comment = Comment.new_for_composer(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
|
-
|
|
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
|
|
@@ -53,40 +53,46 @@ module Bible270
|
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
56
|
+
# Formats every reflection surface through the same sanitizer. Web views link
|
|
57
|
+
# resolved mentions; email can opt out because it has no mounted request path.
|
|
58
|
+
def b270_comment_body(comment, link_mentions: true)
|
|
59
|
+
format = comment.rendered_body_format
|
|
60
|
+
html = if link_mentions
|
|
61
|
+
readers = Bible270::Reader.mentioned_in(comment.body)
|
|
62
|
+
Bible270::CommentFormatter.html(comment.body, format: format) do |handle|
|
|
63
|
+
reader = readers.find { |candidate| candidate.answers_to?(handle) }
|
|
64
|
+
reader_path(reader) if reader
|
|
65
|
+
end
|
|
66
|
+
else
|
|
67
|
+
Bible270::CommentFormatter.html(comment.body, format: format)
|
|
68
|
+
end
|
|
69
|
+
html.html_safe
|
|
70
|
+
end
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
def b270_comment_text(comment)
|
|
73
|
+
Bible270::CommentFormatter.text(comment.body, format: comment.rendered_body_format)
|
|
74
|
+
end
|
|
70
75
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
def b270_editor_rows(body, columns: 48, minimum: 4)
|
|
77
|
+
rows = body.to_s.split("\n", -1).sum do |line|
|
|
78
|
+
[1, (line.length.to_f / columns).ceil].max
|
|
79
|
+
end
|
|
80
|
+
[minimum, rows].max
|
|
81
|
+
end
|
|
74
82
|
|
|
75
|
-
|
|
83
|
+
# Backward-compatible plain-text entry point for host views that called the old
|
|
84
|
+
# mention helper directly.
|
|
85
|
+
def b270_with_mentions(body)
|
|
86
|
+
readers = Bible270::Reader.mentioned_in(body)
|
|
87
|
+
html = Bible270::CommentFormatter.html(body, format: Bible270::CommentFormatter::PLAIN) do |handle|
|
|
76
88
|
reader = readers.find { |candidate| candidate.answers_to?(handle) }
|
|
77
|
-
|
|
78
|
-
link_to(match[0], reader_path(reader), class: 'b270-mention')
|
|
79
|
-
else
|
|
80
|
-
ERB::Util.html_escape(match[0])
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
cursor = match.end(0)
|
|
89
|
+
reader_path(reader) if reader
|
|
84
90
|
end
|
|
85
|
-
|
|
86
|
-
pieces << ERB::Util.html_escape(text[cursor..].to_s)
|
|
87
|
-
safe_join(pieces)
|
|
91
|
+
html.html_safe
|
|
88
92
|
end
|
|
89
93
|
|
|
94
|
+
# The mark beside the title in the header. Larger than the favicon and inlined
|
|
95
|
+
# as SVG, so it stays crisp at any size and needs no asset pipeline.
|
|
90
96
|
def b270_mark(size: 38)
|
|
91
97
|
configured = Bible270.config.header_mark
|
|
92
98
|
return nil if configured == false
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Bible270
|
|
4
4
|
class ApplicationMailer < ActionMailer::Base
|
|
5
5
|
default from: -> { Bible270.config.mailer_from }
|
|
6
|
+
helper Bible270::PlanHelper
|
|
6
7
|
# `layout nil` means "normal lookup", which finds the host app's mailer layout and
|
|
7
8
|
# nests our complete HTML document inside theirs. Only `false` disables it.
|
|
8
9
|
layout false
|
|
@@ -11,6 +11,8 @@ module Bible270
|
|
|
11
11
|
# the paper background; an outline glyph like U+2661 is drawn by the font
|
|
12
12
|
# rather than the emoji set and needs hand-tuning to match.
|
|
13
13
|
HEART = '❤️'
|
|
14
|
+
BODY_FORMAT_COLUMN = 'body_format'
|
|
15
|
+
BODY_FORMATS = CommentFormatter::FORMATS
|
|
14
16
|
ThreadPage = Struct.new(:threads, :activity_by_id, :page, :pages, :total, keyword_init: true)
|
|
15
17
|
|
|
16
18
|
self.table_name = 'bible270_comments'
|
|
@@ -26,6 +28,7 @@ module Bible270
|
|
|
26
28
|
|
|
27
29
|
validates :day, inclusion: { in: 1..Plan::DAYS }
|
|
28
30
|
validates :body, presence: true, length: { maximum: 4000 }
|
|
31
|
+
validates :body_format, inclusion: { in: BODY_FORMATS }, if: :body_format_column?
|
|
29
32
|
# A reflection can be about one track or about the whole day. The form's
|
|
30
33
|
# "The whole day" option submits an empty string, so normalise it to NULL
|
|
31
34
|
# before validating — allow_nil alone rejects "" with
|
|
@@ -54,6 +57,23 @@ module Bible270
|
|
|
54
57
|
}
|
|
55
58
|
scope :recent, -> { approved.order(created_at: :desc) }
|
|
56
59
|
|
|
60
|
+
def self.body_format_column?
|
|
61
|
+
column_names.include?(BODY_FORMAT_COLUMN)
|
|
62
|
+
rescue ActiveRecord::StatementInvalid
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.new_for_composer(attributes = {})
|
|
67
|
+
attributes = attributes.merge(BODY_FORMAT_COLUMN => CommentFormatter::MARKDOWN) if body_format_column?
|
|
68
|
+
new(attributes)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def body_format_column? = self.class.body_format_column?
|
|
72
|
+
|
|
73
|
+
def rendered_body_format
|
|
74
|
+
body_format_column? ? body_format.presence || CommentFormatter::PLAIN : CommentFormatter::PLAIN
|
|
75
|
+
end
|
|
76
|
+
|
|
57
77
|
# One page of top-level reflections ordered by the newest activity in each
|
|
58
78
|
# conversation. Loading only IDs and timestamps keeps older archives cheap to
|
|
59
79
|
# sort, while the displayed page still eager-loads its readers, replies and likes.
|
|
@@ -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.
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
<% if comment.reply? %>· reply to <strong><%= comment.parent&.reader&.display_name || 'a deleted reflection' %></strong><% end %>
|
|
23
23
|
<% if comment.hidden? %><span class="b270-badge">Hidden</span><% end %>
|
|
24
24
|
</div>
|
|
25
|
-
<
|
|
25
|
+
<div class="b270-cbody"><%= b270_comment_body(comment) %></div>
|
|
26
26
|
<div style="display:flex;gap:10px;align-items:center;margin-top:6px">
|
|
27
27
|
<% if comment.hidden? %>
|
|
28
28
|
<%= button_to 'Restore', admin_unhide_comment_path(comment), method: :patch,
|
|
@@ -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 %>
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
<% if editing %>
|
|
33
33
|
<%= render partial: "bible270/comments/edit_fields", locals: { comment: comment } %>
|
|
34
34
|
<% else %>
|
|
35
|
-
<
|
|
35
|
+
<div class="b270-cbody"><%= b270_comment_body(comment) %></div>
|
|
36
36
|
<% end %>
|
|
37
37
|
|
|
38
38
|
<%# Only a top-level reflection can be replied to: one level deep, so a reply
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
<p class="b270-editerror"><%= comment.errors.full_messages.to_sentence %></p>
|
|
14
14
|
<% end %>
|
|
15
15
|
|
|
16
|
-
<%= f
|
|
16
|
+
<%= render "bible270/comments/editor", form: f, comment: comment,
|
|
17
|
+
label_text: (comment.reply? ? "Reply" : "Reflection") %>
|
|
17
18
|
<div class="row">
|
|
18
19
|
<% unless comment.reply? %>
|
|
19
20
|
<%= f.select :track,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<%# locals: (form:, comment:, label_text:, placeholder: nil) %>
|
|
2
|
+
<% editor_key = comment.persisted? ? "edit-#{comment.id}" : "new" %>
|
|
3
|
+
<% textarea_id = "b270-comment-body-#{editor_key}" %>
|
|
4
|
+
<% options_id = "b270-mention-options-#{editor_key}" %>
|
|
5
|
+
<% write_id = "b270-comment-write-#{editor_key}" %>
|
|
6
|
+
<% preview_id = "b270-comment-preview-#{editor_key}" %>
|
|
7
|
+
<% drawer_id = "b270-comment-formatting-#{editor_key}" %>
|
|
8
|
+
<% markdown_available = Bible270::Comment.body_format_column? %>
|
|
9
|
+
|
|
10
|
+
<div class="b270-mention-composer" data-b270-mention-composer
|
|
11
|
+
data-suggestions-url="<%= mention_suggestions_path %>">
|
|
12
|
+
<div class="b270-markdown-editor" data-b270-markdown-editor data-b270-editor-key="<%= editor_key %>"
|
|
13
|
+
data-b270-preview-url="<%= comment_preview_path if markdown_available %>"
|
|
14
|
+
data-b270-formatting-open="false" data-b270-editor-mode="write">
|
|
15
|
+
<% if markdown_available %>
|
|
16
|
+
<div class="b270-editor-utilities" data-b270-editor-utilities hidden>
|
|
17
|
+
<button type="button" class="b270-editor-utility" aria-expanded="false"
|
|
18
|
+
aria-controls="<%= drawer_id %>" data-b270-formatting-toggle>
|
|
19
|
+
Formatting <span class="b270-format-chevron" aria-hidden="true">▾</span>
|
|
20
|
+
</button>
|
|
21
|
+
<button type="button" class="b270-editor-utility" aria-pressed="false"
|
|
22
|
+
aria-controls="<%= write_id %> <%= preview_id %>" data-b270-preview-toggle>Preview</button>
|
|
23
|
+
</div>
|
|
24
|
+
<div id="<%= drawer_id %>" class="b270-formatting-drawer" aria-hidden="true"
|
|
25
|
+
data-b270-formatting-drawer inert>
|
|
26
|
+
<div class="b270-formatting-drawer-inner">
|
|
27
|
+
<div class="b270-markdown-toolbar" role="toolbar" aria-label="Reflection formatting"
|
|
28
|
+
data-b270-markdown-toolbar>
|
|
29
|
+
<button type="button" data-b270-markdown-action="bold" aria-label="Bold selected text" title="Bold (Ctrl+B)"><span class="b270-format-glyph b270-format-bold" aria-hidden="true">B</span></button>
|
|
30
|
+
<button type="button" data-b270-markdown-action="italic" aria-label="Italicize selected text" title="Italic (Ctrl+I)"><span class="b270-format-glyph b270-format-italic" aria-hidden="true">I</span></button>
|
|
31
|
+
<button type="button" data-b270-markdown-action="quote" aria-label="Format as a quotation" title="Quotation">“ ”</button>
|
|
32
|
+
<button type="button" data-b270-markdown-action="bullet-list" aria-label="Format as a bulleted list" title="Bulleted list">• List</button>
|
|
33
|
+
<button type="button" data-b270-markdown-action="number-list" aria-label="Format as a numbered list" title="Numbered list">1. List</button>
|
|
34
|
+
<button type="button" data-b270-markdown-action="link" aria-label="Add a link" title="Link">Link</button>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<%= form.hidden_field :body_format, data: { b270_body_format: true } %>
|
|
39
|
+
<% end %>
|
|
40
|
+
|
|
41
|
+
<div id="<%= write_id %>" data-b270-write-panel>
|
|
42
|
+
<%= form.label :body, label_text, for: textarea_id, class: "b270-sr-only" %>
|
|
43
|
+
<%= form.text_area :body, id: textarea_id, placeholder: placeholder,
|
|
44
|
+
rows: (b270_editor_rows(comment.body) if comment.persisted?),
|
|
45
|
+
role: "combobox", autocomplete: "off", data: { b270_autosize: comment.persisted? },
|
|
46
|
+
aria: { autocomplete: "list", expanded: "false", haspopup: "listbox", controls: options_id } %>
|
|
47
|
+
</div>
|
|
48
|
+
<% if markdown_available %>
|
|
49
|
+
<div id="<%= preview_id %>" class="b270-markdown-preview b270-cbody"
|
|
50
|
+
aria-label="Reflection preview" aria-live="polite" data-b270-preview-panel hidden></div>
|
|
51
|
+
<% end %>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="b270-mention-options" id="<%= options_id %>" role="listbox"
|
|
55
|
+
data-b270-mention-options hidden></div>
|
|
56
|
+
<p class="b270-sr-only" data-b270-mention-status aria-live="polite"></p>
|
|
57
|
+
</div>
|
|
@@ -18,18 +18,9 @@
|
|
|
18
18
|
</p>
|
|
19
19
|
<% end %>
|
|
20
20
|
<%= f.hidden_field :parent_id %>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<%= f.text_area :body,
|
|
25
|
-
placeholder: posting_reply ? "Write your reply…" : "Share a reflection on today's reading…",
|
|
26
|
-
role: "combobox", autocomplete: "off",
|
|
27
|
-
aria: { autocomplete: "list", expanded: "false", haspopup: "listbox",
|
|
28
|
-
controls: "b270-mention-options" } %>
|
|
29
|
-
<div class="b270-mention-options" id="b270-mention-options" role="listbox"
|
|
30
|
-
data-b270-mention-options hidden></div>
|
|
31
|
-
<p class="b270-sr-only" data-b270-mention-status aria-live="polite"></p>
|
|
32
|
-
</div>
|
|
21
|
+
<%= render "bible270/comments/editor", form: f, comment: comment,
|
|
22
|
+
label_text: (posting_reply ? "Reply" : "Reflection"),
|
|
23
|
+
placeholder: (posting_reply ? "Write your reply…" : "Share a reflection on today's reading…") %>
|
|
33
24
|
<p class="b270-draft-status" data-b270-draft-status aria-live="polite"></p>
|
|
34
25
|
<div class="row">
|
|
35
26
|
<%= f.select :track,
|
|
@@ -10,5 +10,6 @@
|
|
|
10
10
|
<% end %>
|
|
11
11
|
|
|
12
12
|
<%= turbo_stream.replace "new_comment_form" do %>
|
|
13
|
-
<%= render "bible270/comments/form", day: @day,
|
|
13
|
+
<%= render "bible270/comments/form", day: @day,
|
|
14
|
+
comment: Bible270::Comment.new_for_composer(day: @day), posted: true %>
|
|
14
15
|
<% end %>
|