From dac2611c57267125949a2ac0cf8e0b660d4bd5a6 Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Wed, 12 Feb 2025 10:41:11 +0000 Subject: [PATCH] Auto-pop bubbles --- app/controllers/bubbles/pops_controller.rb | 2 +- .../controllers/local_time_controller.js | 28 ++++++++++++++++++- app/models/bubble/poppable.rb | 26 ++++++++++++++++- app/models/event.rb | 2 ++ app/views/bubbles/_pop_toggle.html.erb | 7 ++--- ...250212103251_add_auto_pop_at_to_bubbles.rb | 13 +++++++++ db/schema.rb | 4 ++- test/fixtures/bubbles.yml | 4 +++ test/models/bubble/poppable_test.rb | 5 ++-- 9 files changed, 79 insertions(+), 12 deletions(-) diff --git a/app/controllers/bubbles/pops_controller.rb b/app/controllers/bubbles/pops_controller.rb index c19784433..7abedb285 100644 --- a/app/controllers/bubbles/pops_controller.rb +++ b/app/controllers/bubbles/pops_controller.rb @@ -2,7 +2,7 @@ class Bubbles::PopsController < ApplicationController include BubbleScoped, BucketScoped def create - @bubble.pop! + @bubble.pop!(user: Current.user) redirect_to @bubble end diff --git a/app/javascript/controllers/local_time_controller.js b/app/javascript/controllers/local_time_controller.js index 8151d6e40..bd02d8598 100644 --- a/app/javascript/controllers/local_time_controller.js +++ b/app/javascript/controllers/local_time_controller.js @@ -1,7 +1,7 @@ import { Controller } from "@hotwired/stimulus" export default class extends Controller { - static targets = ["time", "date", "datetime", "shortdate", "ago"] + static targets = [ "time", "date", "datetime", "shortdate", "ago", "indays" ] #timer @@ -11,6 +11,7 @@ export default class extends Controller { this.shortDateFormatter = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric" }) this.dateTimeFormatter = new Intl.DateTimeFormat(undefined, { timeStyle: "short", dateStyle: "short" }) this.agoFormatter = new AgoFormatter() + this.indaysFormatter = new InDaysFormatter() } connect() { @@ -41,6 +42,10 @@ export default class extends Controller { this.#formatTime(this.agoFormatter, target) } + indaysTargetConnected(target) { + this.#formatTime(this.indaysFormatter, target) + } + #refreshRelativeTimes() { this.agoTargets.forEach(target => { this.#formatTime(this.agoFormatter, target) @@ -81,3 +86,24 @@ class AgoFormatter { return `${quantity} ${word}${suffix} ago` } } + +class InDaysFormatter { + format(dt) { + const target = this.#beginningOfDay(dt) + const today = this.#beginningOfDay(new Date()) + const days = Math.round((target - today) / (1000 * 60 * 60 * 24)) + + if (days <= 0) { + return "today" + } + if (days === 1) { + return "tomorrow" + } + + return `in ${Math.round(days)} days` + } + + #beginningOfDay(dt) { + return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()) + } +} diff --git a/app/models/bubble/poppable.rb b/app/models/bubble/poppable.rb index c60474d89..afd1803fa 100644 --- a/app/models/bubble/poppable.rb +++ b/app/models/bubble/poppable.rb @@ -1,21 +1,45 @@ module Bubble::Poppable extend ActiveSupport::Concern + AUTO_POP_AFTER = 30.days + included do has_one :pop, dependent: :destroy scope :popped, -> { joins(:pop) } scope :active, -> { where.missing(:pop) } + + after_create -> { update_auto_pop_at(created_at) } + end + + class_methods do + def auto_pop_all_due + active.where(auto_pop_at: ..Time.current).find_each do |bubble| + bubble.pop!(user: bubble.bucket.account.users.system) + end + end + end + + def update_auto_pop_at(timestamp) + update!(auto_pop_at: timestamp + AUTO_POP_AFTER) end def popped? pop.present? end + def popped_by + pop&.user + end + + def popped_at + pop&.created_at + end + def pop!(user: Current.user) unless popped? create_pop!(user: user) - track_event :popped + track_event :popped, creator: user end end diff --git a/app/models/event.rb b/app/models/event.rb index 6b29885bc..b8a5f5bbd 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -13,6 +13,8 @@ class Event < ApplicationRecord scope :non_boosts, -> { where.not action: :boosted } scope :boosts, -> { where action: :boosted } + after_create -> { bubble.update_auto_pop_at(created_at) } + def generate_notifications Notifier.for(self)&.generate end diff --git a/app/views/bubbles/_pop_toggle.html.erb b/app/views/bubbles/_pop_toggle.html.erb index ff430eae8..02b92c49f 100644 --- a/app/views/bubbles/_pop_toggle.html.erb +++ b/app/views/bubbles/_pop_toggle.html.erb @@ -1,19 +1,16 @@
<% if bubble.popped? %> - <% event = bubble.events.where(action: "popped").last %> - Popped by <%= "#{ event.creator.name } on #{ event.created_at.to_date.strftime('%B %-d') }" if event %>. + Popped by <%= bubble.popped_by.name %> on <%= local_datetime_tag(bubble.popped_at, style: :shortdate) %>. <%= button_to bucket_bubble_pop_path(bubble.bucket, bubble), method: :delete, class: "btn btn--plain borderless fill-transparent" do %> Un-pop <% end %> <% else %> - <% event = bubble.events.last %> <%= button_to bucket_bubble_pop_path(bubble.bucket, bubble), class: "btn borderless" do %> <%= image_tag "pop.svg", aria: { hidden: true }, size: 24, class: "colorize--white" %> Pop <% end %> - <% time_until_pop = (event&.created_at || bubble.created_at) + 30.days %> - Auto-pops <%= Time.current >= time_until_pop ? "today" : "in #{distance_of_time_in_words(Time.current, time_until_pop)}" %>. + Auto-pops <%= local_datetime_tag(bubble.auto_pop_at, style: :indays) %>. <% end %>
diff --git a/db/migrate/20250212103251_add_auto_pop_at_to_bubbles.rb b/db/migrate/20250212103251_add_auto_pop_at_to_bubbles.rb index d2185265f..39f59c3db 100644 --- a/db/migrate/20250212103251_add_auto_pop_at_to_bubbles.rb +++ b/db/migrate/20250212103251_add_auto_pop_at_to_bubbles.rb @@ -3,5 +3,18 @@ class AddAutoPopAtToBubbles < ActiveRecord::Migration[8.1] change_table :bubbles do |t| t.datetime :auto_pop_at, index: true end + + execute " + update bubbles + set auto_pop_at = datetime(activity.last_active_at, '+30 days') + from ( + select bubbles.id as bubble_id, coalesce(max(events.created_at), bubbles.created_at) as last_active_at + from bubbles + left join events on bubbles.id = events.bubble_id group by bubbles.id + ) as activity + where bubbles.id = activity.bubble_id + " + + change_column_null :bubbles, :auto_pop_at, false end end diff --git a/db/schema.rb b/db/schema.rb index c8a0a15d9..16dd73512 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2025_02_12_094743) do +ActiveRecord::Schema[8.1].define(version: 2025_02_12_103251) do create_table "accesses", force: :cascade do |t| t.integer "bucket_id", null: false t.integer "user_id", null: false @@ -107,6 +107,8 @@ ActiveRecord::Schema[8.1].define(version: 2025_02_12_094743) do t.integer "comments_count", default: 0, null: false t.integer "activity_score", default: 0, null: false t.text "status", default: "creating", null: false + t.datetime "auto_pop_at", null: false + t.index ["auto_pop_at"], name: "index_bubbles_on_auto_pop_at" t.index ["bucket_id"], name: "index_bubbles_on_bucket_id" t.index ["stage_id"], name: "index_bubbles_on_stage_id" end diff --git a/test/fixtures/bubbles.yml b/test/fixtures/bubbles.yml index 8bbde128f..e135db675 100644 --- a/test/fixtures/bubbles.yml +++ b/test/fixtures/bubbles.yml @@ -9,6 +9,7 @@ logo: comments_count: 2 activity_score: 7 status: published + auto_pop_at: <%= 1.week.from_now %> layout: bucket: writebook @@ -19,6 +20,7 @@ layout: comments_count: 1 activity_score: 1 status: published + auto_pop_at: <%= 1.week.from_now %> text: bucket: writebook @@ -27,6 +29,7 @@ text: color: "#3B4B59" created_at: <%= 1.week.ago %> status: published + auto_pop_at: <%= 1.week.from_now %> shipping: bucket: writebook @@ -35,3 +38,4 @@ shipping: color: "#ED8008" created_at: <%= 1.week.ago %> status: published + auto_pop_at: <%= 1.week.from_now %> diff --git a/test/models/bubble/poppable_test.rb b/test/models/bubble/poppable_test.rb index cff8d1a38..f205ba790 100644 --- a/test/models/bubble/poppable_test.rb +++ b/test/models/bubble/poppable_test.rb @@ -9,10 +9,9 @@ class Bubble::PoppableTest < ActiveSupport::TestCase test "popping" do assert_not bubbles(:logo).popped? - with_current_user(:kevin) do - bubbles(:logo).pop! - end + bubbles(:logo).pop!(user: users(:kevin)) assert bubbles(:logo).popped? + assert_equal users(:kevin), bubbles(:logo).popped_by end end