about summary refs log tree commit diff
path: root/app/models
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2020-01-23 22:00:13 +0100
committerGitHub <noreply@github.com>2020-01-23 22:00:13 +0100
commitf52c988e12e464e7baefc2fdb48ddf4a95584664 (patch)
tree1849998c1180280751f829769c10c84986bf0953 /app/models
parent81cc86bb1ffb662843938379eeb522e3a6f11b79 (diff)
Add announcements (#12662)
* Add announcements

Fix #11006

* Add reactions to announcements

* Add admin UI for announcements

* Add unit tests

* Fix issues

- Add `with_dismissed` param to announcements API
- Fix end date not being formatted when time range is given
- Fix announcement delete causing reactions to send streaming updates
- Fix announcements container growing too wide and mascot too small
- Fix `all_day` being settable when no time range is given
- Change text "Update" to "Announcement"

* Fix scheduler unpublishing announcements before they are due

* Fix filter params not being passed to announcements filter
Diffstat (limited to 'app/models')
-rw-r--r--app/models/account.rb6
-rw-r--r--app/models/announcement.rb85
-rw-r--r--app/models/announcement_filter.rb39
-rw-r--r--app/models/announcement_mute.rb19
-rw-r--r--app/models/announcement_reaction.rb37
-rw-r--r--app/models/backup.rb2
-rw-r--r--app/models/bookmark.rb6
-rw-r--r--app/models/concerns/account_interactions.rb1
-rw-r--r--app/models/custom_emoji.rb2
9 files changed, 192 insertions, 5 deletions
diff --git a/app/models/account.rb b/app/models/account.rb
index 1e8abe6ec..da6f51a9c 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -476,6 +476,12 @@ class Account < ApplicationRecord
       records
     end
 
+    def from_text(text)
+      return [] if text.blank?
+
+      text.scan(MENTION_RE).map { |match| match.first.split('@', 2) }.uniq.map { |(username, domain)| EntityCache.instance.mention(username, domain) }
+    end
+
     private
 
     def generate_query_for_search(terms)
diff --git a/app/models/announcement.rb b/app/models/announcement.rb
new file mode 100644
index 000000000..4da9f94d6
--- /dev/null
+++ b/app/models/announcement.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: announcements
+#
+#  id           :bigint(8)        not null, primary key
+#  text         :text             default(""), not null
+#  published    :boolean          default(FALSE), not null
+#  all_day      :boolean          default(FALSE), not null
+#  scheduled_at :datetime
+#  starts_at    :datetime
+#  ends_at      :datetime
+#  created_at   :datetime         not null
+#  updated_at   :datetime         not null
+#
+
+class Announcement < ApplicationRecord
+  after_commit :queue_publish, on: :create
+
+  scope :unpublished, -> { where(published: false) }
+  scope :published, -> { where(published: true) }
+  scope :without_muted, ->(account) { joins("LEFT OUTER JOIN announcement_mutes ON announcement_mutes.announcement_id = announcements.id AND announcement_mutes.account_id = #{account.id}").where('announcement_mutes.id IS NULL') }
+  scope :chronological, -> { order(Arel.sql('COALESCE(announcements.starts_at, announcements.scheduled_at, announcements.created_at) ASC')) }
+
+  has_many :announcement_mutes, dependent: :destroy
+  has_many :announcement_reactions, dependent: :destroy
+
+  validates :text, presence: true
+  validates :starts_at, presence: true, if: -> { ends_at.present? }
+  validates :ends_at, presence: true, if: -> { starts_at.present? }
+
+  before_validation :set_all_day
+  before_validation :set_starts_at, on: :create
+  before_validation :set_ends_at, on: :create
+
+  def time_range?
+    starts_at.present? && ends_at.present?
+  end
+
+  def mentions
+    @mentions ||= Account.from_text(text)
+  end
+
+  def tags
+    @tags ||= Tag.find_or_create_by_names(Extractor.extract_hashtags(text))
+  end
+
+  def emojis
+    @emojis ||= CustomEmoji.from_text(text)
+  end
+
+  def reactions(account = nil)
+    records = begin
+      scope = announcement_reactions.group(:announcement_id, :name, :custom_emoji_id).order(Arel.sql('MIN(created_at) ASC'))
+
+      if account.nil?
+        scope.select('name, custom_emoji_id, count(*) as count, false as me')
+      else
+        scope.select("name, custom_emoji_id, count(*) as count, exists(select 1 from announcement_reactions r where r.account_id = #{account.id} and r.announcement_id = announcement_reactions.announcement_id and r.name = announcement_reactions.name) as me")
+      end
+    end
+
+    ActiveRecord::Associations::Preloader.new.preload(records, :custom_emoji)
+    records
+  end
+
+  private
+
+  def set_all_day
+    self.all_day = false if starts_at.blank? || ends_at.blank?
+  end
+
+  def set_starts_at
+    self.starts_at = starts_at.change(hour: 0, min: 0, sec: 0) if all_day? && starts_at.present?
+  end
+
+  def set_ends_at
+    self.ends_at = ends_at.change(hour: 23, min: 59, sec: 59) if all_day? && ends_at.present?
+  end
+
+  def queue_publish
+    PublishScheduledAnnouncementWorker.perform_async(id) if scheduled_at.blank?
+  end
+end
diff --git a/app/models/announcement_filter.rb b/app/models/announcement_filter.rb
new file mode 100644
index 000000000..950852460
--- /dev/null
+++ b/app/models/announcement_filter.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+class AnnouncementFilter
+  KEYS = %i(
+    published
+    unpublished
+  ).freeze
+
+  attr_reader :params
+
+  def initialize(params)
+    @params = params
+  end
+
+  def results
+    scope = Announcement.unscoped
+
+    params.each do |key, value|
+      next if key.to_s == 'page'
+
+      scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
+    end
+
+    scope.chronological
+  end
+
+  private
+
+  def scope_for(key, _value)
+    case key.to_s
+    when 'published'
+      Announcement.published
+    when 'unpublished'
+      Announcement.unpublished
+    else
+      raise "Unknown filter: #{key}"
+    end
+  end
+end
diff --git a/app/models/announcement_mute.rb b/app/models/announcement_mute.rb
new file mode 100644
index 000000000..46fda2f5d
--- /dev/null
+++ b/app/models/announcement_mute.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: announcement_mutes
+#
+#  id              :bigint(8)        not null, primary key
+#  account_id      :bigint(8)
+#  announcement_id :bigint(8)
+#  created_at      :datetime         not null
+#  updated_at      :datetime         not null
+#
+
+class AnnouncementMute < ApplicationRecord
+  belongs_to :account
+  belongs_to :announcement, inverse_of: :announcement_mutes
+
+  validates :account_id, uniqueness: { scope: :announcement_id }
+end
diff --git a/app/models/announcement_reaction.rb b/app/models/announcement_reaction.rb
new file mode 100644
index 000000000..d22771034
--- /dev/null
+++ b/app/models/announcement_reaction.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: announcement_reactions
+#
+#  id              :bigint(8)        not null, primary key
+#  account_id      :bigint(8)
+#  announcement_id :bigint(8)
+#  name            :string           default(""), not null
+#  custom_emoji_id :bigint(8)
+#  created_at      :datetime         not null
+#  updated_at      :datetime         not null
+#
+
+class AnnouncementReaction < ApplicationRecord
+  after_commit :queue_publish
+
+  belongs_to :account
+  belongs_to :announcement, inverse_of: :announcement_reactions
+  belongs_to :custom_emoji, optional: true
+
+  validates :name, presence: true
+  validates_with ReactionValidator
+
+  before_validation :set_custom_emoji
+
+  private
+
+  def set_custom_emoji
+    self.custom_emoji = CustomEmoji.local.find_by(disabled: false, shortcode: name) if name.present?
+  end
+
+  def queue_publish
+    PublishAnnouncementReactionWorker.perform_async(announcement_id, name) unless announcement.destroyed?
+  end
+end
diff --git a/app/models/backup.rb b/app/models/backup.rb
index 8eeb1748a..d242fd62c 100644
--- a/app/models/backup.rb
+++ b/app/models/backup.rb
@@ -7,11 +7,11 @@
 #  user_id           :bigint(8)
 #  dump_file_name    :string
 #  dump_content_type :string
-#  dump_file_size    :bigint
 #  dump_updated_at   :datetime
 #  processed         :boolean          default(FALSE), not null
 #  created_at        :datetime         not null
 #  updated_at        :datetime         not null
+#  dump_file_size    :bigint(8)
 #
 
 class Backup < ApplicationRecord
diff --git a/app/models/bookmark.rb b/app/models/bookmark.rb
index 01dc48ee7..916261a17 100644
--- a/app/models/bookmark.rb
+++ b/app/models/bookmark.rb
@@ -3,11 +3,11 @@
 #
 # Table name: bookmarks
 #
-#  id         :integer          not null, primary key
+#  id         :bigint(8)        not null, primary key
+#  account_id :bigint(8)        not null
+#  status_id  :bigint(8)        not null
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
-#  account_id :integer          not null
-#  status_id  :integer          not null
 #
 
 class Bookmark < ApplicationRecord
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index f27d39483..14bcf7bb1 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -84,6 +84,7 @@ module AccountInteractions
     has_many :muted_by, -> { order('mutes.id desc') }, through: :muted_by_relationships, source: :account
     has_many :conversation_mutes, dependent: :destroy
     has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy
+    has_many :announcement_mutes, dependent: :destroy
   end
 
   def follow!(other_account, reblogs: nil, uri: nil)
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index 0dacaf654..d177cf281 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -67,7 +67,7 @@ class CustomEmoji < ApplicationRecord
   end
 
   class << self
-    def from_text(text, domain)
+    def from_text(text, domain = nil)
       return [] if text.blank?
 
       shortcodes = text.scan(SCAN_RE).map(&:first).uniq