diff --git a/app/assets/javascripts/discourse/app/components/about-page.gjs b/app/assets/javascripts/discourse/app/components/about-page.gjs index a6643f18535..ecf252ae5a1 100644 --- a/app/assets/javascripts/discourse/app/components/about-page.gjs +++ b/app/assets/javascripts/discourse/app/components/about-page.gjs @@ -2,6 +2,7 @@ import Component from "@glimmer/component"; import { hash } from "@ember/helper"; import { htmlSafe } from "@ember/template"; import PluginOutlet from "discourse/components/plugin-outlet"; +import { number } from "discourse/lib/formatter"; import dIcon from "discourse-common/helpers/d-icon"; import i18n from "discourse-common/helpers/i18n"; import escape from "discourse-common/lib/escape"; @@ -46,6 +47,61 @@ export default class AboutPage extends Component { }), }), }, + { + class: "site-creation-date", + icon: "calendar-alt", + text: this.siteAgeString, + }, + ]; + } + + get siteActivities() { + return [ + { + icon: "scroll", + class: "topics", + activityText: I18n.t("about.activities.topics", { + count: this.args.model.stats.topics_7_days, + formatted_number: number(this.args.model.stats.topics_7_days), + }), + period: I18n.t("about.activities.periods.last_7_days"), + }, + { + icon: "pencil-alt", + class: "posts", + activityText: I18n.t("about.activities.posts", { + count: this.args.model.stats.posts_last_day, + formatted_number: number(this.args.model.stats.posts_last_day), + }), + period: I18n.t("about.activities.periods.today"), + }, + { + icon: "user-friends", + class: "active-users", + activityText: I18n.t("about.activities.active_users", { + count: this.args.model.stats.active_users_7_days, + formatted_number: number(this.args.model.stats.active_users_7_days), + }), + period: I18n.t("about.activities.periods.last_7_days"), + }, + { + icon: "user-plus", + class: "sign-ups", + activityText: I18n.t("about.activities.sign_ups", { + count: this.args.model.stats.users_7_days, + formatted_number: number(this.args.model.stats.users_7_days), + }), + period: I18n.t("about.activities.periods.last_7_days"), + }, + { + icon: "heart", + class: "likes", + activityText: I18n.t("about.activities.likes", { + count: this.args.model.stats.likes_count, + formatted_number: number(this.args.model.stats.likes_count), + }), + period: I18n.t("about.activities.periods.all_time"), + }, ]; } @@ -66,6 +122,22 @@ export default class AboutPage extends Component { } } + get siteAgeString() { + const creationDate = new Date(this.args.model.site_creation_date); + + let diff = new Date() - creationDate; + diff /= 1000 * 3600 * 24 * 30; + + if (diff < 1) { + return I18n.t("about.site_age.less_than_one_month"); + } else if (diff < 12) { + return I18n.t("about.site_age.month", { count: Math.round(diff) }); + } else { + diff /= 12; + return I18n.t("about.site_age.year", { count: Math.round(diff) }); + } + } + diff --git a/app/assets/stylesheets/common/base/about.scss b/app/assets/stylesheets/common/base/about.scss index 4a8af5354f5..f6b878dfa91 100644 --- a/app/assets/stylesheets/common/base/about.scss +++ b/app/assets/stylesheets/common/base/about.scss @@ -1,7 +1,7 @@ .about { &__main-content { display: grid; - grid-template-columns: 2fr 1fr; + grid-template-columns: 2.5fr 1fr; column-gap: 4em; } @@ -23,6 +23,20 @@ min-height: 300px; max-height: 300px; } + + &__activities-item { + display: flex; + align-items: center; + margin-bottom: 1.5em; + } + + &__activities-item-icon { + margin-right: 1em; + } + + &__activities-item-period { + font-size: var(--font-down-2); + } } section.about { diff --git a/app/models/about.rb b/app/models/about.rb index 0cc71542464..26c17495d64 100644 --- a/app/models/about.rb +++ b/app/models/about.rb @@ -64,6 +64,10 @@ class About GlobalPath.full_cdn_url(url) end + def site_creation_date + Discourse.site_creation_date + end + def moderators @moderators ||= User.where(moderator: true, admin: false).human_users.order("last_seen_at DESC") end diff --git a/app/serializers/about_serializer.rb b/app/serializers/about_serializer.rb index c5961242988..30f82d05010 100644 --- a/app/serializers/about_serializer.rb +++ b/app/serializers/about_serializer.rb @@ -22,6 +22,7 @@ class AboutSerializer < ApplicationSerializer :description, :extended_site_description, :banner_image, + :site_creation_date, :title, :locale, :version, @@ -62,6 +63,10 @@ class AboutSerializer < ApplicationSerializer render_redesigned_about_page? end + def include_site_creation_date? + render_redesigned_about_page? + end + private def can_see_about_stats diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 2d636adac1a..4a0a502f518 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -345,6 +345,27 @@ en: active_user_count: "Active users" contact: "Contact us" contact_info: "In the event of a critical issue or urgent matter affecting this site, please contact us at %{contact_info}." + site_activity: "Site activity" + activities: + topics: + one: "%{formatted_number} topic" + other: "%{formatted_number} topics" + posts: + one: "%{formatted_number} post" + other: "%{formatted_number} posts" + active_users: + one: "%{formatted_number} active user" + other: "%{formatted_number} active users" + sign_ups: + one: "%{formatted_number} sign-up" + other: "%{formatted_number} sign-ups" + likes: + one: "%{formatted_number} like" + other: "%{formatted_number} likes" + periods: + last_7_days: "in the last 7 days" + today: "today" + all_time: "all time" member_count: one: "%{formatted_number} Member" other: "%{formatted_number} Members" @@ -355,6 +376,14 @@ en: one: "%{formatted_number} Moderator" other: "%{formatted_number} Moderators" report_inappropriate_content: "If you come across any inappropriate content, don't hesitate to start a conversation with our moderators and admins. Remember to log in before reaching out." + site_age: + less_than_one_month: "Created < 1 month ago" + month: + one: "Created %{count} month ago" + other: "Created %{count} months ago" + year: + one: "Created %{count} year ago" + other: "Created %{count} years ago" bookmarked: title: "Bookmark" diff --git a/lib/discourse.rb b/lib/discourse.rb index c893f6ad9da..53ab64aecee 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -1034,6 +1034,24 @@ module Discourse [SiteSetting.tos_topic_id, SiteSetting.guidelines_topic_id, SiteSetting.privacy_topic_id] end + def self.site_creation_date + @creation_dates ||= {} + current_db = RailsMultisite::ConnectionManagement.current_db + @creation_dates[current_db] ||= begin + result = DB.query_single <<~SQL + SELECT created_at + FROM schema_migration_details + ORDER BY created_at + LIMIT 1 + SQL + result.first + end + end + + def self.clear_site_creation_date_cache + @creation_dates = {} + end + cattr_accessor :last_ar_cache_reset def self.reset_active_record_cache_if_needed(e) diff --git a/lib/svg_sprite.rb b/lib/svg_sprite.rb index ff2269cc8c5..4fd993562bf 100644 --- a/lib/svg_sprite.rb +++ b/lib/svg_sprite.rb @@ -201,6 +201,7 @@ module SvgSprite reply robot rocket + scroll search search-plus search-minus diff --git a/spec/system/about_page_spec.rb b/spec/system/about_page_spec.rb index 7fbf030e4b7..a92553e27fd 100644 --- a/spec/system/about_page_spec.rb +++ b/spec/system/about_page_spec.rb @@ -63,5 +63,113 @@ describe "About page", type: :system do expect(about_page).to have_admins_count(1, "1") expect(about_page).to have_moderators_count(1, "1") end + + describe "displayed site age" do + it "says less than 1 month if the site is less than 1 month old" do + Discourse.stubs(:site_creation_date).returns(1.week.ago) + + about_page.visit + + expect(about_page).to have_site_created_less_than_1_month_ago + end + + it "says how many months old the site is if the site is less than 1 year old" do + Discourse.stubs(:site_creation_date).returns(2.months.ago) + + about_page.visit + + expect(about_page).to have_site_created_in_months_ago(2) + end + + it "says how many years old the site is if the site is more than 1 year old" do + Discourse.stubs(:site_creation_date).returns(5.years.ago) + + about_page.visit + + expect(about_page).to have_site_created_in_years_ago(5) + end + end + + describe "the site activity section" do + describe "topics" do + before do + Fabricate(:topic, created_at: 2.days.ago) + Fabricate(:topic, created_at: 3.days.ago) + Fabricate(:topic, created_at: 8.days.ago) + end + + it "shows the count of topics created in the last 7 days" do + about_page.visit + expect(about_page.site_activities.topics).to have_count(2, "2") + expect(about_page.site_activities.topics).to have_7_days_period + end + end + + describe "posts" do + before do + Fabricate(:post, created_at: 2.days.ago) + Fabricate(:post, created_at: 1.hour.ago) + Fabricate(:post, created_at: 3.hours.ago) + Fabricate(:post, created_at: 23.hours.ago) + end + + it "shows the count of topics created in the last day" do + about_page.visit + expect(about_page.site_activities.posts).to have_count(3, "3") + expect(about_page.site_activities.posts).to have_1_day_period + end + end + + describe "active users" do + before do + User.update_all(last_seen_at: 1.month.ago) + + Fabricate(:user, last_seen_at: 1.hour.ago) + Fabricate(:user, last_seen_at: 1.day.ago) + Fabricate(:user, last_seen_at: 3.days.ago) + Fabricate(:user, last_seen_at: 6.days.ago) + Fabricate(:user, last_seen_at: 8.days.ago) + end + + it "shows the count of active users in the last 7 days" do + about_page.visit + expect(about_page.site_activities.active_users).to have_count(5, "5") # 4 fabricated above + 1 for the current user + expect(about_page.site_activities.active_users).to have_7_days_period + end + end + + describe "sign ups" do + before do + User.update_all(created_at: 1.month.ago) + + Fabricate(:user, created_at: 3.hours.ago) + Fabricate(:user, created_at: 3.days.ago) + Fabricate(:user, created_at: 8.days.ago) + end + + it "shows the count of signups in the last 7 days" do + about_page.visit + expect(about_page.site_activities.sign_ups).to have_count(2, "2") + expect(about_page.site_activities.sign_ups).to have_7_days_period + end + end + + describe "likes" do + before do + UserAction.destroy_all + + Fabricate(:user_action, created_at: 1.hour.ago, action_type: UserAction::LIKE) + Fabricate(:user_action, created_at: 1.day.ago, action_type: UserAction::LIKE) + Fabricate(:user_action, created_at: 1.month.ago, action_type: UserAction::LIKE) + Fabricate(:user_action, created_at: 10.years.ago, action_type: UserAction::LIKE) + end + + it "shows the count of likes of all time" do + about_page.visit + expect(about_page.site_activities.likes).to have_count(4, "4") + expect(about_page.site_activities.likes).to have_all_time_period + end + end + end end end diff --git a/spec/system/page_objects/components/about_page_site_activity.rb b/spec/system/page_objects/components/about_page_site_activity.rb new file mode 100644 index 00000000000..bf369bdfff6 --- /dev/null +++ b/spec/system/page_objects/components/about_page_site_activity.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module PageObjects + module Components + class AboutPageSiteActivity < PageObjects::Components::Base + attr_reader :container + + def initialize(container) + @container = container + end + + def topics + AboutPageSiteActivityItem.new( + container.find(".about__activities-item.topics"), + translation_key: "about.activities.topics", + ) + end + + def posts + AboutPageSiteActivityItem.new( + container.find(".about__activities-item.posts"), + translation_key: "about.activities.posts", + ) + end + + def active_users + AboutPageSiteActivityItem.new( + container.find(".about__activities-item.active-users"), + translation_key: "about.activities.active_users", + ) + end + + def sign_ups + AboutPageSiteActivityItem.new( + container.find(".about__activities-item.sign-ups"), + translation_key: "about.activities.sign_ups", + ) + end + + def likes + AboutPageSiteActivityItem.new( + container.find(".about__activities-item.likes"), + translation_key: "about.activities.likes", + ) + end + end + end +end diff --git a/spec/system/page_objects/components/about_page_site_activity_item.rb b/spec/system/page_objects/components/about_page_site_activity_item.rb new file mode 100644 index 00000000000..56196273366 --- /dev/null +++ b/spec/system/page_objects/components/about_page_site_activity_item.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module PageObjects + module Components + class AboutPageSiteActivityItem < PageObjects::Components::Base + attr_reader :container + + def initialize(container, translation_key:) + @container = container + @translation_key = translation_key + end + + def has_count?(count, formatted_number) + container.find(".about__activities-item-count").has_text?( + I18n.t("js.#{@translation_key}", count: count, formatted_number:), + ) + end + + def has_1_day_period? + period_element.has_text?(I18n.t("js.about.activities.periods.today")) + end + + def has_7_days_period? + period_element.has_text?(I18n.t("js.about.activities.periods.last_7_days")) + end + + def has_all_time_period? + period_element.has_text?(I18n.t("js.about.activities.periods.all_time")) + end + + private + + def period_element + container.find(".about__activities-item-period") + end + end + end +end diff --git a/spec/system/page_objects/pages/about.rb b/spec/system/page_objects/pages/about.rb index 06fb85cb59f..fe59e6e89e5 100644 --- a/spec/system/page_objects/pages/about.rb +++ b/spec/system/page_objects/pages/about.rb @@ -33,6 +33,28 @@ module PageObjects element = find(".about__stats-item.moderators span") element.has_text?(I18n.t("js.about.moderator_count", count:, formatted_number:)) end + + def has_site_created_less_than_1_month_ago? + site_age_stat_element.has_text?(I18n.t("js.about.site_age.less_than_one_month")) + end + + def has_site_created_in_months_ago?(months) + site_age_stat_element.has_text?(I18n.t("js.about.site_age.month", count: months)) + end + + def has_site_created_in_years_ago?(years) + site_age_stat_element.has_text?(I18n.t("js.about.site_age.year", count: years)) + end + + def site_activities + PageObjects::Components::AboutPageSiteActivity.new(find(".about__activities")) + end + + private + + def site_age_stat_element + find(".about__stats-item.site-creation-date span") + end end end end