bible270 0.17.0 → 0.17.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 +4 -4
- data/Rakefile +3 -0
- data/app/controllers/bible270/admin_controller.rb +74 -0
- data/app/controllers/bible270/comments_controller.rb +3 -1
- data/app/controllers/bible270/likes_controller.rb +22 -0
- data/app/controllers/bible270/sessions_controller.rb +3 -1
- data/app/helpers/bible270/plan_helper.rb +12 -0
- data/app/mailers/bible270/broadcast.html.erb +17 -0
- data/app/mailers/bible270/broadcast.text.erb +7 -0
- data/app/mailers/bible270/notice_mailer.rb +20 -0
- data/app/models/bible270/comment.rb +35 -2
- data/app/models/bible270/like.rb +14 -0
- data/app/models/bible270/reader.rb +28 -0
- data/app/views/bible270/admin/comments.html.erb +1 -1
- data/app/views/bible270/admin/index.html.erb +28 -1
- data/app/views/bible270/admin/show.html.erb +22 -0
- data/app/views/bible270/comments/_comment.html.erb +11 -8
- data/app/views/bible270/comments/index.html.erb +1 -1
- data/app/views/bible270/likes/_likes.html.erb +27 -0
- data/app/views/bible270/likes/toggle.turbo_stream.erb +3 -0
- data/app/views/bible270/notice_mailer/broadcast.html.erb +17 -0
- data/app/views/bible270/notice_mailer/broadcast.text.erb +7 -0
- data/app/views/bible270/notice_mailer/new_reader.html.erb +1 -1
- data/app/views/bible270/notice_mailer/new_reader.text.erb +1 -1
- data/app/views/bible270/readers/index.html.erb +1 -1
- data/app/views/bible270/shared/_header.html.erb +2 -2
- data/app/views/bible270/shared/_reflections.html.erb +1 -1
- data/app/views/bible270/shared/_styles.html.erb +12 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20260101000013_create_bible270_likes.rb +21 -0
- data/lib/bible270/version.rb +1 -1
- data/lib/bible270.rb +18 -4
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e3c27f2336f3e2a809205242bbc42c46d241e49b9ea2b1bf49ca288523d7c88
|
|
4
|
+
data.tar.gz: 8e8c25b59eafde481a17546ac14b563527dd591ca64d9cf7541d95a01dd1f36c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 06af092550c30ded960e3611301d941e10fdea25e79fcfcab8aa97c4d68fee7b3780961f22bc4f81be4ba3f065ebe57642386ba318c69f80e2f1bd1e07ce0dec
|
|
7
|
+
data.tar.gz: ed69c75af0e88e8dfeb347b756c6c10692f46b6e90196e96851aecc026a664752ce6f2fbb3de3b324734eff316555c229d19aa15270d26061b370b4db643f3ae
|
data/Rakefile
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'bundler/gem_tasks' # provides build / install / release
|
|
4
4
|
require 'rake/testtask'
|
|
5
|
+
require 'coveralls/rake/task'
|
|
5
6
|
|
|
6
7
|
Rake::TestTask.new(:test) do |t|
|
|
7
8
|
t.libs << 'test' << 'lib'
|
|
@@ -9,4 +10,6 @@ Rake::TestTask.new(:test) do |t|
|
|
|
9
10
|
t.warning = false
|
|
10
11
|
end
|
|
11
12
|
|
|
13
|
+
Coveralls::RakeTask.new
|
|
14
|
+
|
|
12
15
|
task default: :test
|
|
@@ -8,6 +8,9 @@ module Bible270
|
|
|
8
8
|
# neither set the panel is unreachable, and every action 404s rather than 403s
|
|
9
9
|
# so its existence isn't advertised.
|
|
10
10
|
class AdminController < ApplicationController
|
|
11
|
+
LAST_BROADCAST_AT = 'last_broadcast_at'
|
|
12
|
+
LAST_BROADCAST_SUBJECT = 'last_broadcast_subject'
|
|
13
|
+
|
|
11
14
|
before_action :require_admin!
|
|
12
15
|
before_action :load_reader,
|
|
13
16
|
only: %i[show destroy update_start update_profile remove_avatar complete_through toggle_day]
|
|
@@ -32,6 +35,9 @@ module Bible270
|
|
|
32
35
|
counts = Checkoff.group(:reader_id, :day).count
|
|
33
36
|
@days_completed = Hash.new(0)
|
|
34
37
|
counts.each { |(rid, day), n| @days_completed[rid] += 1 if n >= Plan.total_parts(day) }
|
|
38
|
+
@reachable = Reader.where.not(email: [nil, '']).count
|
|
39
|
+
@last_broadcast_at = parsed_time(Setting.read(LAST_BROADCAST_AT))
|
|
40
|
+
@last_broadcast_subject = Setting.read(LAST_BROADCAST_SUBJECT)
|
|
35
41
|
end
|
|
36
42
|
|
|
37
43
|
def show
|
|
@@ -75,6 +81,47 @@ module Bible270
|
|
|
75
81
|
end
|
|
76
82
|
|
|
77
83
|
# Put the reader on a given day as of today, or set an explicit start date.
|
|
84
|
+
def broadcast
|
|
85
|
+
subject = params[:subject].to_s.strip
|
|
86
|
+
body = params[:body].to_s.strip
|
|
87
|
+
|
|
88
|
+
if subject.empty? || body.empty?
|
|
89
|
+
return redirect_to(admin_path, alert: 'A message needs both a subject and something to say.')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
recipients = Reader.where.not(email: [nil, '']).order(:id)
|
|
93
|
+
return redirect_to(admin_path, alert: 'Nobody has an email address.') if recipients.empty?
|
|
94
|
+
|
|
95
|
+
sent = deliver_broadcast(recipients, subject, body)
|
|
96
|
+
Setting.write(LAST_BROADCAST_AT, Time.current.iso8601)
|
|
97
|
+
Setting.write(LAST_BROADCAST_SUBJECT, subject)
|
|
98
|
+
|
|
99
|
+
redirect_to admin_path,
|
|
100
|
+
notice: "Sent to #{sent} #{'reader'.pluralize(sent)}."
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def update_bible_version
|
|
104
|
+
reader = Reader.find_by(id: params[:id])
|
|
105
|
+
return redirect_to(admin_path, alert: 'No such reader.') if reader.nil?
|
|
106
|
+
|
|
107
|
+
code = params[:bible_version].to_s
|
|
108
|
+
# Blank means "no preference": the reader follows the site default, which is
|
|
109
|
+
# not the same as choosing whichever translation the site happens to use now.
|
|
110
|
+
if code.empty?
|
|
111
|
+
reader.update(bible_version: nil)
|
|
112
|
+
return redirect_to admin_reader_path(reader),
|
|
113
|
+
notice: "#{reader.display_name} now follows the site default " \
|
|
114
|
+
"(#{Bible270.config.bible_version})."
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if reader.update_bible_version(code)
|
|
118
|
+
redirect_to admin_reader_path(reader),
|
|
119
|
+
notice: "#{reader.display_name} now reads the #{reader.bible_version_label}."
|
|
120
|
+
else
|
|
121
|
+
redirect_to admin_reader_path(reader), alert: 'That is not a translation on the list.'
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
78
125
|
def update_start
|
|
79
126
|
if params[:start_date].present?
|
|
80
127
|
# set_start_date!, not update_start_date!: an admin is not subject to
|
|
@@ -138,6 +185,33 @@ module Bible270
|
|
|
138
185
|
end
|
|
139
186
|
end
|
|
140
187
|
|
|
188
|
+
# Failures are counted rather than raised: one bad address must not stop the
|
|
189
|
+
# rest of the message going out, and the admin is told how many were sent.
|
|
190
|
+
# A stored timestamp that cannot be read is treated as absent rather than
|
|
191
|
+
# blowing up the admin page.
|
|
192
|
+
def parsed_time(value)
|
|
193
|
+
return nil if value.blank?
|
|
194
|
+
|
|
195
|
+
Time.parse(value.to_s)
|
|
196
|
+
rescue ArgumentError, TypeError
|
|
197
|
+
nil
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def deliver_broadcast(recipients, subject, body)
|
|
201
|
+
later = Bible270.config.registration_notice_deliver_later
|
|
202
|
+
sent = 0
|
|
203
|
+
|
|
204
|
+
recipients.find_each do |reader|
|
|
205
|
+
mail = NoticeMailer.broadcast(reader_id: reader.id, subject: subject, body: body)
|
|
206
|
+
later ? mail.deliver_later : mail.deliver_now
|
|
207
|
+
sent += 1
|
|
208
|
+
rescue StandardError => e
|
|
209
|
+
Rails.logger.error("[bible270] broadcast to reader #{reader.id} failed: #{e.class}: #{e.message}")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
sent
|
|
213
|
+
end
|
|
214
|
+
|
|
141
215
|
private
|
|
142
216
|
|
|
143
217
|
def require_admin!
|
|
@@ -92,7 +92,9 @@ module Bible270
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
ids = roots.sort_by { |_root, at| -at.to_i }.first(wanted).map(&:first)
|
|
95
|
-
threads = Comment.approved.where(id: ids)
|
|
95
|
+
threads = Comment.approved.where(id: ids)
|
|
96
|
+
.includes(:reader, { likes: :reader }, { replies: [:reader, { likes: :reader }] })
|
|
97
|
+
.index_by(&:id)
|
|
96
98
|
ids.filter_map { |id| threads[id] }
|
|
97
99
|
end
|
|
98
100
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bible270
|
|
4
|
+
# A heart on someone's reflection. One route, because liking and unliking are
|
|
5
|
+
# the same gesture — tapping the heart again takes it back.
|
|
6
|
+
class LikesController < ApplicationController
|
|
7
|
+
def toggle
|
|
8
|
+
return unless require_reader!
|
|
9
|
+
|
|
10
|
+
@comment = Comment.approved.find_by(id: params[:id])
|
|
11
|
+
head :not_found and return unless @comment
|
|
12
|
+
|
|
13
|
+
@comment.toggle_like!(current_reader)
|
|
14
|
+
@comment.reload
|
|
15
|
+
|
|
16
|
+
respond_to do |format|
|
|
17
|
+
format.turbo_stream
|
|
18
|
+
format.html { redirect_to day_path(@comment.day, anchor: "comment-#{@comment.id}") }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -36,7 +36,9 @@ module Bible270
|
|
|
36
36
|
remember_reader!(reader)
|
|
37
37
|
@current_reader = reader
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
# first_name is filled in for every route now, but a bridged host user may
|
|
40
|
+
# still arrive without one.
|
|
41
|
+
redirect_to destination, notice: "Welcome #{reader.first_name.presence || reader.display_name}."
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def destroy
|
|
@@ -137,6 +137,18 @@ module Bible270
|
|
|
137
137
|
end
|
|
138
138
|
end
|
|
139
139
|
|
|
140
|
+
# Timestamps in the plan's zone rather than UTC. Dates — started_on and the
|
|
141
|
+
# like — need no conversion and are formatted inline.
|
|
142
|
+
def b270_date(time)
|
|
143
|
+
local = Bible270.local_time(time)
|
|
144
|
+
local&.strftime('%b %-d, %Y')
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def b270_datetime(time)
|
|
148
|
+
local = Bible270.local_time(time)
|
|
149
|
+
local&.strftime('%b %-d, %Y at %-I:%M %p')
|
|
150
|
+
end
|
|
151
|
+
|
|
140
152
|
# Only the writer may change their own words. An admin can remove a reflection
|
|
141
153
|
# but not rewrite it: putting words in someone's mouth is worse than taking
|
|
142
154
|
# them away, and taking them away is already the moderator's job.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<body style="margin:0;padding:24px;background:#ece6da;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#231f18">
|
|
4
|
+
<div style="max-width:520px;margin:0 auto;background:#f4efe6;border:1px solid #d7cfbf;border-radius:14px;padding:28px">
|
|
5
|
+
<p style="margin:0 0 16px;font-size:15px">
|
|
6
|
+
<%= @reader.first_name.presence || @reader.display_name %>,
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<div style="font-size:15px;line-height:1.6"><%= simple_format(@body) %></div>
|
|
10
|
+
|
|
11
|
+
<p style="margin:24px 0 0;padding-top:16px;border-top:1px solid #d7cfbf;font-size:12px;color:#9a9082">
|
|
12
|
+
<%= @app_name %><% if @plan_url %> ·
|
|
13
|
+
<a href="<%= @plan_url %>" style="color:#a5812c"><%= @plan_url %></a><% end %>
|
|
14
|
+
</p>
|
|
15
|
+
</div>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -31,6 +31,22 @@ module Bible270
|
|
|
31
31
|
subject: "#{@app_name}: #{@author.display_name} mentioned you on day #{@comment.day}"
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# A message from whoever runs the plan to one reader. Sent individually rather
|
|
35
|
+
# than as one email with everyone in bcc: a bcc list of a hundred addresses is
|
|
36
|
+
# a spam signal, and it means nobody can be greeted by name or unsubscribe
|
|
37
|
+
# meaningfully.
|
|
38
|
+
def broadcast(reader_id:, subject:, body:)
|
|
39
|
+
@reader = Reader.find_by(id: reader_id)
|
|
40
|
+
return message.perform_deliveries = false if @reader.nil? || @reader.email.blank?
|
|
41
|
+
return message.perform_deliveries = false if subject.to_s.strip.empty? || body.to_s.strip.empty?
|
|
42
|
+
|
|
43
|
+
@body = body.to_s
|
|
44
|
+
@app_name = Bible270.config.app_name
|
|
45
|
+
@plan_url = plan_url
|
|
46
|
+
|
|
47
|
+
mail to: @reader.email, subject: subject.to_s.strip
|
|
48
|
+
end
|
|
49
|
+
|
|
34
50
|
private
|
|
35
51
|
|
|
36
52
|
# A mailer has no request, so the host is config.mailer_host or nothing. No
|
|
@@ -41,6 +57,10 @@ module Bible270
|
|
|
41
57
|
# config.mount_at was worse than useless — ignored when blank, and overriding
|
|
42
58
|
# the true prefix when not, so a config that disagreed with routes.rb would
|
|
43
59
|
# have produced links to nowhere.
|
|
60
|
+
def plan_url
|
|
61
|
+
engine_url(:root_url)
|
|
62
|
+
end
|
|
63
|
+
|
|
44
64
|
def day_url_for(day)
|
|
45
65
|
engine_url(:day_url, day)
|
|
46
66
|
end
|
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
module Bible270
|
|
4
4
|
# A publicly visible reflection attached to a plan day (optionally a track).
|
|
5
5
|
class Comment < ApplicationRecord
|
|
6
|
+
# One heart for both states. An unliked one is the same emoji shown grey by
|
|
7
|
+
# CSS, so it cannot drift in size from a real like, and hovering it previews
|
|
8
|
+
# exactly what clicking will produce.
|
|
9
|
+
#
|
|
10
|
+
# The white heart this replaced was legible on white but nearly invisible on
|
|
11
|
+
# the paper background; an outline glyph like U+2661 is drawn by the font
|
|
12
|
+
# rather than the emoji set and needs hand-tuning to match.
|
|
13
|
+
HEART = '❤️'
|
|
14
|
+
|
|
6
15
|
self.table_name = 'bible270_comments'
|
|
7
16
|
|
|
8
17
|
belongs_to :reader, class_name: 'Bible270::Reader'
|
|
@@ -12,6 +21,7 @@ module Bible270
|
|
|
12
21
|
belongs_to :parent, class_name: 'Bible270::Comment', optional: true
|
|
13
22
|
has_many :replies, class_name: 'Bible270::Comment', foreign_key: :parent_id,
|
|
14
23
|
dependent: :destroy, inverse_of: :parent
|
|
24
|
+
has_many :likes, class_name: 'Bible270::Like', dependent: :destroy, inverse_of: :comment
|
|
15
25
|
|
|
16
26
|
validates :day, inclusion: { in: 1..Plan::DAYS }
|
|
17
27
|
validates :body, presence: true, length: { maximum: 4000 }
|
|
@@ -37,7 +47,9 @@ module Bible270
|
|
|
37
47
|
# Only the reflections themselves: replies are rendered under their parent, so
|
|
38
48
|
# listing them again at the top level would show each twice.
|
|
39
49
|
scope :for_day, ->(d) { approved.where(day: d, parent_id: nil).order(created_at: :asc) }
|
|
40
|
-
scope :threads_for_day, ->(d) {
|
|
50
|
+
scope :threads_for_day, ->(d) {
|
|
51
|
+
for_day(d).includes(:reader, { likes: :reader }, { replies: [:reader, { likes: :reader }] })
|
|
52
|
+
}
|
|
41
53
|
scope :recent, -> { approved.order(created_at: :desc) }
|
|
42
54
|
|
|
43
55
|
# Every reflection is visible when written; these are the moderation actions.
|
|
@@ -46,9 +58,30 @@ module Bible270
|
|
|
46
58
|
def hidden? = !approved
|
|
47
59
|
def reply? = parent_id.present?
|
|
48
60
|
|
|
61
|
+
def liked_by?(reader)
|
|
62
|
+
return false if reader.nil?
|
|
63
|
+
|
|
64
|
+
likes.any? { |like| like.reader_id == reader.id }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Toggling returns the new state, so a caller can say what happened without
|
|
68
|
+
# asking again.
|
|
69
|
+
def toggle_like!(reader)
|
|
70
|
+
existing = likes.find { |like| like.reader_id == reader.id }
|
|
71
|
+
if existing
|
|
72
|
+
existing.destroy
|
|
73
|
+
likes.reload
|
|
74
|
+
false
|
|
75
|
+
else
|
|
76
|
+
likes.create(reader: reader)
|
|
77
|
+
likes.reload
|
|
78
|
+
true
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
49
82
|
# A second's grace: created_at and updated_at differ by microseconds on
|
|
50
83
|
# insert, which would mark every reflection as edited.
|
|
51
|
-
def edited?
|
|
84
|
+
def edited? = updated_at - created_at > 1
|
|
52
85
|
|
|
53
86
|
# Visible replies, oldest first: a conversation reads forwards.
|
|
54
87
|
def visible_replies = replies.approved.order(created_at: :asc)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bible270
|
|
4
|
+
# A reader's heart on a reflection. There is nothing to a like but who gave it
|
|
5
|
+
# and when — the count is the number of rows, and the hover text is the name.
|
|
6
|
+
class Like < ApplicationRecord
|
|
7
|
+
self.table_name = 'bible270_likes'
|
|
8
|
+
|
|
9
|
+
belongs_to :reader, class_name: 'Bible270::Reader'
|
|
10
|
+
belongs_to :comment, class_name: 'Bible270::Comment'
|
|
11
|
+
|
|
12
|
+
validates :reader_id, uniqueness: { scope: :comment_id }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -8,6 +8,7 @@ module Bible270
|
|
|
8
8
|
|
|
9
9
|
has_many :checkoffs, class_name: 'Bible270::Checkoff', dependent: :destroy
|
|
10
10
|
has_many :comments, class_name: 'Bible270::Comment', dependent: :destroy
|
|
11
|
+
has_many :likes, class_name: 'Bible270::Like', dependent: :destroy, inverse_of: :reader
|
|
11
12
|
belongs_to :owner, polymorphic: true, optional: true
|
|
12
13
|
|
|
13
14
|
# Active Storage is optional: an app may have it disabled, and the engine has
|
|
@@ -44,10 +45,37 @@ module Bible270
|
|
|
44
45
|
image = first_present(dig_auth(info, :image), dig_auth(info, :avatar_url))
|
|
45
46
|
reader.email = email if email.present?
|
|
46
47
|
reader.avatar_url = image if image.present?
|
|
48
|
+
assign_names_from(reader, info)
|
|
47
49
|
reader.save
|
|
48
50
|
reader
|
|
49
51
|
end
|
|
50
52
|
|
|
53
|
+
# Providers hand back one display name far more often than two fields, so the
|
|
54
|
+
# name is split when they do not. Without this an OmniAuth reader had no
|
|
55
|
+
# first_name — which made the greeting read "Welcome ." and, more seriously,
|
|
56
|
+
# made them unmentionable: Reader.mentioned_in only considers readers who have
|
|
57
|
+
# one.
|
|
58
|
+
#
|
|
59
|
+
# Only filled in when empty, so a reader who has since corrected their name on
|
|
60
|
+
# their profile does not have it overwritten at every sign-in.
|
|
61
|
+
def self.assign_names_from(reader, info)
|
|
62
|
+
return if reader.first_name.present?
|
|
63
|
+
|
|
64
|
+
given = dig_auth(info, :first_name)
|
|
65
|
+
family = dig_auth(info, :last_name)
|
|
66
|
+
|
|
67
|
+
if given.present?
|
|
68
|
+
reader.first_name = given
|
|
69
|
+
reader.last_name = family
|
|
70
|
+
return
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
split = Names.split_display_name(reader.display_name)
|
|
74
|
+
reader.first_name = split ? split[:first_name] : reader.display_name.to_s.split.first
|
|
75
|
+
reader.last_name = split ? split[:last_name] : nil
|
|
76
|
+
end
|
|
77
|
+
private_class_method :assign_names_from
|
|
78
|
+
|
|
51
79
|
# Find or create the reader behind a verified email address. Uses the same
|
|
52
80
|
# provider/uid identity columns as OmniAuth, with provider "email", so an
|
|
53
81
|
# email reader is indistinguishable from any other downstream.
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<div class="b270-cmeta">
|
|
19
19
|
<strong><%= comment.reader.display_name %></strong>
|
|
20
20
|
· <%= link_to "day #{comment.day}", day_path(comment.day) %>
|
|
21
|
-
· <%= comment.created_at
|
|
21
|
+
· <%= b270_date(comment.created_at) %>
|
|
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>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<% if @enrollment_closed %>
|
|
10
10
|
<% if @enrollment_closed_at %>
|
|
11
11
|
<p class="b270-startline">Enrollment closed
|
|
12
|
-
on <%= @enrollment_closed_at
|
|
12
|
+
on <%= b270_date(@enrollment_closed_at) %><% end %>.
|
|
13
13
|
The <%= @readers.size %> reader<%= 's' unless @readers.size == 1 %> already taking part can
|
|
14
14
|
still sign in and carry on.</p>
|
|
15
15
|
<%= button_to 'Open to new readers', admin_enrollment_path, method: :patch,
|
|
@@ -47,4 +47,31 @@
|
|
|
47
47
|
<p style="color:var(--faint)">No readers yet.</p>
|
|
48
48
|
<% end %>
|
|
49
49
|
|
|
50
|
+
<div class="b270-panel">
|
|
51
|
+
<h2 class="b270-subhead">Write to everyone</h2>
|
|
52
|
+
|
|
53
|
+
<% if @reachable.zero? %>
|
|
54
|
+
<p class="b270-hint" style="margin-top:0">No reader has an email address yet.</p>
|
|
55
|
+
<% else %>
|
|
56
|
+
<p class="b270-hint" style="margin-top:0">
|
|
57
|
+
Goes to <strong><%= @reachable %></strong>
|
|
58
|
+
<%= @reachable == 1 ? 'reader' : 'readers' %> individually, not as one message with
|
|
59
|
+
everyone in bcc — so each is greeted by name.
|
|
60
|
+
<% if @last_broadcast_at.present? %>
|
|
61
|
+
<br>Last sent <%= b270_datetime(@last_broadcast_at) %><%
|
|
62
|
+
%><% if @last_broadcast_subject.present? %>: <em><%= @last_broadcast_subject %></em><% end %>.
|
|
63
|
+
<% end %>
|
|
64
|
+
</p>
|
|
65
|
+
|
|
66
|
+
<%= form_with url: admin_broadcast_path, method: :post, class: "b270-form",
|
|
67
|
+
data: { turbo_confirm: "Send this to #{@reachable} #{@reachable == 1 ? 'reader' : 'readers'}?" } do %>
|
|
68
|
+
<%= text_field_tag :subject, params[:subject], placeholder: "Subject", class: "b270-input" %>
|
|
69
|
+
<%= text_area_tag :body, params[:body], placeholder: "What would you like to say?", rows: 6 %>
|
|
70
|
+
<div class="row">
|
|
71
|
+
<%= submit_tag "Send", class: "b270-btn" %>
|
|
72
|
+
</div>
|
|
73
|
+
<% end %>
|
|
74
|
+
<% end %>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
50
77
|
<p style="margin-top:18px"><%= link_to '← Back to the plan', root_path %></p>
|
|
@@ -92,6 +92,28 @@
|
|
|
92
92
|
<%= render "bible270/shared/reflections", comments: @comments, moderate: true %>
|
|
93
93
|
</div>
|
|
94
94
|
|
|
95
|
+
<div class="b270-panel">
|
|
96
|
+
<h2 class="b270-subhead">Which translation they read</h2>
|
|
97
|
+
<p class="b270-hint" style="margin-top:0">
|
|
98
|
+
Reading links open in this translation.
|
|
99
|
+
<% if @reader.bible_version.blank? %>
|
|
100
|
+
They have expressed no preference, so they follow the site default
|
|
101
|
+
(<%= Bible270.config.bible_version %>).
|
|
102
|
+
<% end %>
|
|
103
|
+
</p>
|
|
104
|
+
|
|
105
|
+
<%= form_with url: admin_reader_version_path(@reader), method: :patch, class: "b270-form" do %>
|
|
106
|
+
<div class="row">
|
|
107
|
+
<%= select_tag :bible_version,
|
|
108
|
+
options_for_select([["Site default (#{Bible270.config.bible_version})", ""]] +
|
|
109
|
+
Bible270::Translations.options,
|
|
110
|
+
@reader.bible_version.to_s),
|
|
111
|
+
class: "b270-select", style: "width:auto" %>
|
|
112
|
+
<%= submit_tag "Save", class: "b270-btn" %>
|
|
113
|
+
</div>
|
|
114
|
+
<% end %>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
95
117
|
<div class="b270-panel">
|
|
96
118
|
<h2 class="b270-subhead">Remove</h2>
|
|
97
119
|
<p class="b270-hint" style="margin-top:0">Deletes the reader together with every check-off and
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
<div style="flex:1">
|
|
12
12
|
<div class="b270-cmeta">
|
|
13
13
|
<strong><%= link_to comment.reader.display_name, reader_path(comment.reader), style: "text-decoration:none" %></strong>
|
|
14
|
-
· <%= comment.created_at
|
|
14
|
+
· <%= b270_date(comment.created_at) %>
|
|
15
15
|
<% if comment.track.present? %>· <%= b270_track(comment.track)[:label] %><% end %>
|
|
16
|
-
<% if comment.edited? %>· <span class="b270-edited" title="<%= comment.updated_at
|
|
16
|
+
<% if comment.edited? %>· <span class="b270-edited" title="<%= b270_datetime(comment.updated_at) %>">edited</span><% end %>
|
|
17
17
|
<% if b270_can_edit?(comment) %>
|
|
18
18
|
<%= link_to "edit", day_path(comment.day, edit: comment.id, anchor: "comment-#{comment.id}"),
|
|
19
19
|
class: "b270-cedit" %>
|
|
@@ -37,13 +37,16 @@
|
|
|
37
37
|
|
|
38
38
|
<%# Only a top-level reflection can be replied to: one level deep, so a reply
|
|
39
39
|
offers no Reply of its own. %>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
<div class="b270-cactions">
|
|
41
|
+
<%= render partial: "bible270/likes/likes", locals: { comment: comment } %>
|
|
42
|
+
<%# A reply may be liked but not replied to: one level deep. %>
|
|
43
|
+
<% if !reply && signed_in? && comment.reader_id != current_reader.id %>
|
|
44
|
+
<%= link_to "Reply", day_path(comment.day, reply_to: comment.id, anchor: "new_comment_form"),
|
|
45
|
+
class: "b270-linkbtn" %>
|
|
46
46
|
<% end %>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<% unless reply %>
|
|
47
50
|
|
|
48
51
|
<div class="b270-replies" id="replies-<%= comment.id %>">
|
|
49
52
|
<%= render partial: "bible270/comments/comment",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<%# locals: (comment:) %>
|
|
2
|
+
<%#
|
|
3
|
+
One heart per like, each carrying its giver's name — so a row of hearts is both
|
|
4
|
+
the count and, on hover, the list of who.
|
|
5
|
+
|
|
6
|
+
Your own like is not drawn in the row: the button is your heart, filled, and
|
|
7
|
+
saying so. Drawing both gave a reader two hearts for one like.
|
|
8
|
+
%>
|
|
9
|
+
<%
|
|
10
|
+
liked = signed_in? && comment.liked_by?(current_reader)
|
|
11
|
+
others = signed_in? ? comment.likes.reject { |like| like.reader_id == current_reader.id } : comment.likes
|
|
12
|
+
%>
|
|
13
|
+
<span class="b270-likes" id="likes-<%= comment.id %>">
|
|
14
|
+
<% others.each do |like| %><span class="b270-heart"
|
|
15
|
+
title="<%= like.reader.display_name %>"><%= Bible270::Comment::HEART %></span><% end %>
|
|
16
|
+
|
|
17
|
+
<% if signed_in? %>
|
|
18
|
+
<%= button_to like_comment_path(comment), method: :post, class: "b270-likebtn#{' liked' if liked}",
|
|
19
|
+
form: { style: "display:inline", data: { turbo_stream: true } },
|
|
20
|
+
aria: { label: liked ? "Take back your like" : "Like this reflection" },
|
|
21
|
+
title: liked ? "You liked this — tap to take it back" : "Like this reflection" do %>
|
|
22
|
+
<%= Bible270::Comment::HEART %>
|
|
23
|
+
<% end %>
|
|
24
|
+
<% elsif comment.likes.any? %>
|
|
25
|
+
<span class="b270-likecount"><%= comment.likes.size %></span>
|
|
26
|
+
<% end %>
|
|
27
|
+
</span>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<body style="margin:0;padding:24px;background:#ece6da;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#231f18">
|
|
4
|
+
<div style="max-width:520px;margin:0 auto;background:#f4efe6;border:1px solid #d7cfbf;border-radius:14px;padding:28px">
|
|
5
|
+
<p style="margin:0 0 16px;font-size:15px">
|
|
6
|
+
<%= @reader.first_name.presence || @reader.display_name %>,
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<div style="font-size:15px;line-height:1.6"><%= simple_format(@body) %></div>
|
|
10
|
+
|
|
11
|
+
<p style="margin:24px 0 0;padding-top:16px;border-top:1px solid #d7cfbf;font-size:12px;color:#9a9082">
|
|
12
|
+
<%= @app_name %><% if @plan_url %> ·
|
|
13
|
+
<a href="<%= @plan_url %>" style="color:#a5812c"><%= @plan_url %></a><% end %>
|
|
14
|
+
</p>
|
|
15
|
+
</div>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<tr><td style="padding:3px 12px 3px 0;color:#6d6558">Name</td><td><%= @reader.display_name %></td></tr>
|
|
12
12
|
<tr><td style="padding:3px 12px 3px 0;color:#6d6558">Email</td><td><%= @reader.email.presence || '(none given)' %></td></tr>
|
|
13
13
|
<tr><td style="padding:3px 12px 3px 0;color:#6d6558">Signed in with</td><td><%= @reader.provider.presence || 'the host application' %></td></tr>
|
|
14
|
-
<tr><td style="padding:3px 12px 3px 0;color:#6d6558">Joined</td><td><%= @reader.created_at.strftime('%B %-d, %Y at %-I:%M %p') %></td></tr>
|
|
14
|
+
<tr><td style="padding:3px 12px 3px 0;color:#6d6558">Joined</td><td><%= Bible270.local_time(@reader.created_at).strftime('%B %-d, %Y at %-I:%M %p') %></td></tr>
|
|
15
15
|
</table>
|
|
16
16
|
|
|
17
17
|
<p style="margin:18px 0 0;font-size:14px;color:#6d6558">
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Name: <%= @reader.display_name %>
|
|
4
4
|
Email: <%= @reader.email.presence || '(none given)' %>
|
|
5
5
|
Signed in with: <%= @reader.provider.presence || 'the host application' %>
|
|
6
|
-
Joined: <%= @reader.created_at.strftime('%B %-d, %Y at %-I:%M %p') %>
|
|
6
|
+
Joined: <%= Bible270.local_time(@reader.created_at).strftime('%B %-d, %Y at %-I:%M %p') %>
|
|
7
7
|
|
|
8
8
|
That makes <%= @total %> reader<%= 's' unless @total == 1 %> taking part.
|
|
9
9
|
<% if @admin_url %>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<%= b270_avatar(r, size: 40) %>
|
|
12
12
|
<div>
|
|
13
13
|
<div class="name"><%= link_to r.display_name, reader_path(r), style: "text-decoration:none" %></div>
|
|
14
|
-
<div class="sub"><%= @days_completed[r.id] %> of <%= Bible270::Plan::DAYS %> days · joined <%= r.created_at.strftime("%b %Y") %></div>
|
|
14
|
+
<div class="sub"><%= @days_completed[r.id] %> of <%= Bible270::Plan::DAYS %> days · joined <%= Bible270.local_time(r.created_at).strftime("%b %Y") %></div>
|
|
15
15
|
</div>
|
|
16
16
|
<div class="stat">
|
|
17
17
|
<span class="big"><%= (@days_completed[r.id].to_f / Bible270::Plan::DAYS * 100).round %>%</span>
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
<!--<%= link_to "Plan", root_path %>-->
|
|
13
13
|
<%= link_to "People", community_path %>
|
|
14
14
|
<%# Public, like the community page: anyone may read reflections. %>
|
|
15
|
-
<%= link_to "
|
|
15
|
+
<%= link_to "Posts", reflections_path %>
|
|
16
16
|
<% if signed_in? %>
|
|
17
|
-
<% if Bible270.config.admin?(current_reader) %><%= link_to "Admin", admin_path %><% end %>
|
|
18
17
|
<%= link_to "Progress", progress_path %>
|
|
19
18
|
<%= link_to "Profile", profile_path %>
|
|
19
|
+
<% if Bible270.config.admin?(current_reader) %><%= link_to "Admin", admin_path %><% end %>
|
|
20
20
|
<%= button_to "Sign out", sign_out_path, method: :delete, class: "b270-linkbtn",
|
|
21
21
|
form: { style: "display:inline" } %>
|
|
22
22
|
<% elsif Bible270.config.any_sign_in_method? %>
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<div style="flex:1">
|
|
20
20
|
<div class="b270-cmeta">
|
|
21
21
|
<%= link_to "Day #{comment.day}", day_path(comment.day) %>
|
|
22
|
-
· <%= comment.created_at
|
|
22
|
+
· <%= b270_date(comment.created_at) %>
|
|
23
23
|
<% if moderate && comment.hidden? %><span class="b270-badge">Hidden</span><% end %>
|
|
24
24
|
</div>
|
|
25
25
|
|
|
@@ -161,6 +161,17 @@
|
|
|
161
161
|
.b270-replying strong{color:var(--ink)}
|
|
162
162
|
.b270-mention{color:var(--teal);font-weight:600;text-decoration:none}
|
|
163
163
|
.b270-mention:hover{text-decoration:underline}
|
|
164
|
+
.b270-cactions{display:flex;align-items:center;gap:12px;margin-top:6px;flex-wrap:wrap}
|
|
165
|
+
.b270-likes{display:inline-flex;align-items:center;gap:2px}
|
|
166
|
+
.b270-heart{font-size:13px;line-height:1;cursor:default}
|
|
167
|
+
/* Same size as a heart in the row: the button is one of the hearts, not a
|
|
168
|
+
control sitting beside them. Unliked, it is the same emoji desaturated —
|
|
169
|
+
visible against the paper, and hovering shows what clicking will give. */
|
|
170
|
+
.b270-likebtn{background:none;border:0;padding:0 2px;font-size:13px;line-height:1;cursor:pointer;
|
|
171
|
+
filter:grayscale(1);opacity:.5;transition:filter .12s,opacity .12s}
|
|
172
|
+
.b270-likebtn:hover{filter:none;opacity:1}
|
|
173
|
+
.b270-likebtn.liked{filter:none;opacity:1}
|
|
174
|
+
.b270-likecount{font-size:11.5px;color:var(--faint);margin-left:2px}
|
|
164
175
|
.b270-creply{margin-top:6px}
|
|
165
176
|
.b270-cbody{margin:3px 0 0;white-space:pre-wrap}
|
|
166
177
|
.b270-editform{margin-top:6px}
|
|
@@ -172,6 +183,7 @@
|
|
|
172
183
|
.b270-comment.b270-editing{background:rgba(165,129,44,.05);border-radius:10px;padding:12px}
|
|
173
184
|
.b270-editing textarea{min-height:64px}
|
|
174
185
|
.b270-cdel{font-size:12px;color:var(--oxblood);background:none;border:none;cursor:pointer;padding:0;margin-left:8px}
|
|
186
|
+
.b270-input{width:100%;padding:9px 11px;border:1px solid var(--line);border-radius:9px;background:#fff;font:inherit;font-size:14px;margin-bottom:8px}
|
|
175
187
|
.b270-form textarea{width:100%;font:inherit;font-size:15px;border:1px solid var(--line);border-radius:10px;padding:11px 13px;background:#fff;resize:vertical;min-height:76px}
|
|
176
188
|
.b270-form .row{display:flex;align-items:center;gap:10px;margin-top:10px}
|
|
177
189
|
.b270 .b270-btn{border:none;border-radius:10px;padding:8px 16px;margin-top:4px;font:inherit;font-weight:600;font-size:14px;cursor:pointer;background:var(--ink);color:var(--paper);text-decoration:none;display:inline-block}
|
data/config/routes.rb
CHANGED
|
@@ -12,6 +12,7 @@ Bible270::Engine.routes.draw do
|
|
|
12
12
|
constraints: { day: %r{\d+} }
|
|
13
13
|
post 'day/:day/comments', to: 'comments#create', as: :day_comments,
|
|
14
14
|
constraints: { day: %r{\d+} }
|
|
15
|
+
post 'comments/:id/like', to: 'likes#toggle', as: :like_comment
|
|
15
16
|
patch 'comments/:id', to: 'comments#update'
|
|
16
17
|
delete 'comments/:id', to: 'comments#destroy', as: :comment
|
|
17
18
|
|
|
@@ -21,10 +22,12 @@ Bible270::Engine.routes.draw do
|
|
|
21
22
|
delete 'admin/readers/:id', to: 'admin#destroy'
|
|
22
23
|
patch 'admin/readers/:id/profile', to: 'admin#update_profile', as: :admin_reader_profile
|
|
23
24
|
delete 'admin/readers/:id/avatar', to: 'admin#remove_avatar', as: :admin_reader_avatar
|
|
25
|
+
patch 'admin/readers/:id/version', to: 'admin#update_bible_version', as: :admin_reader_version
|
|
24
26
|
patch 'admin/readers/:id/start', to: 'admin#update_start', as: :admin_reader_start
|
|
25
27
|
patch 'admin/readers/:id/through', to: 'admin#complete_through', as: :admin_reader_through
|
|
26
28
|
post 'admin/readers/:id/days/:day', to: 'admin#toggle_day', as: :admin_reader_day
|
|
27
29
|
|
|
30
|
+
post 'admin/broadcast', to: 'admin#broadcast', as: :admin_broadcast
|
|
28
31
|
patch 'admin/enrollment', to: 'admin#update_enrollment', as: :admin_enrollment
|
|
29
32
|
get 'admin/comments', to: 'admin#comments', as: :admin_comments
|
|
30
33
|
patch 'admin/comments/:id/hide', to: 'admin#hide_comment', as: :admin_hide_comment
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateBible270Likes < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :bible270_likes do |t|
|
|
6
|
+
t.references :reader, null: false, index: false,
|
|
7
|
+
foreign_key: { to_table: :bible270_readers }
|
|
8
|
+
t.references :comment, null: false, index: false,
|
|
9
|
+
foreign_key: { to_table: :bible270_comments }
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# One like per reader per reflection, enforced by the database as well as the
|
|
14
|
+
# model: a double-tap should not become two hearts.
|
|
15
|
+
add_index :bible270_likes, %i[reader_id comment_id], unique: true,
|
|
16
|
+
name: 'idx_b270_likes_unique'
|
|
17
|
+
# Reading a thread asks for a comment's likes, so that is the index that earns
|
|
18
|
+
# its keep.
|
|
19
|
+
add_index :bible270_likes, :comment_id, name: 'idx_b270_likes_comment'
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/bible270/version.rb
CHANGED
data/lib/bible270.rb
CHANGED
|
@@ -22,10 +22,24 @@ module_function
|
|
|
22
22
|
# it; Date.today follows the machine. A self-hosted reading plan wants the
|
|
23
23
|
# latter, so that is the default, with config.time_zone to override.
|
|
24
24
|
def today
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
local_time(Time.now).to_date
|
|
26
|
+
end
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
# A stored timestamp in the plan's own zone.
|
|
29
|
+
#
|
|
30
|
+
# created_at and friends come back in Rails' Time.zone, which is UTC unless the
|
|
31
|
+
# host app sets it — so a reflection written at 5pm in Los Angeles was being
|
|
32
|
+
# shown as the next day. This is the same rule Bible270.today follows: the
|
|
33
|
+
# machine's zone, or config.time_zone when one is given.
|
|
34
|
+
def local_time(time)
|
|
35
|
+
return nil if time.nil?
|
|
36
|
+
|
|
37
|
+
zone = config.time_zone
|
|
38
|
+
if zone.to_s.empty?
|
|
39
|
+
time.getlocal
|
|
40
|
+
else
|
|
41
|
+
found = Time.respond_to?(:find_zone) ? Time.find_zone(zone) : nil
|
|
42
|
+
found ? time.in_time_zone(found) : time.getlocal
|
|
43
|
+
end
|
|
30
44
|
end
|
|
31
45
|
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: 0.17.
|
|
4
|
+
version: 0.17.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew vonderLuft
|
|
@@ -85,16 +85,20 @@ files:
|
|
|
85
85
|
- app/controllers/bible270/checkoffs_controller.rb
|
|
86
86
|
- app/controllers/bible270/comments_controller.rb
|
|
87
87
|
- app/controllers/bible270/days_controller.rb
|
|
88
|
+
- app/controllers/bible270/likes_controller.rb
|
|
88
89
|
- app/controllers/bible270/profiles_controller.rb
|
|
89
90
|
- app/controllers/bible270/readers_controller.rb
|
|
90
91
|
- app/controllers/bible270/sessions_controller.rb
|
|
91
92
|
- app/helpers/bible270/plan_helper.rb
|
|
92
93
|
- app/mailers/bible270/application_mailer.rb
|
|
94
|
+
- app/mailers/bible270/broadcast.html.erb
|
|
95
|
+
- app/mailers/bible270/broadcast.text.erb
|
|
93
96
|
- app/mailers/bible270/notice_mailer.rb
|
|
94
97
|
- app/mailers/bible270/sign_in_mailer.rb
|
|
95
98
|
- app/models/bible270/application_record.rb
|
|
96
99
|
- app/models/bible270/checkoff.rb
|
|
97
100
|
- app/models/bible270/comment.rb
|
|
101
|
+
- app/models/bible270/like.rb
|
|
98
102
|
- app/models/bible270/reader.rb
|
|
99
103
|
- app/models/bible270/setting.rb
|
|
100
104
|
- app/models/bible270/sign_in_token.rb
|
|
@@ -115,6 +119,10 @@ files:
|
|
|
115
119
|
- app/views/bible270/days/_start_date.html.erb
|
|
116
120
|
- app/views/bible270/days/index.html.erb
|
|
117
121
|
- app/views/bible270/days/show.html.erb
|
|
122
|
+
- app/views/bible270/likes/_likes.html.erb
|
|
123
|
+
- app/views/bible270/likes/toggle.turbo_stream.erb
|
|
124
|
+
- app/views/bible270/notice_mailer/broadcast.html.erb
|
|
125
|
+
- app/views/bible270/notice_mailer/broadcast.text.erb
|
|
118
126
|
- app/views/bible270/notice_mailer/mentioned.html.erb
|
|
119
127
|
- app/views/bible270/notice_mailer/mentioned.text.erb
|
|
120
128
|
- app/views/bible270/notice_mailer/new_reader.html.erb
|
|
@@ -148,6 +156,7 @@ files:
|
|
|
148
156
|
- db/migrate/20260101000010_add_remember_token_to_bible270_readers.rb
|
|
149
157
|
- db/migrate/20260101000011_add_mention_notices_to_bible270_readers.rb
|
|
150
158
|
- db/migrate/20260101000012_add_parent_to_bible270_comments.rb
|
|
159
|
+
- db/migrate/20260101000013_create_bible270_likes.rb
|
|
151
160
|
- lib/bible270.rb
|
|
152
161
|
- lib/bible270/avatars.rb
|
|
153
162
|
- lib/bible270/configuration.rb
|