mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 22:43:33 +08:00
FEATURE: show read time in last 60 days
This commit is contained in:
@ -6,6 +6,7 @@ import { default as computed, observes } from 'ember-addons/ember-computed-decor
|
|||||||
import DiscourseURL from 'discourse/lib/url';
|
import DiscourseURL from 'discourse/lib/url';
|
||||||
import User from 'discourse/models/user';
|
import User from 'discourse/models/user';
|
||||||
import { userPath } from 'discourse/lib/url';
|
import { userPath } from 'discourse/lib/url';
|
||||||
|
import { durationTiny } from 'discourse/lib/formatter';
|
||||||
|
|
||||||
const clickOutsideEventName = "mousedown.outside-user-card";
|
const clickOutsideEventName = "mousedown.outside-user-card";
|
||||||
const clickDataExpand = "click.discourse-user-card";
|
const clickDataExpand = "click.discourse-user-card";
|
||||||
@ -87,6 +88,16 @@ export default Ember.Component.extend(CleansUp, {
|
|||||||
$this.css('background-image', bg);
|
$this.css('background-image', bg);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@computed('user.time_read', 'user.recent_time_read')
|
||||||
|
showRecentTimeRead(timeRead, recentTimeRead) {
|
||||||
|
return timeRead !== recentTimeRead && recentTimeRead !== 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('user.recent_time_read')
|
||||||
|
recentTimeRead(recentTimeReadSeconds) {
|
||||||
|
return durationTiny(recentTimeReadSeconds);
|
||||||
|
},
|
||||||
|
|
||||||
_show(username, $target) {
|
_show(username, $target) {
|
||||||
// No user card for anon
|
// No user card for anon
|
||||||
if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) {
|
if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import computed from 'ember-addons/ember-computed-decorators';
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
import { durationTiny } from 'discourse/lib/formatter';
|
||||||
|
|
||||||
// should be kept in sync with 'UserSummary::MAX_BADGES'
|
// should be kept in sync with 'UserSummary::MAX_BADGES'
|
||||||
const MAX_BADGES = 6;
|
const MAX_BADGES = 6;
|
||||||
@ -9,4 +10,19 @@ export default Ember.Controller.extend({
|
|||||||
|
|
||||||
@computed("model.badges.length")
|
@computed("model.badges.length")
|
||||||
moreBadges(badgesLength) { return badgesLength >= MAX_BADGES; },
|
moreBadges(badgesLength) { return badgesLength >= MAX_BADGES; },
|
||||||
|
|
||||||
|
@computed('model.time_read')
|
||||||
|
timeRead(timeReadSeconds) {
|
||||||
|
return durationTiny(timeReadSeconds);
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('model.time_read', 'model.recent_time_read')
|
||||||
|
showRecentTimeRead(timeRead, recentTimeRead) {
|
||||||
|
return timeRead !== recentTimeRead && recentTimeRead !== 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('model.recent_time_read')
|
||||||
|
recentTimeRead(recentTimeReadSeconds) {
|
||||||
|
return recentTimeReadSeconds > 0 ? durationTiny(recentTimeReadSeconds) : null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { autoUpdatingRelativeAge } from 'discourse/lib/formatter';
|
import { autoUpdatingRelativeAge, durationTiny } from 'discourse/lib/formatter';
|
||||||
import { registerUnbound } from 'discourse-common/lib/helpers';
|
import { registerUnbound } from 'discourse-common/lib/helpers';
|
||||||
|
|
||||||
registerUnbound('format-age', function(dt) {
|
registerUnbound('format-age', function(dt) {
|
||||||
dt = new Date(dt);
|
dt = new Date(dt);
|
||||||
return new Handlebars.SafeString(autoUpdatingRelativeAge(dt));
|
return new Handlebars.SafeString(autoUpdatingRelativeAge(dt));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerUnbound('format-duration', function(seconds) {
|
||||||
|
return new Handlebars.SafeString(durationTiny(seconds));
|
||||||
|
});
|
||||||
|
@ -129,6 +129,56 @@ function wrapAgo(dateStr) {
|
|||||||
return I18n.t("dates.wrap_ago", { date: dateStr });
|
return I18n.t("dates.wrap_ago", { date: dateStr });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function durationTiny(distance, ageOpts) {
|
||||||
|
const dividedDistance = Math.round(distance / 60.0);
|
||||||
|
const distanceInMinutes = (dividedDistance < 1) ? 1 : dividedDistance;
|
||||||
|
|
||||||
|
const t = function(key, opts) {
|
||||||
|
const result = I18n.t("dates.tiny." + key, opts);
|
||||||
|
return (ageOpts && ageOpts.addAgo) ? wrapAgo(result) : result;
|
||||||
|
};
|
||||||
|
|
||||||
|
let formatted;
|
||||||
|
|
||||||
|
switch(true) {
|
||||||
|
case(distance <= 59):
|
||||||
|
formatted = t("less_than_x_minutes", {count: 1});
|
||||||
|
break;
|
||||||
|
case(distanceInMinutes >= 0 && distanceInMinutes <= 44):
|
||||||
|
formatted = t("x_minutes", {count: distanceInMinutes});
|
||||||
|
break;
|
||||||
|
case(distanceInMinutes >= 45 && distanceInMinutes <= 89):
|
||||||
|
formatted = t("about_x_hours", {count: 1});
|
||||||
|
break;
|
||||||
|
case(distanceInMinutes >= 90 && distanceInMinutes <= 1409):
|
||||||
|
formatted = t("about_x_hours", {count: Math.round(distanceInMinutes / 60.0)});
|
||||||
|
break;
|
||||||
|
case(distanceInMinutes >= 1410 && distanceInMinutes <= 2519):
|
||||||
|
formatted = t("x_days", {count: 1});
|
||||||
|
break;
|
||||||
|
case(distanceInMinutes >= 2520 && distanceInMinutes <= 129599):
|
||||||
|
formatted = t("x_days", {count: Math.round(distanceInMinutes / 1440.0)});
|
||||||
|
break;
|
||||||
|
case(distanceInMinutes >= 129600 && distanceInMinutes <= 525599):
|
||||||
|
formatted = t("x_months", {count: Math.round(distanceInMinutes / 43200.0)});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
const numYears = distanceInMinutes / 525600.0;
|
||||||
|
const remainder = numYears % 1;
|
||||||
|
if (remainder < 0.25) {
|
||||||
|
formatted = t("about_x_years", {count: parseInt(numYears)});
|
||||||
|
} else if (remainder < 0.75) {
|
||||||
|
formatted = t("over_x_years", {count: parseInt(numYears)});
|
||||||
|
} else {
|
||||||
|
formatted = t("almost_x_years", {count: parseInt(numYears) + 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
function relativeAgeTiny(date, ageOpts) {
|
function relativeAgeTiny(date, ageOpts) {
|
||||||
const format = "tiny";
|
const format = "tiny";
|
||||||
const distance = Math.round((new Date() - date) / 1000);
|
const distance = Math.round((new Date() - date) / 1000);
|
||||||
|
@ -118,7 +118,13 @@
|
|||||||
<h3><span class='desc'>{{i18n 'last_post'}}</span> {{format-date user.last_posted_at leaveAgo="true"}}</h3>
|
<h3><span class='desc'>{{i18n 'last_post'}}</span> {{format-date user.last_posted_at leaveAgo="true"}}</h3>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<h3><span class='desc'>{{i18n 'joined'}}</span> {{format-date user.created_at leaveAgo="true"}}</h3>
|
<h3><span class='desc'>{{i18n 'joined'}}</span> {{format-date user.created_at leaveAgo="true"}}</h3>
|
||||||
<h3><span class='desc'>{{i18n 'time_read'}}</span> {{user.time_read}}</h3>
|
<h3>
|
||||||
|
<span class='desc'>{{i18n 'time_read'}}</span>
|
||||||
|
{{format-duration user.time_read}}
|
||||||
|
{{#if showRecentTimeRead}}
|
||||||
|
<span title="{{i18n 'time_read_recently_tooltip' time_read=recentTimeRead}}">({{i18n 'time_read_recently' time_read=recentTimeRead}})</span>
|
||||||
|
{{/if}}
|
||||||
|
</h3>
|
||||||
{{plugin-outlet name="user-card-metadata" args=(hash user=user)}}
|
{{plugin-outlet name="user-card-metadata" args=(hash user=user)}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -6,8 +6,13 @@
|
|||||||
{{user-stat value=model.days_visited label="user.summary.days_visited"}}
|
{{user-stat value=model.days_visited label="user.summary.days_visited"}}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{{user-stat value=model.time_read label="user.summary.time_read" type="string"}}
|
{{user-stat value=timeRead label="user.summary.time_read" type="string"}}
|
||||||
</li>
|
</li>
|
||||||
|
{{#if showRecentTimeRead}}
|
||||||
|
<li>
|
||||||
|
{{user-stat value=recentTimeRead label="user.summary.recent_time_read" type="string"}}
|
||||||
|
</li>
|
||||||
|
{{/if}}
|
||||||
<li>
|
<li>
|
||||||
{{user-stat value=model.posts_read_count label="user.summary.posts_read"}}
|
{{user-stat value=model.posts_read_count label="user.summary.posts_read"}}
|
||||||
</li>
|
</li>
|
||||||
|
21
app/jobs/onceoff/retro_recent_time_read.rb
Normal file
21
app/jobs/onceoff/retro_recent_time_read.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
module Jobs
|
||||||
|
class RetroRecentTimeRead < Jobs::Onceoff
|
||||||
|
def execute_onceoff(args)
|
||||||
|
# update past records by evenly distributing total time reading among each post read
|
||||||
|
sql = <<~SQL
|
||||||
|
UPDATE user_visits uv1
|
||||||
|
SET time_read = (
|
||||||
|
SELECT (
|
||||||
|
uv1.posts_read
|
||||||
|
/ (SELECT CAST(sum(uv2.posts_read) AS FLOAT) FROM user_visits uv2 where uv2.user_id = uv1.user_id)
|
||||||
|
* COALESCE((SELECT us.time_read FROM user_stats us WHERE us.user_id = uv1.user_id), 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
WHERE EXISTS (SELECT 1 FROM user_stats stats WHERE stats.user_id = uv1.user_id AND stats.posts_read_count > 0 LIMIT 1)
|
||||||
|
AND EXISTS (SELECT 1 FROM user_visits visits WHERE visits.user_id = uv1.user_id AND visits.posts_read > 0 LIMIT 1)
|
||||||
|
SQL
|
||||||
|
|
||||||
|
UserVisit.exec_sql(sql)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -965,6 +965,12 @@ class User < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def recent_time_read
|
||||||
|
self.created_at && self.created_at < 60.days.ago ?
|
||||||
|
self.user_visits.where('visited_at >= ?', 60.days.ago).sum(:time_read) :
|
||||||
|
self.user_stat&.time_read
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def badge_grant
|
def badge_grant
|
||||||
|
@ -72,7 +72,9 @@ class UserStat < ActiveRecord::Base
|
|||||||
if last_seen = last_seen_cached
|
if last_seen = last_seen_cached
|
||||||
diff = (Time.now.to_f - last_seen.to_f).round
|
diff = (Time.now.to_f - last_seen.to_f).round
|
||||||
if diff > 0 && diff < MAX_TIME_READ_DIFF
|
if diff > 0 && diff < MAX_TIME_READ_DIFF
|
||||||
UserStat.where(user_id: id, time_read: time_read).update_all ["time_read = time_read + ?", diff]
|
update_args = ["time_read = time_read + ?", diff]
|
||||||
|
UserStat.where(user_id: id, time_read: time_read).update_all(update_args)
|
||||||
|
UserVisit.where(user_id: id, visited_at: Time.zone.now.to_date).update_all(update_args)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
cache_last_seen(Time.now.to_f)
|
cache_last_seen(Time.now.to_f)
|
||||||
|
@ -151,6 +151,10 @@ class UserSummary
|
|||||||
.count
|
.count
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def recent_time_read
|
||||||
|
@user.recent_time_read
|
||||||
|
end
|
||||||
|
|
||||||
delegate :likes_given,
|
delegate :likes_given,
|
||||||
:likes_received,
|
:likes_received,
|
||||||
:days_visited,
|
:days_visited,
|
||||||
|
@ -67,6 +67,7 @@ class UserSerializer < BasicUserSerializer
|
|||||||
:pending_count,
|
:pending_count,
|
||||||
:profile_view_count,
|
:profile_view_count,
|
||||||
:time_read,
|
:time_read,
|
||||||
|
:recent_time_read,
|
||||||
:primary_group_name,
|
:primary_group_name,
|
||||||
:primary_group_flair_url,
|
:primary_group_flair_url,
|
||||||
:primary_group_flair_bg_color,
|
:primary_group_flair_bg_color,
|
||||||
@ -403,7 +404,11 @@ class UserSerializer < BasicUserSerializer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def time_read
|
def time_read
|
||||||
AgeWords.age_words(object.user_stat&.time_read)
|
object.user_stat&.time_read
|
||||||
|
end
|
||||||
|
|
||||||
|
def recent_time_read
|
||||||
|
time = object.recent_time_read
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -37,6 +37,7 @@ class UserSummarySerializer < ApplicationSerializer
|
|||||||
:topic_count,
|
:topic_count,
|
||||||
:post_count,
|
:post_count,
|
||||||
:time_read,
|
:time_read,
|
||||||
|
:recent_time_read,
|
||||||
:bookmark_count
|
:bookmark_count
|
||||||
|
|
||||||
def include_badges?
|
def include_badges?
|
||||||
@ -48,6 +49,10 @@ class UserSummarySerializer < ApplicationSerializer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def time_read
|
def time_read
|
||||||
AgeWords.age_words(object.time_read)
|
object.time_read
|
||||||
|
end
|
||||||
|
|
||||||
|
def recent_time_read
|
||||||
|
object.recent_time_read
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -74,6 +74,9 @@ en:
|
|||||||
x_seconds:
|
x_seconds:
|
||||||
one: "1s"
|
one: "1s"
|
||||||
other: "%{count}s"
|
other: "%{count}s"
|
||||||
|
less_than_x_minutes:
|
||||||
|
one: "< 1m"
|
||||||
|
other: "< %{count}m"
|
||||||
x_minutes:
|
x_minutes:
|
||||||
one: "1m"
|
one: "1m"
|
||||||
other: "%{count}m"
|
other: "%{count}m"
|
||||||
@ -83,6 +86,9 @@ en:
|
|||||||
x_days:
|
x_days:
|
||||||
one: "1d"
|
one: "1d"
|
||||||
other: "%{count}d"
|
other: "%{count}d"
|
||||||
|
x_months:
|
||||||
|
one: "1mon"
|
||||||
|
other: "%{count}mon"
|
||||||
about_x_years:
|
about_x_years:
|
||||||
one: "1y"
|
one: "1y"
|
||||||
other: "%{count}y"
|
other: "%{count}y"
|
||||||
@ -892,6 +898,7 @@ en:
|
|||||||
title: "Summary"
|
title: "Summary"
|
||||||
stats: "Stats"
|
stats: "Stats"
|
||||||
time_read: "read time"
|
time_read: "read time"
|
||||||
|
recent_time_read: "recent read time"
|
||||||
topic_count:
|
topic_count:
|
||||||
one: "topic created"
|
one: "topic created"
|
||||||
other: "topics created"
|
other: "topics created"
|
||||||
@ -1005,6 +1012,8 @@ en:
|
|||||||
unmute: Unmute
|
unmute: Unmute
|
||||||
last_post: Posted
|
last_post: Posted
|
||||||
time_read: Read
|
time_read: Read
|
||||||
|
time_read_recently: '%{time_read} recently'
|
||||||
|
time_read_recently_tooltip: '%{time_read} read time in the last 60 days'
|
||||||
last_reply_lowercase: last reply
|
last_reply_lowercase: last reply
|
||||||
replies_lowercase:
|
replies_lowercase:
|
||||||
one: reply
|
one: reply
|
||||||
|
11
db/migrate/20171113214725_add_time_read_to_user_visits.rb
Normal file
11
db/migrate/20171113214725_add_time_read_to_user_visits.rb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
class AddTimeReadToUserVisits < ActiveRecord::Migration[5.1]
|
||||||
|
def up
|
||||||
|
add_column :user_visits, :time_read, :integer, null: false, default: 0 # in seconds
|
||||||
|
add_index :user_visits, [:user_id, :visited_at, :time_read]
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_index :user_visits, [:user_id, :visited_at, :time_read]
|
||||||
|
remove_column :user_visits, :time_read
|
||||||
|
end
|
||||||
|
end
|
@ -1,6 +1,6 @@
|
|||||||
var clock;
|
var clock;
|
||||||
|
|
||||||
import { relativeAge, autoUpdatingRelativeAge, updateRelativeAge, breakUp, number, longDate } from 'discourse/lib/formatter';
|
import { relativeAge, autoUpdatingRelativeAge, updateRelativeAge, breakUp, number, longDate, durationTiny } from 'discourse/lib/formatter';
|
||||||
|
|
||||||
QUnit.module("lib:formatter", {
|
QUnit.module("lib:formatter", {
|
||||||
beforeEach() {
|
beforeEach() {
|
||||||
@ -212,3 +212,25 @@ QUnit.test("number", assert => {
|
|||||||
assert.equal(number(3333), "3.3k", "it abbreviates thousands");
|
assert.equal(number(3333), "3.3k", "it abbreviates thousands");
|
||||||
assert.equal(number(2499999), "2.5M", "it abbreviates millions");
|
assert.equal(number(2499999), "2.5M", "it abbreviates millions");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test("durationTiny", assert => {
|
||||||
|
assert.equal(durationTiny(0), '< 1m', "0 seconds shows as < 1m");
|
||||||
|
assert.equal(durationTiny(59), '< 1m', "59 seconds shows as < 1m");
|
||||||
|
assert.equal(durationTiny(60), '1m', "60 seconds shows as 1m");
|
||||||
|
assert.equal(durationTiny(90), '2m', "90 seconds shows as 2m");
|
||||||
|
assert.equal(durationTiny(120), '2m', "120 seconds shows as 2m");
|
||||||
|
assert.equal(durationTiny(60 * 45), '1h', "45 minutes shows as 1h");
|
||||||
|
assert.equal(durationTiny(60 * 60), '1h', "60 minutes shows as 1h");
|
||||||
|
assert.equal(durationTiny(60 * 90), '2h', "90 minutes shows as 2h");
|
||||||
|
assert.equal(durationTiny(3600 * 23), '23h', "23 hours shows as 23h");
|
||||||
|
assert.equal(durationTiny(3600 * 24 - 29), '1d', "23 hours 31 mins shows as 1d");
|
||||||
|
assert.equal(durationTiny(3600 * 24 * 89), '89d', "89 days shows as 89d");
|
||||||
|
assert.equal(durationTiny(60 * (525600 - 1)), '12mon', "364 days shows as 12mon");
|
||||||
|
assert.equal(durationTiny(60 * 525600), '1y', "365 days shows as 1y");
|
||||||
|
assert.equal(durationTiny(86400 * 456), '1y', "456 days shows as 1y");
|
||||||
|
assert.equal(durationTiny(86400 * 457), '> 1y', "457 days shows as > 1y");
|
||||||
|
assert.equal(durationTiny(86400 * 638), '> 1y', "638 days shows as > 1y");
|
||||||
|
assert.equal(durationTiny(86400 * 639), '2y', "639 days shows as 2y");
|
||||||
|
assert.equal(durationTiny(86400 * 821), '2y', "821 days shows as 2y");
|
||||||
|
assert.equal(durationTiny(86400 * 822), '> 2y', "822 days shows as > 2y");
|
||||||
|
});
|
||||||
|
Reference in New Issue
Block a user