Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d165376a1 | ||
|
|
b56070313e | ||
|
|
89d78e5401 | ||
|
|
0a8aa07078 | ||
|
|
8593c27e7c |
@@ -4,12 +4,8 @@ module ::DiscourseRewind
|
|||||||
class RewindsController < ::ApplicationController
|
class RewindsController < ::ApplicationController
|
||||||
requires_plugin PLUGIN_NAME
|
requires_plugin PLUGIN_NAME
|
||||||
|
|
||||||
requires_login
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
DiscourseRewind::Rewind::Fetch.call(service_params) do
|
DiscourseRewind::Rewind::Fetch.call(service_params) do
|
||||||
on_model_not_found(:year) { raise Discourse::NotFound }
|
|
||||||
on_model_not_found(:user) { raise Discourse::NotFound }
|
|
||||||
on_success do |reports:|
|
on_success do |reports:|
|
||||||
@reports = reports
|
@reports = reports
|
||||||
render json: MultiJson.dump(reports), status: 200
|
render json: MultiJson.dump(reports), status: 200
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# For a GitHub like calendar
|
|
||||||
# https://docs.github.com/assets/cb-35216/mw-1440/images/help/profile/contributions-graph.webp
|
|
||||||
module DiscourseRewind
|
|
||||||
class Rewind::Action::ActivityCalendar < Rewind::Action::BaseReport
|
|
||||||
FakeData = {
|
|
||||||
data:
|
|
||||||
(Date.new(2024, 1, 1)..Date.new(2024, 12, 31)).map do |date|
|
|
||||||
{ date: date.strftime("%Y-%m-%d"), post_count: rand(0..20), visited: false }
|
|
||||||
end,
|
|
||||||
identifier: "activity-calendar",
|
|
||||||
}
|
|
||||||
|
|
||||||
def call
|
|
||||||
return FakeData if Rails.env.development?
|
|
||||||
|
|
||||||
calendar =
|
|
||||||
Post
|
|
||||||
.unscoped
|
|
||||||
.joins(<<~SQL)
|
|
||||||
RIGHT JOIN
|
|
||||||
generate_series('#{date.first}', '#{date.last}', '1 day'::interval) ON
|
|
||||||
posts.created_at::date = generate_series::date AND
|
|
||||||
posts.user_id = #{user.id} AND
|
|
||||||
posts.deleted_at IS NULL
|
|
||||||
SQL
|
|
||||||
.joins(
|
|
||||||
"LEFT JOIN user_visits ON generate_series::date = visited_at AND user_visits.user_id = #{user.id}",
|
|
||||||
)
|
|
||||||
.select(
|
|
||||||
"generate_series::date as date, count(posts.id) as post_count, COUNT(visited_at) > 0 as visited",
|
|
||||||
)
|
|
||||||
.group("generate_series, user_visits.id")
|
|
||||||
.order("generate_series")
|
|
||||||
.map do |row|
|
|
||||||
{
|
|
||||||
date: row.date.strftime("%Y-%m-%d"),
|
|
||||||
post_count: row.post_count,
|
|
||||||
visited: row.visited,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
{ data: calendar, identifier: "activity-calendar" }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module DiscourseRewind
|
|
||||||
class Rewind::Action::BaseReport < Service::ActionBase
|
|
||||||
option :user
|
|
||||||
option :date
|
|
||||||
|
|
||||||
def call
|
|
||||||
raise NotImplementedError
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.enabled?
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module DiscourseRewind
|
|
||||||
class Rewind::Action::BestPosts < Rewind::Action::BaseReport
|
|
||||||
def call
|
|
||||||
best_posts =
|
|
||||||
Post
|
|
||||||
.where(user_id: user.id)
|
|
||||||
.where(created_at: date)
|
|
||||||
.where(deleted_at: nil)
|
|
||||||
.where("post_number > 1")
|
|
||||||
.order("like_count DESC NULLS LAST")
|
|
||||||
.limit(3)
|
|
||||||
.pluck(:post_number, :topic_id, :like_count, :reply_count, :raw, :cooked)
|
|
||||||
|
|
||||||
{ data: best_posts, identifier: "best-posts" }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module DiscourseRewind
|
|
||||||
class Rewind::Action::BestTopics < Rewind::Action::BaseReport
|
|
||||||
def call
|
|
||||||
best_topics =
|
|
||||||
TopTopic
|
|
||||||
.includes(:topic)
|
|
||||||
.references(:topic)
|
|
||||||
.where(topic: { deleted_at: nil, created_at: date, user_id: user.id })
|
|
||||||
.order("yearly_score DESC NULLS LAST")
|
|
||||||
.limit(3)
|
|
||||||
.pluck(:topic_id, :title, :excerpt, :yearly_score)
|
|
||||||
.map do |topic_id, title, excerpt, yearly_score|
|
|
||||||
{ topic_id: topic_id, title: title, excerpt: excerpt, yearly_score: yearly_score }
|
|
||||||
end
|
|
||||||
|
|
||||||
{ data: best_topics, identifier: "best-topics" }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# Topics visited grouped by category
|
|
||||||
module DiscourseRewind
|
|
||||||
class Rewind::Action::FavoriteCategories < Rewind::Action::BaseReport
|
|
||||||
FakeData = {
|
|
||||||
data: [
|
|
||||||
{ category_id: 1, name: "cats" },
|
|
||||||
{ category_id: 2, name: "dogs" },
|
|
||||||
{ category_id: 3, name: "countries" },
|
|
||||||
{ category_id: 4, name: "management" },
|
|
||||||
{ category_id: 5, name: "things" },
|
|
||||||
],
|
|
||||||
identifier: "favorite-categories",
|
|
||||||
}
|
|
||||||
def call
|
|
||||||
return FakeData if Rails.env.development?
|
|
||||||
|
|
||||||
favorite_categories =
|
|
||||||
TopicViewItem
|
|
||||||
.joins(:topic)
|
|
||||||
.joins("INNER JOIN categories ON categories.id = topics.category_id")
|
|
||||||
.where(user: user)
|
|
||||||
.where(viewed_at: date)
|
|
||||||
.group("categories.id, categories.name")
|
|
||||||
.order("COUNT(*) DESC")
|
|
||||||
.limit(5)
|
|
||||||
.pluck("categories.id, categories.name")
|
|
||||||
.map { |category_id, name| { category_id: category_id, name: name } }
|
|
||||||
|
|
||||||
{ data: favorite_categories, identifier: "favorite-categories" }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# Topics visited grouped by tag
|
|
||||||
module DiscourseRewind
|
|
||||||
class Rewind::Action::FavoriteTags < Rewind::Action::BaseReport
|
|
||||||
FakeData = {
|
|
||||||
data: [
|
|
||||||
{ tag_id: 1, name: "cats" },
|
|
||||||
{ tag_id: 2, name: "dogs" },
|
|
||||||
{ tag_id: 3, name: "countries" },
|
|
||||||
{ tag_id: 4, name: "management" },
|
|
||||||
{ tag_id: 5, name: "things" },
|
|
||||||
],
|
|
||||||
identifier: "favorite-tags",
|
|
||||||
}
|
|
||||||
|
|
||||||
def call
|
|
||||||
return FakeData if Rails.env.development?
|
|
||||||
|
|
||||||
favorite_tags =
|
|
||||||
TopicViewItem
|
|
||||||
.joins(:topic)
|
|
||||||
.joins("INNER JOIN topic_tags ON topic_tags.topic_id = topics.id")
|
|
||||||
.joins("INNER JOIN tags ON tags.id = topic_tags.tag_id")
|
|
||||||
.where(user: user)
|
|
||||||
.where(viewed_at: date)
|
|
||||||
.group("tags.id, tags.name")
|
|
||||||
.order("COUNT(DISTINCT topic_views.topic_id) DESC")
|
|
||||||
.limit(5)
|
|
||||||
.pluck("tags.id, tags.name")
|
|
||||||
.map { |tag_id, name| { tag_id: tag_id, name: name } }
|
|
||||||
|
|
||||||
{ data: favorite_tags, identifier: "favorite-tags" }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# Forum Best Friend Forever ranking
|
|
||||||
# Score is informative only, do not show in UI
|
|
||||||
module DiscourseRewind
|
|
||||||
class Rewind::Action::Fbff < Rewind::Action::BaseReport
|
|
||||||
MAX_SUMMARY_RESULTS ||= 50
|
|
||||||
LIKE_SCORE ||= 1
|
|
||||||
REPLY_SCORE ||= 10
|
|
||||||
|
|
||||||
def call
|
|
||||||
most_liked_users =
|
|
||||||
like_query(date)
|
|
||||||
.where(acting_user_id: user.id)
|
|
||||||
.group(:user_id)
|
|
||||||
.order("COUNT(*) DESC")
|
|
||||||
.limit(MAX_SUMMARY_RESULTS)
|
|
||||||
.pluck("user_actions.user_id, COUNT(*)")
|
|
||||||
.map { |user_id, count| { user_id => count } }
|
|
||||||
.reduce({}, :merge)
|
|
||||||
|
|
||||||
most_liked_by_users =
|
|
||||||
like_query(date)
|
|
||||||
.where(user: user)
|
|
||||||
.group(:acting_user_id)
|
|
||||||
.order("COUNT(*) DESC")
|
|
||||||
.limit(MAX_SUMMARY_RESULTS)
|
|
||||||
.pluck("acting_user_id, COUNT(*)")
|
|
||||||
.map { |acting_user_id, count| { acting_user_id => count } }
|
|
||||||
.reduce({}, :merge)
|
|
||||||
|
|
||||||
users_who_most_replied_me =
|
|
||||||
post_query(user, date)
|
|
||||||
.where(posts: { user_id: user.id })
|
|
||||||
.group("replies.user_id")
|
|
||||||
.order("COUNT(*) DESC")
|
|
||||||
.limit(MAX_SUMMARY_RESULTS)
|
|
||||||
.pluck("replies.user_id, COUNT(*)")
|
|
||||||
.map { |user_id, count| { user_id => count } }
|
|
||||||
.reduce({}, :merge)
|
|
||||||
|
|
||||||
users_i_most_replied =
|
|
||||||
post_query(user, date)
|
|
||||||
.where("replies.user_id = ?", user.id)
|
|
||||||
.group("posts.user_id")
|
|
||||||
.order("COUNT(*) DESC")
|
|
||||||
.limit(MAX_SUMMARY_RESULTS)
|
|
||||||
.pluck("posts.user_id, COUNT(*)")
|
|
||||||
.map { |user_id, count| { user_id => count } }
|
|
||||||
.reduce({}, :merge)
|
|
||||||
|
|
||||||
#TODO include chat?
|
|
||||||
|
|
||||||
fbffs = [
|
|
||||||
apply_score(most_liked_users, LIKE_SCORE),
|
|
||||||
apply_score(most_liked_by_users, LIKE_SCORE),
|
|
||||||
apply_score(users_who_most_replied_me, REPLY_SCORE),
|
|
||||||
apply_score(users_i_most_replied, REPLY_SCORE),
|
|
||||||
]
|
|
||||||
|
|
||||||
fbff_id =
|
|
||||||
fbffs
|
|
||||||
.flatten
|
|
||||||
.inject { |h1, h2| h1.merge(h2) { |_, v1, v2| v1 + v2 } }
|
|
||||||
&.sort_by { |_, v| -v }
|
|
||||||
&.first
|
|
||||||
&.first
|
|
||||||
|
|
||||||
return if !fbff_id
|
|
||||||
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
fbff: BasicUserSerializer.new(User.find(fbff_id), root: false).as_json,
|
|
||||||
yourself: BasicUserSerializer.new(user, root: false).as_json,
|
|
||||||
},
|
|
||||||
identifier: "fbff",
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def post_query(user, date)
|
|
||||||
Post
|
|
||||||
.joins(:topic)
|
|
||||||
.includes(:topic)
|
|
||||||
.where(
|
|
||||||
"posts.post_type IN (?)",
|
|
||||||
Topic.visible_post_types(user, include_moderator_actions: false),
|
|
||||||
)
|
|
||||||
.joins(
|
|
||||||
"INNER JOIN posts replies ON posts.topic_id = replies.topic_id AND posts.reply_to_post_number = replies.post_number",
|
|
||||||
)
|
|
||||||
.joins(
|
|
||||||
"INNER JOIN topics ON replies.topic_id = topics.id AND topics.archetype <> 'private_message'",
|
|
||||||
)
|
|
||||||
.joins(
|
|
||||||
"AND replies.post_type IN (#{Topic.visible_post_types(user, include_moderator_actions: false).join(",")})",
|
|
||||||
)
|
|
||||||
.where("replies.created_at BETWEEN ? AND ?", date.first, date.last)
|
|
||||||
.where("replies.user_id <> posts.user_id")
|
|
||||||
end
|
|
||||||
|
|
||||||
def like_query(date)
|
|
||||||
UserAction
|
|
||||||
.joins(:target_topic, :target_post)
|
|
||||||
.where(created_at: date)
|
|
||||||
.where(action_type: UserAction::WAS_LIKED)
|
|
||||||
end
|
|
||||||
|
|
||||||
def apply_score(users, score)
|
|
||||||
users.map { |user_id, count| { user_id => count * score } }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# For a GitHub like calendar
|
||||||
|
# https://docs.github.com/assets/cb-35216/mw-1440/images/help/profile/contributions-graph.webp
|
||||||
|
module DiscourseRewind
|
||||||
|
class Rewind::Action::PostingCalendar < Service::ActionBase
|
||||||
|
option :user
|
||||||
|
option :date
|
||||||
|
|
||||||
|
def call
|
||||||
|
calendar =
|
||||||
|
Post
|
||||||
|
.where(user: user)
|
||||||
|
.where(created_at: date)
|
||||||
|
.select("DATE(created_at) as date, count(*) as count")
|
||||||
|
.group("DATE(created_at)")
|
||||||
|
.order("DATE(created_at)")
|
||||||
|
.map { |post| { date: post.date, count: post.count } }
|
||||||
|
|
||||||
|
{ data: calendar, identifier: "posting-calendar" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# For a most user / received reactions cards
|
|
||||||
module DiscourseRewind
|
|
||||||
class Rewind::Action::Reactions < Rewind::Action::BaseReport
|
|
||||||
FakeData = {
|
|
||||||
data: {
|
|
||||||
post_received_reactions: {
|
|
||||||
open_mouth: 2,
|
|
||||||
cat: 32,
|
|
||||||
dog: 34,
|
|
||||||
heart: 45,
|
|
||||||
grinning: 82,
|
|
||||||
},
|
|
||||||
post_used_reactions: {
|
|
||||||
open_mouth: 2,
|
|
||||||
cat: 32,
|
|
||||||
dog: 34,
|
|
||||||
heart: 45,
|
|
||||||
grinning: 82,
|
|
||||||
},
|
|
||||||
chat_used_reactions: {
|
|
||||||
open_mouth: 2,
|
|
||||||
cat: 32,
|
|
||||||
dog: 34,
|
|
||||||
heart: 45,
|
|
||||||
grinning: 82,
|
|
||||||
},
|
|
||||||
chat_received_reactions: {
|
|
||||||
open_mouth: 2,
|
|
||||||
cat: 32,
|
|
||||||
dog: 34,
|
|
||||||
heart: 45,
|
|
||||||
grinning: 82,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
identifier: "reactions",
|
|
||||||
}
|
|
||||||
|
|
||||||
def call
|
|
||||||
return FakeData if Rails.env.development?
|
|
||||||
|
|
||||||
data = {}
|
|
||||||
if defined?(DiscourseReactions::Reaction)
|
|
||||||
# This is missing heart reactions (default like)
|
|
||||||
data[:post_used_reactions] = sort_and_limit(
|
|
||||||
DiscourseReactions::Reaction
|
|
||||||
.by_user(user)
|
|
||||||
.where(created_at: date)
|
|
||||||
.group(:reaction_value)
|
|
||||||
.count,
|
|
||||||
)
|
|
||||||
|
|
||||||
data[:post_received_reactions] = sort_and_limit(
|
|
||||||
DiscourseReactions::Reaction
|
|
||||||
.includes(:post)
|
|
||||||
.where(posts: { user_id: user.id })
|
|
||||||
.where(created_at: date)
|
|
||||||
.group(:reaction_value)
|
|
||||||
.count,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
if SiteSetting.chat_enabled
|
|
||||||
data[:chat_used_reactions] = sort_and_limit(
|
|
||||||
Chat::MessageReaction.where(user: user).where(created_at: date).group(:emoji).count,
|
|
||||||
)
|
|
||||||
|
|
||||||
data[:chat_received_reactions] = sort_and_limit(
|
|
||||||
Chat::MessageReaction
|
|
||||||
.includes(:chat_message)
|
|
||||||
.where(chat_message: { user_id: user.id })
|
|
||||||
.where(created_at: date)
|
|
||||||
.group(:emoji)
|
|
||||||
.count,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
{ data:, identifier: "reactions" }
|
|
||||||
end
|
|
||||||
|
|
||||||
def enabled?
|
|
||||||
SiteSetting.discourse_reactions_enabled || SiteSetting.chat_enabled
|
|
||||||
end
|
|
||||||
|
|
||||||
def sort_and_limit(reactions)
|
|
||||||
reactions.sort_by { |_, v| -v }.first(5).reverse.to_h
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -3,198 +3,55 @@
|
|||||||
# For showcasing the reading time of a user
|
# For showcasing the reading time of a user
|
||||||
# Should we show book covers or just the names?
|
# Should we show book covers or just the names?
|
||||||
module DiscourseRewind
|
module DiscourseRewind
|
||||||
class Rewind::Action::ReadingTime < Rewind::Action::BaseReport
|
class Rewind::Action::ReadingTime < Service::ActionBase
|
||||||
FakeData = {
|
option :user
|
||||||
data: {
|
option :date
|
||||||
reading_time: 2_880_000,
|
|
||||||
book: "The Combined Cosmere works + Wheel of Time",
|
|
||||||
isbn: "978-0812511819",
|
|
||||||
series: true,
|
|
||||||
},
|
|
||||||
identifier: "reading-time",
|
|
||||||
}
|
|
||||||
|
|
||||||
def call
|
def call
|
||||||
return FakeData if Rails.env.development?
|
|
||||||
|
|
||||||
reading_time = UserVisit.where(user_id: user.id).where(visited_at: date).sum(:time_read)
|
reading_time = UserVisit.where(user_id: user.id).where(visited_at: date).sum(:time_read)
|
||||||
book = best_book_fit(reading_time)
|
|
||||||
|
|
||||||
return if book.nil?
|
|
||||||
|
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
reading_time: reading_time,
|
reading_time: reading_time,
|
||||||
book: book[:title],
|
books: best_book_fit(reading_time),
|
||||||
isbn: book[:isbn],
|
|
||||||
series: book[:series],
|
|
||||||
},
|
},
|
||||||
identifier: "reading-time",
|
identifier: "reading-time",
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def popular_books
|
def popular_book_reading_time
|
||||||
{
|
{
|
||||||
"The Hunger Games" => {
|
"The Hunger Games" => 19_740,
|
||||||
reading_time: 19_740,
|
"The Metamorphosis" => 3120,
|
||||||
isbn: "978-0439023481",
|
"To Kill a Mockingbird" => 22_800,
|
||||||
series: false,
|
"Pride and Prejudice" => 25_200,
|
||||||
},
|
"1984" => 16_800,
|
||||||
"The Metamorphosis" => {
|
"The Lord of the Rings" => 108_000,
|
||||||
reading_time: 3120,
|
"Harry Potter and the Sorcerer's Stone" => 24_600,
|
||||||
isbn: "978-0553213690",
|
"The Great Gatsby" => 12_600,
|
||||||
series: false,
|
"The Little Prince" => 5400,
|
||||||
},
|
"Animal Farm" => 7200,
|
||||||
"To Kill a Mockingbird" => {
|
"The Catcher in the Rye" => 18_000,
|
||||||
reading_time: 22_800,
|
"Jane Eyre" => 34_200,
|
||||||
isbn: "978-0061120084",
|
"Fahrenheit 451" => 15_000,
|
||||||
series: false,
|
"The Hobbit" => 27_000,
|
||||||
},
|
"The Da Vinci Code" => 37_800,
|
||||||
"Pride and Prejudice" => {
|
"Little Women" => 30_000,
|
||||||
reading_time: 25_200,
|
"One Hundred Years of Solitude" => 46_800,
|
||||||
isbn: "978-1503290563",
|
"And Then There Were None" => 16_200,
|
||||||
series: false,
|
"The Alchemist" => 10_800,
|
||||||
},
|
"The Hitchhiker's Guide to the Galaxy" => 12_600,
|
||||||
"1984" => {
|
|
||||||
reading_time: 16_800,
|
|
||||||
isbn: "978-0451524935",
|
|
||||||
series: false,
|
|
||||||
},
|
|
||||||
"The Lord of the Rings" => {
|
|
||||||
reading_time: 108_000,
|
|
||||||
isbn: "978-0544003415",
|
|
||||||
series: true,
|
|
||||||
},
|
|
||||||
"Harry Potter and the Sorcerer's Stone" => {
|
|
||||||
reading_time: 24_600,
|
|
||||||
isbn: "978-0590353427",
|
|
||||||
series: true,
|
|
||||||
},
|
|
||||||
"The Great Gatsby" => {
|
|
||||||
reading_time: 12_600,
|
|
||||||
isbn: "978-0743273565",
|
|
||||||
series: false,
|
|
||||||
},
|
|
||||||
"The Little Prince" => {
|
|
||||||
reading_time: 5400,
|
|
||||||
isbn: "978-0156012195",
|
|
||||||
series: false,
|
|
||||||
},
|
|
||||||
"Animal Farm" => {
|
|
||||||
reading_time: 7200,
|
|
||||||
isbn: "978-0451526342",
|
|
||||||
series: false,
|
|
||||||
},
|
|
||||||
"The Catcher in the Rye" => {
|
|
||||||
reading_time: 18_000,
|
|
||||||
isbn: "978-0316769488",
|
|
||||||
series: false,
|
|
||||||
},
|
|
||||||
"Jane Eyre" => {
|
|
||||||
reading_time: 34_200,
|
|
||||||
isbn: "978-0141441146",
|
|
||||||
series: false,
|
|
||||||
},
|
|
||||||
"Fahrenheit 451" => {
|
|
||||||
reading_time: 15_000,
|
|
||||||
isbn: "978-1451673319",
|
|
||||||
series: false,
|
|
||||||
},
|
|
||||||
"The Hobbit" => {
|
|
||||||
reading_time: 27_000,
|
|
||||||
isbn: "978-0547928227",
|
|
||||||
series: false,
|
|
||||||
},
|
|
||||||
"The Da Vinci Code" => {
|
|
||||||
reading_time: 37_800,
|
|
||||||
isbn: "978-0307474278",
|
|
||||||
series: false,
|
|
||||||
},
|
|
||||||
"Little Women" => {
|
|
||||||
reading_time: 30_000,
|
|
||||||
isbn: "978-0147514011",
|
|
||||||
series: false,
|
|
||||||
},
|
|
||||||
"One Hundred Years of Solitude" => {
|
|
||||||
reading_time: 46_800,
|
|
||||||
isbn: "978-0060883287",
|
|
||||||
series: false,
|
|
||||||
},
|
|
||||||
"And Then There Were None" => {
|
|
||||||
reading_time: 16_200,
|
|
||||||
isbn: "978-0062073488",
|
|
||||||
series: false,
|
|
||||||
},
|
|
||||||
"The Alchemist" => {
|
|
||||||
reading_time: 10_800,
|
|
||||||
isbn: "978-0061122415",
|
|
||||||
series: false,
|
|
||||||
},
|
|
||||||
"The Hitchhiker's Guide to the Galaxy" => {
|
|
||||||
reading_time: 12_600,
|
|
||||||
isbn: "978-0345391803",
|
|
||||||
series: false,
|
|
||||||
},
|
|
||||||
"The Complete works of Shakespeare" => {
|
|
||||||
reading_time: 180_000,
|
|
||||||
isbn: "978-1853268953",
|
|
||||||
series: true,
|
|
||||||
},
|
|
||||||
"The Game of Thrones Series" => {
|
|
||||||
reading_time: 360_000,
|
|
||||||
isbn: "978-0007477159",
|
|
||||||
series: true,
|
|
||||||
},
|
|
||||||
"Malazan Book of the Fallen" => {
|
|
||||||
reading_time: 720_000,
|
|
||||||
isbn: "978-0765348821",
|
|
||||||
series: true,
|
|
||||||
},
|
|
||||||
"Terry Pratchett’s Discworld series" => {
|
|
||||||
reading_time: 1_440_000,
|
|
||||||
isbn: "978-9123684458",
|
|
||||||
series: true,
|
|
||||||
},
|
|
||||||
"The Wandering Inn web series" => {
|
|
||||||
reading_time: 2_160_000,
|
|
||||||
isbn: "the-wandering-inn",
|
|
||||||
series: true,
|
|
||||||
},
|
|
||||||
"The Combined Cosmere works + Wheel of Time" => {
|
|
||||||
reading_time: 2_880_000,
|
|
||||||
isbn: "978-0812511819",
|
|
||||||
series: true,
|
|
||||||
},
|
|
||||||
"The Star Trek novels" => {
|
|
||||||
reading_time: 3_600_000,
|
|
||||||
isbn: "978-1852860691",
|
|
||||||
series: true,
|
|
||||||
},
|
|
||||||
}.symbolize_keys
|
}.symbolize_keys
|
||||||
end
|
end
|
||||||
|
|
||||||
def best_book_fit(reading_time)
|
def best_book_fit(reading_time)
|
||||||
reading_time_rest = reading_time
|
reading_time_rest = reading_time
|
||||||
books = []
|
books = []
|
||||||
|
|
||||||
while reading_time_rest > 0
|
while reading_time_rest > 0
|
||||||
best_fit = popular_books.min_by { |_, v| (v[:reading_time] - reading_time_rest).abs }
|
books << popular_book_reading_time.min_by { |_, v| (v - reading_time_rest).abs }.first
|
||||||
break if best_fit.nil?
|
reading_time_rest -= popular_book_reading_time[books.last]
|
||||||
|
|
||||||
books << best_fit.first
|
|
||||||
reading_time_rest -= best_fit.last[:reading_time]
|
|
||||||
end
|
end
|
||||||
|
books.group_by(&:itself).transform_values(&:count)
|
||||||
return if books.empty?
|
|
||||||
|
|
||||||
book_title =
|
|
||||||
books.group_by { |book| book }.transform_values(&:count).max_by { |_, count| count }.first
|
|
||||||
|
|
||||||
{
|
|
||||||
title: book_title,
|
|
||||||
isbn: popular_books[book_title][:isbn],
|
|
||||||
series: popular_books[book_title][:series],
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module DiscourseRewind
|
|
||||||
class Rewind::Action::TopWords < Rewind::Action::BaseReport
|
|
||||||
FakeData = {
|
|
||||||
data: [
|
|
||||||
{ word: "what", score: 100 },
|
|
||||||
{ word: "have", score: 90 },
|
|
||||||
{ word: "you", score: 80 },
|
|
||||||
{ word: "overachieved", score: 70 },
|
|
||||||
{ word: "this", score: 60 },
|
|
||||||
{ word: "week", score: 50 },
|
|
||||||
],
|
|
||||||
identifier: "top-words",
|
|
||||||
}
|
|
||||||
|
|
||||||
def call
|
|
||||||
return FakeData if Rails.env.development?
|
|
||||||
|
|
||||||
words = DB.query(<<~SQL, user_id: user.id, date_start: date.first, date_end: date.last)
|
|
||||||
WITH popular_words AS (
|
|
||||||
SELECT
|
|
||||||
*
|
|
||||||
FROM
|
|
||||||
ts_stat(
|
|
||||||
$INNERSQL$
|
|
||||||
SELECT
|
|
||||||
search_data
|
|
||||||
FROM
|
|
||||||
post_search_data
|
|
||||||
INNER JOIN
|
|
||||||
posts ON posts.id = post_search_data.post_id
|
|
||||||
WHERE
|
|
||||||
posts.user_id = :user_id
|
|
||||||
AND posts.created_at BETWEEN :date_start AND :date_end
|
|
||||||
$INNERSQL$
|
|
||||||
) AS search_data
|
|
||||||
ORDER BY
|
|
||||||
nentry DESC,
|
|
||||||
ndoc DESC,
|
|
||||||
word
|
|
||||||
LIMIT
|
|
||||||
100
|
|
||||||
), lex AS (
|
|
||||||
SELECT
|
|
||||||
DISTINCT ON (lexeme) to_tsvector('english', word) as lexeme,
|
|
||||||
word as original_word
|
|
||||||
FROM
|
|
||||||
ts_stat ($INNERSQL$
|
|
||||||
SELECT
|
|
||||||
to_tsvector('simple', raw)
|
|
||||||
FROM
|
|
||||||
posts AS p
|
|
||||||
WHERE
|
|
||||||
p.created_at BETWEEN :date_start AND :date_end
|
|
||||||
AND p.user_id = :user_id
|
|
||||||
$INNERSQL$)
|
|
||||||
), ranked_words AS (
|
|
||||||
SELECT
|
|
||||||
popular_words.*, lex.original_word,
|
|
||||||
ROW_NUMBER() OVER (PARTITION BY word ORDER BY LENGTH(original_word)) AS rn
|
|
||||||
FROM
|
|
||||||
popular_words
|
|
||||||
INNER JOIN
|
|
||||||
lex ON lex.lexeme @@ to_tsquery('english', popular_words.word)
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
word,
|
|
||||||
ndoc,
|
|
||||||
nentry,
|
|
||||||
original_word
|
|
||||||
FROM
|
|
||||||
ranked_words
|
|
||||||
WHERE
|
|
||||||
rn = 1
|
|
||||||
ORDER BY
|
|
||||||
ndoc + nentry DESC
|
|
||||||
LIMIT 100
|
|
||||||
SQL
|
|
||||||
|
|
||||||
word_score = words.map { { word: _1.original_word, score: _1.ndoc + _1.nentry } }
|
|
||||||
|
|
||||||
{ data: word_score, identifier: "top-words" }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -18,9 +18,14 @@ module DiscourseRewind
|
|||||||
# @option params [Integer] :username of the rewind
|
# @option params [Integer] :username of the rewind
|
||||||
# @return [Service::Base::Context]
|
# @return [Service::Base::Context]
|
||||||
|
|
||||||
|
# order matters, rewinds are displayed in the order they are defined
|
||||||
|
REPORTS = [
|
||||||
|
DiscourseRewind::Rewind::Action::ReadingTime,
|
||||||
|
DiscourseRewind::Rewind::Action::PostingCalendar,
|
||||||
|
].freeze
|
||||||
|
|
||||||
CACHE_DURATION = 5.minutes
|
CACHE_DURATION = 5.minutes
|
||||||
|
|
||||||
model :year
|
|
||||||
model :user
|
model :user
|
||||||
model :date
|
model :date
|
||||||
model :reports
|
model :reports
|
||||||
@@ -31,38 +36,26 @@ module DiscourseRewind
|
|||||||
User.find_by_username(guardian.user.username)
|
User.find_by_username(guardian.user.username)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_year
|
def fetch_date(params:)
|
||||||
current_date = Time.zone.now
|
Date.new(2024).all_year
|
||||||
current_month = current_date.month
|
end
|
||||||
current_year = current_date.year
|
|
||||||
|
|
||||||
case current_month
|
def fetch_reports(date:, user:, guardian:)
|
||||||
when 1
|
key = "rewind:#{guardian.user.username}:#{2024}"
|
||||||
current_year - 1
|
reports = Discourse.redis.get(key)
|
||||||
when 12
|
|
||||||
current_year
|
if Rails.env.development? || !reports
|
||||||
|
reports = REPORTS.map { |report| report.call(date:, user:, guardian:) }
|
||||||
|
Discourse.redis.setex(key, CACHE_DURATION, MultiJson.dump(reports))
|
||||||
else
|
else
|
||||||
false
|
reports = MultiJson.load(reports, symbolize_keys: true)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_date(params:, year:)
|
if Rails.env.development?
|
||||||
Date.new(year).all_year
|
reports.each do |report|
|
||||||
end
|
puts "\n--- #{report[:identifier]} ---\n"
|
||||||
|
pp report
|
||||||
def fetch_reports(date:, user:, guardian:, year:)
|
end
|
||||||
key = "rewind:#{guardian.user.id}:#{year}"
|
|
||||||
cached_reports = Discourse.redis.get(key)
|
|
||||||
|
|
||||||
if !cached_reports
|
|
||||||
reports =
|
|
||||||
::DiscourseRewind::Rewind::Action::BaseReport
|
|
||||||
.descendants
|
|
||||||
.filter { _1.enabled? }
|
|
||||||
.map { |report| report.call(date:, user:, guardian:) }
|
|
||||||
Discourse.redis.setex(key, CACHE_DURATION, reports.to_json)
|
|
||||||
else
|
|
||||||
reports = JSON.parse(cached_reports, symbolize_names: true)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
reports
|
reports
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
import Component from "@glimmer/component";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import concatClass from "discourse/helpers/concat-class";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
|
|
||||||
const ROWS = 7;
|
|
||||||
const COLS = 53;
|
|
||||||
|
|
||||||
export default class ActivityCalendar extends Component {
|
|
||||||
get rowsArray() {
|
|
||||||
const data = this.args.report.data;
|
|
||||||
let rowsArray = [];
|
|
||||||
|
|
||||||
for (let r = 0; r < ROWS; r++) {
|
|
||||||
let rowData = [];
|
|
||||||
for (let c = 0; c < COLS; c++) {
|
|
||||||
const index = c * ROWS + r;
|
|
||||||
rowData.push(data[index] ? data[index] : "");
|
|
||||||
}
|
|
||||||
rowsArray.push(rowData);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rowsArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
getAbbreviatedMonth(monthIndex) {
|
|
||||||
return moment().month(monthIndex).format("MMM");
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
computeClass(count) {
|
|
||||||
if (!count) {
|
|
||||||
return "-empty";
|
|
||||||
} else if (count < 10) {
|
|
||||||
return "-low";
|
|
||||||
} else if (count < 20) {
|
|
||||||
return "-medium";
|
|
||||||
} else {
|
|
||||||
return "-high";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="rewind-report-page -activity-calendar">
|
|
||||||
<h2 class="rewind-report-title">{{i18n
|
|
||||||
"discourse_rewind.reports.activity_calendar.title"
|
|
||||||
}}</h2>
|
|
||||||
|
|
||||||
<div class="rewind-card">
|
|
||||||
<table class="rewind-calendar">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colspan="5"
|
|
||||||
class="activity-header-cell"
|
|
||||||
>{{this.getAbbreviatedMonth 0}}</td>
|
|
||||||
<td
|
|
||||||
colspan="4"
|
|
||||||
class="activity-header-cell"
|
|
||||||
>{{this.getAbbreviatedMonth 1}}</td>
|
|
||||||
<td
|
|
||||||
colspan="4"
|
|
||||||
class="activity-header-cell"
|
|
||||||
>{{this.getAbbreviatedMonth 2}}</td>
|
|
||||||
<td
|
|
||||||
colspan="5"
|
|
||||||
class="activity-header-cell"
|
|
||||||
>{{this.getAbbreviatedMonth 3}}</td>
|
|
||||||
<td
|
|
||||||
colspan="4"
|
|
||||||
class="activity-header-cell"
|
|
||||||
>{{this.getAbbreviatedMonth 4}}</td>
|
|
||||||
<td
|
|
||||||
colspan="4"
|
|
||||||
class="activity-header-cell"
|
|
||||||
>{{this.getAbbreviatedMonth 5}}</td>
|
|
||||||
<td
|
|
||||||
colspan="5"
|
|
||||||
class="activity-header-cell"
|
|
||||||
>{{this.getAbbreviatedMonth 6}}</td>
|
|
||||||
<td
|
|
||||||
colspan="4"
|
|
||||||
class="activity-header-cell"
|
|
||||||
>{{this.getAbbreviatedMonth 7}}</td>
|
|
||||||
<td
|
|
||||||
colspan="5"
|
|
||||||
class="activity-header-cell"
|
|
||||||
>{{this.getAbbreviatedMonth 8}}</td>
|
|
||||||
<td
|
|
||||||
colspan="4"
|
|
||||||
class="activity-header-cell"
|
|
||||||
>{{this.getAbbreviatedMonth 9}}</td>
|
|
||||||
<td
|
|
||||||
colspan="4"
|
|
||||||
class="activity-header-cell"
|
|
||||||
>{{this.getAbbreviatedMonth 10}}</td>
|
|
||||||
<td
|
|
||||||
colspan="4"
|
|
||||||
class="activity-header-cell"
|
|
||||||
>{{this.getAbbreviatedMonth 11}}</td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{#each this.rowsArray as |row|}}
|
|
||||||
<tr>
|
|
||||||
{{#each row as |cell|}}
|
|
||||||
<td
|
|
||||||
data-date={{cell.date}}
|
|
||||||
title={{cell.date}}
|
|
||||||
class={{concatClass
|
|
||||||
"rewind-calendar-cell"
|
|
||||||
(this.computeClass cell.post_count)
|
|
||||||
}}
|
|
||||||
></td>
|
|
||||||
{{/each}}
|
|
||||||
</tr>
|
|
||||||
{{/each}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import Component from "@glimmer/component";
|
|
||||||
import { concat } from "@ember/helper";
|
|
||||||
import { get } from "@ember/object";
|
|
||||||
import { htmlSafe } from "@ember/template";
|
|
||||||
import icon from "discourse-common/helpers/d-icon";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
|
|
||||||
export default class BestPosts extends Component {
|
|
||||||
rank(idx) {
|
|
||||||
return idx + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
{{#if @report.data.length}}
|
|
||||||
<div class="rewind-report-page -best-posts">
|
|
||||||
<h2 class="rewind-report-title">{{i18n
|
|
||||||
"discourse_rewind.reports.best_posts.title"
|
|
||||||
[email protected]
|
|
||||||
}}</h2>
|
|
||||||
<div class="rewind-report-container">
|
|
||||||
{{#each @report.data as |post idx|}}
|
|
||||||
<div class={{concat "rewind-card" " rank-" (this.rank idx)}}>
|
|
||||||
<span class="best-posts -rank"></span>
|
|
||||||
<span class="best-posts -rank"></span>
|
|
||||||
<div class="best-posts__post">{{htmlSafe (get post "5")}}</div>
|
|
||||||
<div class="best-posts__metadata">
|
|
||||||
<span class="best-posts__likes">
|
|
||||||
{{icon "heart"}}{{htmlSafe (get post "2")}}</span>
|
|
||||||
<span class="best-posts__replies">
|
|
||||||
{{icon "comment"}}{{htmlSafe (get post "3")}}</span>
|
|
||||||
<a href="/t/{{get post '1'}}/{{get post '0'}}">{{i18n
|
|
||||||
"discourse_rewind.reports.best_posts.view_post"
|
|
||||||
}}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</template>
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import Component from "@glimmer/component";
|
|
||||||
import { concat } from "@ember/helper";
|
|
||||||
import { htmlSafe } from "@ember/template";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
|
|
||||||
export default class BestTopics extends Component {
|
|
||||||
rank(idx) {
|
|
||||||
return idx + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
{{#if @report.data.length}}
|
|
||||||
<div class="rewind-report-page -best-topics">
|
|
||||||
<h2 class="rewind-report-title">{{i18n
|
|
||||||
"discourse_rewind.reports.best_topics.title"
|
|
||||||
[email protected]
|
|
||||||
}}</h2>
|
|
||||||
<div class="rewind-report-container">
|
|
||||||
<div class="rewind-card">
|
|
||||||
{{#each @report.data as |topic idx|}}
|
|
||||||
<a
|
|
||||||
href={{concat "/t/-/" topic.topic_id}}
|
|
||||||
class={{concat "best-topics__topic" " rank-" (this.rank idx)}}
|
|
||||||
>
|
|
||||||
<span class="best-topics -rank"></span>
|
|
||||||
<span class="best-topics -rank"></span>
|
|
||||||
<h2 class="best-topics__header">{{topic.title}}</h2>
|
|
||||||
<span class="best-topics__excerpt">{{htmlSafe
|
|
||||||
topic.excerpt
|
|
||||||
}}</span>
|
|
||||||
</a>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</template>
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { concat } from "@ember/helper";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
|
|
||||||
const FavoriteCategories = <template>
|
|
||||||
{{#if @report.data.length}}
|
|
||||||
<div class="rewind-report-page -favorite-categories">
|
|
||||||
<h2 class="rewind-report-title">{{i18n
|
|
||||||
"discourse_rewind.reports.favorite_categories.title"
|
|
||||||
[email protected]
|
|
||||||
}}</h2>
|
|
||||||
<div class="rewind-report-container">
|
|
||||||
{{#each @report.data as |data|}}
|
|
||||||
<a href={{concat "/c/-/" data.category_id}} class="rewind-card">
|
|
||||||
<p
|
|
||||||
class="favorite-categories__category"
|
|
||||||
href={{concat "/c/-/" data.category_id}}
|
|
||||||
>{{data.name}}</p>
|
|
||||||
</a>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</template>;
|
|
||||||
|
|
||||||
export default FavoriteCategories;
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { concat } from "@ember/helper";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
|
|
||||||
const FavoriteTags = <template>
|
|
||||||
{{#if @report.data.length}}
|
|
||||||
<div class="rewind-report-page -favorite-tags">
|
|
||||||
<h2 class="rewind-report-title">{{i18n
|
|
||||||
"discourse_rewind.reports.favorite_tags.title"
|
|
||||||
[email protected]
|
|
||||||
}}</h2>
|
|
||||||
<div class="rewind-report-container">
|
|
||||||
{{#each @report.data as |data|}}
|
|
||||||
<a class="rewind-card" href={{concat "/tag/" data.name}}>
|
|
||||||
<p
|
|
||||||
class="favorite-tags__tag"
|
|
||||||
href={{concat "/tag/" data.name}}
|
|
||||||
>#{{data.name}}</p>
|
|
||||||
</a>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</template>;
|
|
||||||
|
|
||||||
export default FavoriteTags;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { hash } from "@ember/helper";
|
|
||||||
import avatar from "discourse/helpers/bound-avatar-template";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
|
|
||||||
const FBFF = <template>
|
|
||||||
<div class="rewind-report-page -fbff">
|
|
||||||
<h2 class="rewind-report-title">{{i18n
|
|
||||||
"discourse_rewind.reports.fbff.title"
|
|
||||||
}}</h2>
|
|
||||||
<div class="rewind-report-container">
|
|
||||||
<div class="rewind-card">
|
|
||||||
<div class="fbff-avatar-container">
|
|
||||||
{{avatar
|
|
||||||
@report.data.fbff.avatar_template
|
|
||||||
"huge"
|
|
||||||
(hash [email protected])
|
|
||||||
}}
|
|
||||||
<p class="fbff-avatar-name">@{{@report.data.fbff.username}}</p>
|
|
||||||
</div>
|
|
||||||
<div class="fbff-gif-container">
|
|
||||||
<img
|
|
||||||
class="fbff-gif"
|
|
||||||
src="/plugins/discourse-rewind/images/fbff.gif"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="fbff-avatar-container">
|
|
||||||
{{avatar
|
|
||||||
@report.data.yourself.avatar_template
|
|
||||||
"huge"
|
|
||||||
(hash [email protected])
|
|
||||||
}}
|
|
||||||
<p class="fbff-avatar-name">@{{@report.data.yourself.username}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>;
|
|
||||||
|
|
||||||
export default FBFF;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
const RewindHeader = <template>
|
|
||||||
<div class="rewind__header">
|
|
||||||
<img
|
|
||||||
class="rewind-logo -light"
|
|
||||||
src="/plugins/discourse-rewind/images/discourse-rewind-logo.png"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
class="rewind-logo -dark"
|
|
||||||
src="/plugins/discourse-rewind/images/discourse-rewind-logo-dark.png"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>;
|
|
||||||
|
|
||||||
export default RewindHeader;
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import Component from "@glimmer/component";
|
|
||||||
import { concat } from "@ember/helper";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import { htmlSafe } from "@ember/template";
|
|
||||||
import replaceEmoji from "discourse/helpers/replace-emoji";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
|
|
||||||
export default class Reactions extends Component {
|
|
||||||
get totalPostUsedReactions() {
|
|
||||||
return Object.values(
|
|
||||||
this.args.report.data.post_used_reactions ?? {}
|
|
||||||
).reduce((acc, count) => acc + count, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
get receivedReactions() {
|
|
||||||
return this.args.report.data.post_received_reactions ?? {};
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
cleanEmoji(emojiName) {
|
|
||||||
return emojiName.replaceAll(/_/g, " ");
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
computePercentage(count) {
|
|
||||||
return `${((count / this.totalPostUsedReactions) * 100).toFixed(2)}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
computePercentageStyle(count) {
|
|
||||||
return htmlSafe(`width: ${this.computePercentage(count)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="rewind-report-page -post-received-reactions">
|
|
||||||
<h2 class="rewind-report-title">{{i18n
|
|
||||||
"discourse_rewind.reports.post_received_reactions.title"
|
|
||||||
}}</h2>
|
|
||||||
<div class="rewind-report-container">
|
|
||||||
{{#each-in this.receivedReactions as |emojiName count|}}
|
|
||||||
<div class="rewind-card scale">
|
|
||||||
<span class="rewind-card__emoji">{{replaceEmoji
|
|
||||||
(concat ":" emojiName ":")
|
|
||||||
}}</span>
|
|
||||||
<span class="rewind-card__data">{{count}}</span>
|
|
||||||
</div>
|
|
||||||
{{/each-in}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rewind-report-page -post-used-reactions">
|
|
||||||
<h2 class="rewind-report-title">{{i18n
|
|
||||||
"discourse_rewind.reports.post_used_reactions.title"
|
|
||||||
}}</h2>
|
|
||||||
<div class="rewind-card">
|
|
||||||
<div class="rewind-reactions-chart">
|
|
||||||
{{#each-in @report.data.post_used_reactions as |emojiName count|}}
|
|
||||||
<div class="rewind-reactions-row">
|
|
||||||
<span class="emoji">{{replaceEmoji
|
|
||||||
(concat ":" emojiName ":")
|
|
||||||
}}</span>
|
|
||||||
<span class="percentage">{{this.computePercentage count}}</span>
|
|
||||||
<div
|
|
||||||
class="rewind-reactions-bar"
|
|
||||||
style={{this.computePercentageStyle count}}
|
|
||||||
title={{count}}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
{{/each-in}}
|
|
||||||
|
|
||||||
<span class="rewind-total-reactions">
|
|
||||||
{{i18n
|
|
||||||
"discourse_rewind.reports.post_used_reactions.total_number"
|
|
||||||
count=this.totalPostUsedReactions
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import Component from "@glimmer/component";
|
|
||||||
import { htmlSafe } from "@ember/template";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
|
|
||||||
export default class ReadingTime extends Component {
|
|
||||||
get readTimeString() {
|
|
||||||
let totalMinutes = Math.floor(this.args.report.data.reading_time / 60);
|
|
||||||
let leftOverMinutes = totalMinutes % 60;
|
|
||||||
let totalHours = (totalMinutes - leftOverMinutes) / 60;
|
|
||||||
|
|
||||||
if (leftOverMinutes >= 35) {
|
|
||||||
totalHours += 1;
|
|
||||||
leftOverMinutes = 0;
|
|
||||||
return `${totalHours}h`;
|
|
||||||
} else {
|
|
||||||
return `${totalHours}h ${leftOverMinutes}m`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
{{#if @report.data}}
|
|
||||||
<div class="rewind-report-page -reading-time">
|
|
||||||
<h2 class="rewind-report-title">{{i18n
|
|
||||||
"discourse_rewind.reports.reading_time.title"
|
|
||||||
}}</h2>
|
|
||||||
<div class="rewind-card">
|
|
||||||
<p class="reading-time__text">
|
|
||||||
{{htmlSafe
|
|
||||||
(i18n
|
|
||||||
"discourse_rewind.reports.reading_time.book_comparison"
|
|
||||||
readingTitme=this.readTimeString
|
|
||||||
[email protected]
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
<div class="reading-time__book">
|
|
||||||
<div class="book">
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
src="/plugins/discourse-rewind/images/books/{{@report.data.isbn}}.jpg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{#if @report.data.series}}
|
|
||||||
<div class="book-series one"></div>
|
|
||||||
<div class="book-series two"></div>
|
|
||||||
<div class="book-series three"></div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</template>
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import Component from "@glimmer/component";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
import WordCard from "discourse/plugins/discourse-rewind/discourse/components/reports/top-words/word-card";
|
|
||||||
|
|
||||||
export default class WordCards extends Component {
|
|
||||||
get topWords() {
|
|
||||||
return this.args.report.data.sort((a, b) => b.score - a.score).slice(0, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="rewind-report-page -top-words">
|
|
||||||
<div class="rewind-report-container">
|
|
||||||
<h2 class="rewind-report-title">{{i18n
|
|
||||||
"discourse_rewind.reports.top_words.title"
|
|
||||||
}}</h2>
|
|
||||||
<div class="cards-container">
|
|
||||||
{{#each this.topWords as |entry index|}}
|
|
||||||
<WordCard
|
|
||||||
@word={{entry.word}}
|
|
||||||
@count={{entry.score}}
|
|
||||||
@index={{index}}
|
|
||||||
/>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import Component from "@glimmer/component";
|
|
||||||
import { on } from "@ember/modifier";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
|
||||||
import { htmlSafe } from "@ember/template";
|
|
||||||
import concatClass from "discourse/helpers/concat-class";
|
|
||||||
import emoji from "discourse/helpers/emoji";
|
|
||||||
import discourseLater from "discourse-common/lib/later";
|
|
||||||
|
|
||||||
const MYSTERY_EMOJIS = [
|
|
||||||
"mag", // 🔍
|
|
||||||
"question", // ❓
|
|
||||||
"8ball", // 🎱
|
|
||||||
"crystal_ball", // 🔮
|
|
||||||
"crescent_moon", // 🌙
|
|
||||||
];
|
|
||||||
|
|
||||||
const BACKGROUND_COLORS = [
|
|
||||||
[
|
|
||||||
"251, 245, 175",
|
|
||||||
"40, 171, 226",
|
|
||||||
"12, 166, 78",
|
|
||||||
"240, 121, 74",
|
|
||||||
"232, 74, 81",
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"197, 193, 140",
|
|
||||||
"39, 137, 178",
|
|
||||||
"17, 138, 68",
|
|
||||||
"188, 105, 65",
|
|
||||||
"183, 64, 70",
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
export default class WordCard extends Component {
|
|
||||||
get randomStyle() {
|
|
||||||
return `--rand: ${Math.random()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
get mysteryData() {
|
|
||||||
return {
|
|
||||||
emoji: MYSTERY_EMOJIS[this.args.index],
|
|
||||||
color: `--mystery-color-light: ${
|
|
||||||
BACKGROUND_COLORS[0][this.args.index]
|
|
||||||
}; --mystery-color-dark: ${BACKGROUND_COLORS[1][this.args.index]};`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get longWord() {
|
|
||||||
return this.args.word.length >= 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
get cardStyle() {
|
|
||||||
return htmlSafe(`${this.randomStyle}; ${this.mysteryData.color};`);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
registerCardContainer(element) {
|
|
||||||
this.cardContainer = element;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
handleClick() {
|
|
||||||
this.cardContainer.classList.toggle("flipped");
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
handleLeave() {
|
|
||||||
const cardContainer = this.cardContainer;
|
|
||||||
cardContainer.classList.toggle("mouseleave");
|
|
||||||
discourseLater(() => {
|
|
||||||
cardContainer.classList.remove("mouseleave");
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
{{on "click" this.handleClick}}
|
|
||||||
{{on "mouseleave" this.handleLeave}}
|
|
||||||
class={{concatClass
|
|
||||||
"rewind-card__wrapper"
|
|
||||||
(if this.longWord "-long-word")
|
|
||||||
}}
|
|
||||||
style={{this.cardStyle}}
|
|
||||||
{{didInsert this.registerCardContainer}}
|
|
||||||
role="button"
|
|
||||||
>
|
|
||||||
<div class="rewind-card__inner">
|
|
||||||
<div class="rewind-card -front">
|
|
||||||
<span class="rewind-card__image tl">{{emoji
|
|
||||||
this.mysteryData.emoji
|
|
||||||
}}</span>
|
|
||||||
<span class="rewind-card__image cr">{{emoji
|
|
||||||
this.mysteryData.emoji
|
|
||||||
}}</span>
|
|
||||||
<span class="rewind-card__image br">{{emoji
|
|
||||||
this.mysteryData.emoji
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="rewind-card -back">
|
|
||||||
<span class="rewind-card__title">{{@word}}</span>
|
|
||||||
<span class="rewind-card__data">{{@count}}x</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
}
|
|
||||||
@@ -1,152 +1,27 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { on } from "@ember/modifier";
|
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||||
import { service } from "@ember/service";
|
|
||||||
import { eq } from "truth-helpers";
|
|
||||||
import DButton from "discourse/components/d-button";
|
|
||||||
import concatClass from "discourse/helpers/concat-class";
|
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import ActivityCalendar from "discourse/plugins/discourse-rewind/discourse/components/reports/activity-calendar";
|
|
||||||
import BestPosts from "discourse/plugins/discourse-rewind/discourse/components/reports/best-posts";
|
|
||||||
import BestTopics from "discourse/plugins/discourse-rewind/discourse/components/reports/best-topics";
|
|
||||||
import FavoriteCategories from "discourse/plugins/discourse-rewind/discourse/components/reports/favorite-categories";
|
|
||||||
import FavoriteTags from "discourse/plugins/discourse-rewind/discourse/components/reports/favorite-tags";
|
|
||||||
import FBFF from "discourse/plugins/discourse-rewind/discourse/components/reports/fbff";
|
|
||||||
import RewindHeader from "discourse/plugins/discourse-rewind/discourse/components/reports/header";
|
|
||||||
import Reactions from "discourse/plugins/discourse-rewind/discourse/components/reports/reactions";
|
|
||||||
import ReadingTime from "discourse/plugins/discourse-rewind/discourse/components/reports/reading-time";
|
|
||||||
import TopWords from "discourse/plugins/discourse-rewind/discourse/components/reports/top-words";
|
|
||||||
|
|
||||||
export default class Rewind extends Component {
|
export default class Rewind extends Component {
|
||||||
@service siteSettings;
|
|
||||||
|
|
||||||
@tracked rewind = [];
|
@tracked rewind = [];
|
||||||
|
|
||||||
@tracked fullScreen = true;
|
|
||||||
|
|
||||||
@tracked loadingRewind = false;
|
|
||||||
|
|
||||||
@action
|
|
||||||
registerScrollWrapper(element) {
|
|
||||||
this.scrollWrapper = element;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async loadRewind() {
|
async loadRewind() {
|
||||||
try {
|
try {
|
||||||
this.loadingRewind = true;
|
|
||||||
this.rewind = await ajax("/rewinds");
|
this.rewind = await ajax("/rewinds");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
popupAjaxError(e);
|
popupAjaxError(e);
|
||||||
} finally {
|
|
||||||
this.loadingRewind = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
toggleFullScreen() {
|
|
||||||
this.fullScreen = !this.fullScreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
handleEscape(event) {
|
|
||||||
if (this.fullScreen && event.key === "Escape") {
|
|
||||||
this.fullScreen = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
handleScroll({ target }) {
|
|
||||||
let children = this.rewindContainer.getElementsByClassName("parallax-bg");
|
|
||||||
|
|
||||||
for (let i = 0; i < children.length; i++) {
|
|
||||||
children[i].style.transform = `translateY(-${
|
|
||||||
(target.scrollTop * (i + 1)) / 5
|
|
||||||
}px)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
registerRewindContainer(element) {
|
|
||||||
this.rewindContainer = element;
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="rewind" {{didInsert this.loadRewind}}>
|
||||||
class={{concatClass
|
{{#each this.rewind as |report|}}
|
||||||
"rewind-container"
|
<p>{{report.identifier}}</p>
|
||||||
(if this.fullScreen "-fullscreen")
|
{{/each}}
|
||||||
}}
|
|
||||||
{{didInsert this.loadRewind}}
|
|
||||||
{{on "keydown" this.handleEscape}}
|
|
||||||
{{didInsert this.registerRewindContainer}}
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<div class="rewind">
|
|
||||||
<RewindHeader />
|
|
||||||
<div class="background-1 parallax-bg"></div>
|
|
||||||
{{#if this.loadingRewind}}
|
|
||||||
<div class="rewind-loader">
|
|
||||||
<div class="spinner small"></div>
|
|
||||||
<div class="rewind-loader__text">Crunching your data...</div>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<DButton
|
|
||||||
class="rewind__exit-fullscreen-btn"
|
|
||||||
@icon={{if this.fullScreen "discourse-compress" "discourse-expand"}}
|
|
||||||
@action={{this.toggleFullScreen}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="rewind__scroll-wrapper"
|
|
||||||
{{didInsert this.registerScrollWrapper}}
|
|
||||||
{{on "scroll" this.handleScroll}}
|
|
||||||
>
|
|
||||||
|
|
||||||
{{#each this.rewind as |report|}}
|
|
||||||
<div class={{concatClass "rewind-report" report.identifier}}>
|
|
||||||
{{#if (eq report.identifier "fbff")}}
|
|
||||||
<FBFF @report={{report}} />
|
|
||||||
{{else if (eq report.identifier "reactions")}}
|
|
||||||
<Reactions @report={{report}} />
|
|
||||||
{{else if (eq report.identifier "top-words")}}
|
|
||||||
<TopWords @report={{report}} />
|
|
||||||
{{else if (eq report.identifier "best-posts")}}
|
|
||||||
<BestPosts @report={{report}} />
|
|
||||||
{{else if (eq report.identifier "best-topics")}}
|
|
||||||
<BestTopics @report={{report}} />
|
|
||||||
{{else if (eq report.identifier "activity-calendar")}}
|
|
||||||
<ActivityCalendar @report={{report}} />
|
|
||||||
{{else if (eq report.identifier "favorite-tags")}}
|
|
||||||
<FavoriteTags @report={{report}} />
|
|
||||||
{{else if (eq report.identifier "reading-time")}}
|
|
||||||
<ReadingTime @report={{report}} />
|
|
||||||
{{else if (eq report.identifier "favorite-categories")}}
|
|
||||||
<FavoriteCategories @report={{report}} />
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if this.showPrev}}
|
|
||||||
<DButton
|
|
||||||
class="rewind__prev-btn"
|
|
||||||
@icon="chevron-left"
|
|
||||||
@action={{this.prev}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.showNext}}
|
|
||||||
<DButton
|
|
||||||
class="rewind__next-btn"
|
|
||||||
@icon="chevron-right"
|
|
||||||
@action={{this.next}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import Component from "@glimmer/component";
|
|
||||||
import { TrackedObject } from "@ember-compat/tracked-built-ins";
|
|
||||||
import bodyClass from "discourse/helpers/body-class";
|
|
||||||
import KeyValueStore from "discourse/lib/key-value-store";
|
|
||||||
import isRewindActive from "discourse/plugins/discourse-rewind/discourse/lib/is-rewind-active";
|
|
||||||
|
|
||||||
export default class AvatarDecorator extends Component {
|
|
||||||
store = new TrackedObject(
|
|
||||||
new KeyValueStore("discourse_rewind_" + this.fetchYear)
|
|
||||||
);
|
|
||||||
|
|
||||||
get fetchYear() {
|
|
||||||
const currentDate = new Date();
|
|
||||||
const currentMonth = currentDate.getMonth();
|
|
||||||
const currentYear = currentDate.getFullYear();
|
|
||||||
|
|
||||||
if (currentMonth === 0) {
|
|
||||||
return currentYear - 1;
|
|
||||||
} else {
|
|
||||||
return currentYear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get dismissed() {
|
|
||||||
return this.store.getObject("_dismissed") ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
get showDecorator() {
|
|
||||||
return isRewindActive() && !this.dismissed;
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
{{#if this.showDecorator}}
|
|
||||||
{{bodyClass "rewind-notification-active"}}
|
|
||||||
{{/if}}
|
|
||||||
</template>
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import Component from "@glimmer/component";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import { service } from "@ember/service";
|
|
||||||
import DButton from "discourse/components/d-button";
|
|
||||||
import KeyValueStore from "discourse/lib/key-value-store";
|
|
||||||
import icon from "discourse-common/helpers/d-icon";
|
|
||||||
import isRewindActive from "discourse/plugins/discourse-rewind/discourse/lib/is-rewind-active";
|
|
||||||
|
|
||||||
export default class RewindTab extends Component {
|
|
||||||
@service router;
|
|
||||||
|
|
||||||
store = new KeyValueStore("discourse_rewind_" + this.fetchYear);
|
|
||||||
|
|
||||||
get showCallout() {
|
|
||||||
return isRewindActive() && !this.dismissed;
|
|
||||||
}
|
|
||||||
|
|
||||||
get fetchYear() {
|
|
||||||
const currentDate = new Date();
|
|
||||||
const currentMonth = currentDate.getMonth();
|
|
||||||
const currentYear = currentDate.getFullYear();
|
|
||||||
|
|
||||||
if (currentMonth === 0) {
|
|
||||||
return currentYear - 1;
|
|
||||||
} else {
|
|
||||||
return currentYear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get dismissed() {
|
|
||||||
return this.store.getObject("_dismissed") ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
openRewind() {
|
|
||||||
this.store.setObject({ key: "_dismissed", value: true });
|
|
||||||
this.router.transitionTo("/my/activity/rewind");
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
{{#if this.showCallout}}
|
|
||||||
<div class="rewind-callout__container">
|
|
||||||
<DButton
|
|
||||||
@action={{this.openRewind}}
|
|
||||||
class="rewind-callout btn-transparent"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="rewind-logo -light"
|
|
||||||
src="/plugins/discourse-rewind/images/discourse-rewind-logo.png"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
class="rewind-logo -dark"
|
|
||||||
src="/plugins/discourse-rewind/images/discourse-rewind-logo-dark.png"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{{icon "arrow-right" class="rewind-callaout__arrow"}}
|
|
||||||
</DButton>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</template>
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,17 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
import DNavigationItem from "discourse/components/d-navigation-item";
|
import DNavigationItem from "discourse/components/d-navigation-item";
|
||||||
import icon from "discourse-common/helpers/d-icon";
|
import icon from "discourse-common/helpers/d-icon";
|
||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
import isRewindActive from "discourse/plugins/discourse-rewind/discourse/lib/is-rewind-active";
|
|
||||||
|
|
||||||
const RewindTab = <template>
|
export default class RewindTab extends Component {
|
||||||
{{#if isRewindActive}}
|
<template>
|
||||||
<DNavigationItem
|
<DNavigationItem
|
||||||
@route="userActivity.rewind"
|
@route="userActivity.rewind"
|
||||||
@ariaCurrentContext="subNav"
|
@ariaCurrentContext="subNav"
|
||||||
class="user-nav__activity-rewind"
|
class="user-nav__activity-rewind"
|
||||||
>
|
>
|
||||||
{{icon "repeat"}}
|
{{icon "repeat"}}
|
||||||
<span>{{i18n "discourse_rewind.title"}}</span>
|
<span>{{i18n "rewind.title"}}</span>
|
||||||
</DNavigationItem>
|
</DNavigationItem>
|
||||||
{{/if}}
|
</template>
|
||||||
</template>;
|
}
|
||||||
|
|
||||||
export default RewindTab;
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
export default function isRewindActive() {
|
|
||||||
const currentDate = new Date();
|
|
||||||
const currentMonth = currentDate.getMonth();
|
|
||||||
return currentMonth === 0 || currentMonth === 11;
|
|
||||||
}
|
|
||||||
@@ -1 +1 @@
|
|||||||
<Rewind />
|
<Rewind />
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user