Compare commits
5
Commits
cache
...
falco-reports
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d165376a1 | ||
|
|
b56070313e | ||
|
|
89d78e5401 | ||
|
|
0a8aa07078 | ||
|
|
8593c27e7c |
@@ -4,9 +4,4 @@ Display stats on your last year of Discourse usage.
|
||||
|
||||
## Usage
|
||||
|
||||
- add `http://127.0.0.1:4200/rewinds?` in `SiteSetting.allowed_iframes`
|
||||
- example embed of a rewind in a post:
|
||||
|
||||
```
|
||||
<iframe src="http://127.0.0.1:4200/rewinds?year=2024&username=j.jaffeux" width="320" height="500" frameborder="0" style="border:0;" allowfullscreen="" aria-hidden="false" tabindex="0" />
|
||||
```
|
||||
Navigate to `/my/activity/rewind`
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ::DiscourseRewind
|
||||
class RewindsAssetsController < ::ApplicationController
|
||||
requires_plugin PLUGIN_NAME
|
||||
|
||||
skip_before_action :preload_json, :check_xhr, only: %i[show]
|
||||
skip_before_action :verify_authenticity_token, only: %i[show]
|
||||
|
||||
def show
|
||||
no_cookies
|
||||
|
||||
name = params[:name]
|
||||
path, content_type =
|
||||
if name == "rewind"
|
||||
%w[rewind.css text/css]
|
||||
else
|
||||
raise Discourse::NotFound
|
||||
end
|
||||
|
||||
content = File.read(DiscourseRewind.public_asset_path("css/#{path}"))
|
||||
|
||||
# note, path contains a ":version" which automatically busts the cache
|
||||
# based on file content, so this is safe
|
||||
response.headers["Last-Modified"] = 10.years.ago.httpdate
|
||||
response.headers["Content-Length"] = content.bytesize.to_s
|
||||
immutable_for 1.year
|
||||
|
||||
render plain: content, disposition: :nil, content_type: content_type
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,16 +4,11 @@ module ::DiscourseRewind
|
||||
class RewindsController < ::ApplicationController
|
||||
requires_plugin PLUGIN_NAME
|
||||
|
||||
skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required, only: %i[show]
|
||||
|
||||
def show
|
||||
# expires_in 1.minute, public: false
|
||||
response.headers["X-Robots-Tag"] = "noindex"
|
||||
|
||||
DiscourseRewind::Rewind::Fetch.call(service_params) do
|
||||
on_success do |reports:|
|
||||
@reports = reports
|
||||
render "show", layout: false
|
||||
render json: MultiJson.dump(reports), status: 200
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseRewind
|
||||
module RewindsHelper
|
||||
# keeping it here for caching
|
||||
def self.rewind_asset_url(asset_name)
|
||||
if !%w[rewind.css].include?(asset_name)
|
||||
raise StandardError, "unknown asset type #{asset_name}"
|
||||
end
|
||||
|
||||
@urls ||= {}
|
||||
url = @urls[asset_name]
|
||||
return url if url
|
||||
|
||||
content = File.read(DiscourseRewind.public_asset_path("css/#{asset_name}"))
|
||||
sha1 = Digest::SHA1.hexdigest(content)
|
||||
url = "/rewinds/assets/#{sha1}/#{asset_name}"
|
||||
@urls[asset_name] = GlobalPath.cdn_path(url)
|
||||
end
|
||||
|
||||
def rewind_asset_url(asset_name)
|
||||
DiscourseRewind::RewindsHelper.rewind_asset_url(asset_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,23 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseRewind
|
||||
class Rewind::Action::CreatePostsCountReport < Service::ActionBase
|
||||
option :params
|
||||
option :guardian
|
||||
option :date
|
||||
option :user
|
||||
|
||||
delegate :username, :year, to: :params, private: true
|
||||
|
||||
def call
|
||||
p date
|
||||
p user
|
||||
p guardian
|
||||
p username
|
||||
p year
|
||||
p params
|
||||
|
||||
{ data: { count: 5 }, identifier: "posts-count-report" }
|
||||
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
|
||||
@@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# For showcasing the reading time of a user
|
||||
# Should we show book covers or just the names?
|
||||
module DiscourseRewind
|
||||
class Rewind::Action::ReadingTime < Service::ActionBase
|
||||
option :user
|
||||
option :date
|
||||
|
||||
def call
|
||||
reading_time = UserVisit.where(user_id: user.id).where(visited_at: date).sum(:time_read)
|
||||
|
||||
{
|
||||
data: {
|
||||
reading_time: reading_time,
|
||||
books: best_book_fit(reading_time),
|
||||
},
|
||||
identifier: "reading-time",
|
||||
}
|
||||
end
|
||||
|
||||
def popular_book_reading_time
|
||||
{
|
||||
"The Hunger Games" => 19_740,
|
||||
"The Metamorphosis" => 3120,
|
||||
"To Kill a Mockingbird" => 22_800,
|
||||
"Pride and Prejudice" => 25_200,
|
||||
"1984" => 16_800,
|
||||
"The Lord of the Rings" => 108_000,
|
||||
"Harry Potter and the Sorcerer's Stone" => 24_600,
|
||||
"The Great Gatsby" => 12_600,
|
||||
"The Little Prince" => 5400,
|
||||
"Animal Farm" => 7200,
|
||||
"The Catcher in the Rye" => 18_000,
|
||||
"Jane Eyre" => 34_200,
|
||||
"Fahrenheit 451" => 15_000,
|
||||
"The Hobbit" => 27_000,
|
||||
"The Da Vinci Code" => 37_800,
|
||||
"Little Women" => 30_000,
|
||||
"One Hundred Years of Solitude" => 46_800,
|
||||
"And Then There Were None" => 16_200,
|
||||
"The Alchemist" => 10_800,
|
||||
"The Hitchhiker's Guide to the Galaxy" => 12_600,
|
||||
}.symbolize_keys
|
||||
end
|
||||
|
||||
def best_book_fit(reading_time)
|
||||
reading_time_rest = reading_time
|
||||
books = []
|
||||
while reading_time_rest > 0
|
||||
books << popular_book_reading_time.min_by { |_, v| (v - reading_time_rest).abs }.first
|
||||
reading_time_rest -= popular_book_reading_time[books.last]
|
||||
end
|
||||
books.group_by(&:itself).transform_values(&:count)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,11 +5,7 @@ module DiscourseRewind
|
||||
#
|
||||
# @example
|
||||
# ::DiscourseRewind::Rewind::Fetch.call(
|
||||
# guardian: guardian,
|
||||
# params: {
|
||||
# username: "falco",
|
||||
# year: 2024,
|
||||
# }
|
||||
# guardian: guardian
|
||||
# )
|
||||
#
|
||||
class Rewind::Fetch
|
||||
@@ -23,43 +19,45 @@ module DiscourseRewind
|
||||
# @return [Service::Base::Context]
|
||||
|
||||
# order matters, rewinds are displayed in the order they are defined
|
||||
REPORTS = [DiscourseRewind::Rewind::Action::CreatePostsCountReport]
|
||||
REPORTS = [
|
||||
DiscourseRewind::Rewind::Action::ReadingTime,
|
||||
DiscourseRewind::Rewind::Action::PostingCalendar,
|
||||
].freeze
|
||||
|
||||
CACHE_DURATION = 5.minutes
|
||||
|
||||
params do
|
||||
attribute :year, :integer
|
||||
attribute :username, :string
|
||||
|
||||
validates :year, presence: true
|
||||
validates :username, presence: true
|
||||
end
|
||||
|
||||
model :user
|
||||
model :date
|
||||
model :reports
|
||||
|
||||
private
|
||||
|
||||
def fetch_user(params:)
|
||||
User.find_by_username(params.username)
|
||||
def fetch_user(guardian:)
|
||||
User.find_by_username(guardian.user.username)
|
||||
end
|
||||
|
||||
def fetch_date(params:)
|
||||
Date.new(params.year).all_year
|
||||
Date.new(2024).all_year
|
||||
end
|
||||
|
||||
def fetch_reports(params:, date:, user:, guardian:)
|
||||
key = "rewind:#{params.username}:#{params.year}"
|
||||
def fetch_reports(date:, user:, guardian:)
|
||||
key = "rewind:#{guardian.user.username}:#{2024}"
|
||||
reports = Discourse.redis.get(key)
|
||||
|
||||
if Rails.env.development? || !reports
|
||||
reports = REPORTS.map { |report| report.call(params:, date:, user:, guardian:) }
|
||||
reports = REPORTS.map { |report| report.call(date:, user:, guardian:) }
|
||||
Discourse.redis.setex(key, CACHE_DURATION, MultiJson.dump(reports))
|
||||
else
|
||||
reports = MultiJson.load(reports, symbolize_keys: true)
|
||||
end
|
||||
|
||||
if Rails.env.development?
|
||||
reports.each do |report|
|
||||
puts "\n--- #{report[:identifier]} ---\n"
|
||||
pp report
|
||||
end
|
||||
end
|
||||
|
||||
reports
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= I18n.t("discourse_rewind.title", title: "RETRO TITLE", site_name: SiteSetting.title) %></title>
|
||||
<meta property="og:title" content="<%= I18n.t("discourse_rewind.title", title: "RETRO TITLE", site_name: SiteSetting.title) %>">
|
||||
<meta property="og:description" content="DESC">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="<%= request.original_url %>">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="<%= I18n.t("discourse_rewind.title", title: "RETRO TITLE", site_name: SiteSetting.title) %>">
|
||||
<meta name="twitter:description" content="DESC">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<link rel="stylesheet" href="<%= rewind_asset_url("rewind.css") %>">
|
||||
</head>
|
||||
<body>
|
||||
<div class="rewind__wrapper">
|
||||
<div class="rewind">
|
||||
<%- @reports.each do |report| %>
|
||||
<div class="rewind__report">
|
||||
<div class="rewind__report__header">
|
||||
<%= report[:identifier] %>
|
||||
</div>
|
||||
<div class="rewind__report__body">
|
||||
<%= report[:data][:count] %>
|
||||
</div>
|
||||
</div>
|
||||
<%- end %>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,27 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
|
||||
export default class Rewind extends Component {
|
||||
@tracked rewind = [];
|
||||
|
||||
@action
|
||||
async loadRewind() {
|
||||
try {
|
||||
this.rewind = await ajax("/rewinds");
|
||||
} catch (e) {
|
||||
popupAjaxError(e);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="rewind" {{didInsert this.loadRewind}}>
|
||||
{{#each this.rewind as |report|}}
|
||||
<p>{{report.identifier}}</p>
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import Component from "@glimmer/component";
|
||||
import DNavigationItem from "discourse/components/d-navigation-item";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class RewindTab extends Component {
|
||||
<template>
|
||||
<DNavigationItem
|
||||
@route="userActivity.rewind"
|
||||
@ariaCurrentContext="subNav"
|
||||
class="user-nav__activity-rewind"
|
||||
>
|
||||
{{icon "repeat"}}
|
||||
<span>{{i18n "rewind.title"}}</span>
|
||||
</DNavigationItem>
|
||||
</template>
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export default function () {
|
||||
this.route(
|
||||
"user",
|
||||
{ path: "/u/:username", resetNamespace: true },
|
||||
function () {
|
||||
this.route("userActivity", function () {
|
||||
this.route("rewind");
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class UserActivityRewind extends DiscourseRoute {
|
||||
templateName = "user/rewind";
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<Rewind />
|
||||
@@ -0,0 +1 @@
|
||||
@import "rewind";
|
||||
@@ -0,0 +1,6 @@
|
||||
.rewind {
|
||||
width: 280px;
|
||||
height: 400px;
|
||||
background-color: var(--secondary-high);
|
||||
border-radius: var(--d-border-radius);
|
||||
}
|
||||
+1
-4
@@ -1,8 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
DiscourseRewind::Engine.routes.draw do
|
||||
get "/rewinds" => "rewinds#show"
|
||||
get "/rewinds/assets/:version/:name" => "rewinds_assets#show"
|
||||
end
|
||||
DiscourseRewind::Engine.routes.draw { get "/rewinds" => "rewinds#show" }
|
||||
|
||||
Discourse::Application.routes.draw { mount ::DiscourseRewind::Engine, at: "/" }
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
|
||||
enabled_site_setting :discourse_rewind_enabled
|
||||
|
||||
register_svg_icon "repeat"
|
||||
|
||||
register_asset "stylesheets/common/index.scss"
|
||||
register_asset "stylesheets/desktop/index.scss", :desktop
|
||||
register_asset "stylesheets/mobile/index.scss", :mobile
|
||||
|
||||
module ::DiscourseRewind
|
||||
PLUGIN_NAME = "discourse-rewind"
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
/* RESET CSS */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
* { margin: 0; padding: 0; }
|
||||
ul[role='list'], ol[role='list'] { list-style: none; }
|
||||
html:focus-within { scroll-behavior: smooth; }
|
||||
a:not([class]) { text-decoration-skip-ink: auto; }
|
||||
input, button, textarea, select { font: inherit; }
|
||||
body, html { height: 100%; scroll-behavior: smooth; }
|
||||
/* END RESET CSS */
|
||||
|
||||
.rewind {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 5px;
|
||||
}
|
||||
Reference in New Issue
Block a user