Skip to content

Commit

Permalink
Show localized date/time on hover for articles/comments (forem#2722)
Browse files Browse the repository at this point in the history
* Add article decorator published_timestamp

* Use time HTML5 element and refactor date in partial

* Add published_timestamp to Article

Adding `published_timestamp` to the homepage we can then use JS to render the full timestamp localized for the user.

We've also added the timestamp to the index and the API

* Display article published timestamp on hover

* Use time also in the article show page

* Add timestamp to bottom articles as well

* Remove published_timestamp from index because it is not used

* Fix broken specs

* Add more article dates specs

* Refactor date initializers
  • Loading branch information
rhymes authored and benhalpern committed May 9, 2019
1 parent 260fe90 commit 4e591fe
Show file tree
Hide file tree
Showing 25 changed files with 211 additions and 101 deletions.
1 change: 1 addition & 0 deletions app/assets/javascripts/initializePage.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function callInitalizers(){
initializeCommentsPage();
initEditorResize();
initLeaveEditorWarning();
initializeArticleDate();
initializeArticleReactions();
initNotifications();
initializeSplitTestTracking();
Expand Down
10 changes: 10 additions & 0 deletions app/assets/javascripts/initializers/initializeArticleDate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* Show article date/time according to user's locale */
/* global addLocalizedDateTimeToElementsTitles */

function initializeArticleDate() {
var articlesDates = document.querySelectorAll(
'.single-article time, article time, .single-other-article time',
);

addLocalizedDateTimeToElementsTitles(articlesDates, 'datetime');
}
48 changes: 3 additions & 45 deletions app/assets/javascripts/initializers/initializeCommentDate.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,8 @@
/* Show comment date/time according to user's locale */
/* global timestampToLocalDateTime */
/* global addLocalizedDateTimeToElementsTitles */

function initializeCommentDate() {
// example: "Wednesday, April 3, 2019, 2:55:14 PM"
var hoverTimeOptions = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
};
var commentsDates = document.querySelectorAll('.comment-date time');

// example: "Apr 3"
var visibleDateOptions = {
month: 'short',
day: 'numeric',
};

var commentDates = document.getElementsByClassName('comment-date');
for (var i = 0; i < commentDates.length; i += 1) {
// get UTC timestamp set by the server
var ts = commentDates[i].getAttribute('data-published-timestamp');

// add a full datetime to the comment date string, visible on hover
// `navigator.language` is used for full date times to allow the hover date
// to be localized according to the user's locale
var hoverTime = timestampToLocalDateTime(
ts,
navigator.language,
hoverTimeOptions,
);
commentDates[i].setAttribute('title', hoverTime);

// replace the comment short visible date with the equivalent localized one
var visibleDate = commentDates[i].querySelector('a');
if (visibleDate) {
var localVisibleDate = timestampToLocalDateTime(
ts,
'en-US', // en-US because for now we want all users to see `Apr 3`
visibleDateOptions,
);
if (localVisibleDate) {
visibleDate.innerHTML = localVisibleDate;
}
}
}
addLocalizedDateTimeToElementsTitles(commentsDates, 'datetime');
}
6 changes: 5 additions & 1 deletion app/assets/javascripts/utilities/buildArticleHTML.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ function buildArticleHTML(article) {

var publishDate = '';
if (article.readable_publish_date) {
publishDate = '・'+article.readable_publish_date
if (article.published_timestamp) {
publishDate = '・' + '<time datetime="'+article.published_timestamp+'">'+article.readable_publish_date+'</time>';
} else {
publishDate = '・' + '<time>'+article.readable_publish_date+'</time>';
}
}
var readingTimeHTML = '';
if (article.reading_time && article.class_name === "Article") {
Expand Down
8 changes: 6 additions & 2 deletions app/assets/javascripts/utilities/buildCommentHTML.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,12 @@ function buildCommentHTML(comment) {
</a>\
'+twitterIcon+'\
'+githubIcon+'\
<div class="comment-date" data-published-timestamp="' + comment.published_timestamp + '">\
<a href="'+comment.url+'">'+comment.readable_publish_date+'</a>\
<div class="comment-date">\
<a href="'+comment.url+'">\
<time datetime="'+comment.published_timestamp+'">\
'+comment.readable_publish_date+'\
</time>\
</a>\
</div>\
<button class="dropbtn">\
<%= image_tag("three-dots.svg", class: "dropdown-icon", alt: "Toggle dropdown menu") %>\
Expand Down
35 changes: 35 additions & 0 deletions app/assets/javascripts/utilities/localDateTime.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* Local date/time utilities */

/*
Convert string timestamp to local time, using the given locale.
Expand All @@ -20,3 +22,36 @@ function timestampToLocalDateTime(timestamp, locale, options) {
return '';
}
}

function addLocalizedDateTimeToElementsTitles(elements, timestampAttribute) {
// example: "Wednesday, April 3, 2019, 2:55:14 PM"
var timeOptions = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
};

for (var i = 0; i < elements.length; i += 1) {
var element = elements[i];

// get UTC timestamp set by the server
var timestamp = element.getAttribute(timestampAttribute || 'datetime');

if (timestamp) {
// add a full datetime to the element title, visible on hover.
// `navigator.language` is used to allow the date to be localized
// according to the browser's locale
// see <https://developer.mozilla.org/en-US/docs/Web/API/NavigatorLanguage/language>
var localDateTime = timestampToLocalDateTime(
timestamp,
navigator.language,
timeOptions,
);
element.setAttribute('title', localDateTime);
}
}
}
2 changes: 1 addition & 1 deletion app/assets/stylesheets/comments.scss
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,7 @@ a.header-link {
.comment-date {
border: none;
position: absolute;
top: calc(16px - 0.25vw);
top: calc(14px - 0.25vw);
right: calc(35px + 0.2vw);
font-size: 12px;
text-align: right;
Expand Down
Empty file removed app/models/.keep
Empty file.
22 changes: 11 additions & 11 deletions app/models/article.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class Article < ApplicationRecord
}

scope :limited_column_select, lambda {
select(:path, :title, :id,
select(:path, :title, :id, :published,
:comments_count, :positive_reactions_count, :cached_tag_list,
:main_image, :main_image_background_hex_color, :updated_at, :slug,
:video, :user_id, :organization_id, :video_source_url, :video_code,
Expand Down Expand Up @@ -131,10 +131,7 @@ class Article < ApplicationRecord

algoliasearch per_environment: true, auto_remove: false, enqueue: :trigger_delayed_index do
attribute :title
add_index "searchables",
id: :index_id,
per_environment: true,
enqueue: :trigger_delayed_index do
add_index "searchables", id: :index_id, per_environment: true, enqueue: :trigger_delayed_index do
attributes :title, :tag_list, :main_image, :id, :reading_time, :score,
:featured, :published, :published_at, :featured_number,
:comments_count, :reactions_count, :positive_reactions_count,
Expand Down Expand Up @@ -163,10 +160,7 @@ class Article < ApplicationRecord
customRanking ["desc(search_score)", "desc(hotness_score)"]
end

add_index "ordered_articles",
id: :index_id,
per_environment: true,
enqueue: :trigger_delayed_index do
add_index "ordered_articles", id: :index_id, per_environment: true, enqueue: :trigger_delayed_index do
attributes :title, :path, :class_name, :comments_count, :reading_time, :language,
:tag_list, :positive_reactions_count, :id, :hotness_score, :score, :readable_publish_date, :flare_tag, :user_id,
:organization_id, :cloudinary_video_url, :video_duration_in_minutes, :experience_level_rating, :experience_level_rating_distribution
Expand Down Expand Up @@ -385,6 +379,13 @@ def readable_publish_date
end
end

def published_timestamp
return "" unless published
return "" unless crossposted_at || published_at

(crossposted_at || published_at).utc.iso8601
end

def self.seo_boostable(tag = nil, time_ago = 18.days.ago)
time_ago = 5.days.ago if time_ago == "latest" # Time ago sometimes returns this phrase instead of a date
time_ago = 75.days.ago if time_ago.nil? # Time ago sometimes is given as nil and should then be the default. I know, sloppy.
Expand Down Expand Up @@ -539,7 +540,6 @@ def create_password
end

def update_cached_user
cached_org_object = nil
if organization
cached_org_object = {
name: organization.name,
Expand All @@ -550,7 +550,7 @@ def update_cached_user
}
self.cached_organization = OpenStruct.new(cached_org_object)
end
cached_user_object = nil

if user
cached_user_object = {
name: user.name,
Expand Down
3 changes: 2 additions & 1 deletion app/views/api/v0/articles/index.json.jbuilder
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ json.array! @articles do |article|
json.id article.id
json.title article.title
json.description article.description
json.cover_image cloud_cover_url article.main_image
json.cover_image cloud_cover_url(article.main_image)
json.published_at article.published_at
json.tag_list article.cached_tag_list_array
json.slug article.slug
Expand All @@ -12,6 +12,7 @@ json.array! @articles do |article|
json.canonical_url article.processed_canonical_url
json.comments_count article.comments_count
json.positive_reactions_count article.positive_reactions_count
json.published_timestamp article.published_timestamp

json.user do
json.name article.user.name
Expand Down
1 change: 1 addition & 0 deletions app/views/api/v0/articles/show.json.jbuilder
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ json.url @article.url
json.canonical_url @article.processed_canonical_url
json.comments_count @article.comments_count
json.positive_reactions_count @article.positive_reactions_count
json.published_timestamp @article.published_timestamp

json.body_html @article.processed_html
json.ltag_style(@article.liquid_tags_used.map { |ltag| Rails.application.assets["ltags/#{ltag}.css"].to_s.html_safe })
Expand Down
3 changes: 1 addition & 2 deletions app/views/articles/_bottom_content.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
<div class="content">
<h3><%= article.title %></h3>
<h4>
<%= article.user.name %>
<span class="published-at">- <%= article.readable_publish_date if article.published_at %></span>
<%= article.user.name %> - <time datetime="<%= article.published_timestamp %>"><%= article.readable_publish_date %></time>
</h4>
</div>
</div>
Expand Down
6 changes: 4 additions & 2 deletions app/views/articles/_single_story.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@
</div>
</a>
<h4>
<a href="/<%= story.cached_user.username %>"><%= story.cached_user.name %><%= story.readable_publish_date %><span class="time-ago-indicator-initial-placeholder" data-seconds="<%= story.published_at_int %>"></span></a>

<a href="/<%= story.cached_user.username %>">
<%= story.cached_user.name %><time datetime="<%= story.published_timestamp %>"><%= story.readable_publish_date %></time>
<span class="time-ago-indicator-initial-placeholder" data-seconds="<%= story.published_at_int %>"></span>
</a>
</h4>
<div class="tags">
<% story.cached_tag_list_array.each do |tag| %>
Expand Down
18 changes: 15 additions & 3 deletions app/views/articles/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
data-algolia-tag=""
data-feed="<%= params[:timeframe] || "base-feed" %>"
data-articles-since="<%= Timeframer.new(params[:timeframe]).datetime.to_i %>">

<%= render "articles/sidebar" %>

<div class="articles-list" id="articles-list">
<div class="on-page-nav-controls" id="on-page-nav-controls">
<div class="on-page-nav-label">
Expand Down Expand Up @@ -80,10 +82,12 @@
<img src="<%= asset_path "lightning.svg" %>" alt="right-sidebar-nav">
</button>
</div>

<% if @home_page %>
<% @featured_story ||= @stories.where.not(main_image: nil).first&.decorate || Article.new %>
<% @stories = @stories.decorate %>
<% end %>
<% end %>

<% if @featured_story.id %>
<div id="featured-story-marker" data-featured-article="articles-<%= @featured_story.id %>"></div>
<img src="<%= cloud_cover_url(@featured_story.main_image) %>" style="display:none" alt="<%= @featured_story.title %>" />
Expand All @@ -98,8 +102,12 @@
<a href="/<%= @featured_story.cached_user.username %>" class="featured-profile-button">
<img class="featured-profile-pic" src="<%= ProfileImage.new(@featured_story.cached_user).get(90) %>" alt="<%= @featured_story.title %>" />
</a>
<div class="featured-user-name"><a href="/<%= @featured_story.cached_user.username %>"><%= @featured_story.cached_user.name %>
<%= @featured_story.readable_publish_date %><span class="time-ago-indicator-initial-placeholder" data-seconds="<%= @featured_story.published_at_int %>"></span></a></div>
<div class="featured-user-name">
<a href="/<%= @featured_story.cached_user.username %>">
<%= @featured_story.cached_user.name %><time datetime="<%= @featured_story.published_timestamp %>"><%= @featured_story.readable_publish_date %></time>
<span class="time-ago-indicator-initial-placeholder" data-seconds="<%= @featured_story.published_at_int %>"></span>
</a>
</div>
<div class="featured-tags tags">
<% @featured_story.cached_tag_list_array.each do |tag| %>
<a href="/t/<%= tag %>"><span class="tag">#<%= tag %></span></a>
Expand Down Expand Up @@ -136,16 +144,20 @@
</div>
</a>
<% end %>

<div id="article-index-podcast-div"></div>

<div class="substories" id="substories">
<% if @stories.any? %>
<%= render "stories/main_stories_feed" %>
<% end %>
</div>

<div class="loading-articles" id="loading-articles">
loading...
</div>
</div>

<%= render "articles/sidebar_additional" %>
</div>

Expand Down
12 changes: 8 additions & 4 deletions app/views/articles/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -127,22 +127,26 @@
<% if @user.github_username.present? %>
<a href="http://github.com/<%= @user.github_username %>"><%= image_tag_or_inline_svg "github" %></a>
<% end %>
<span class="published-at" itemprop="datePublished"><%= @article.readable_publish_date if @article.published_at %></span>
<% if @article.published_timestamp.present? %>
<time itemprop="datePublished" datetime="<%= @article.published_timestamp %>"><%= @article.readable_publish_date %></time>
<% end %>
<% if @second_user.present? %>
<em>with <b><a href="<%= @second_user.path %>"><%= @second_user.name %></a></b></em>
<% end %>
<% if @third_user.present? %>
<em> and <b><a href="<%= @third_user.path %>"><%= @third_user.name %></a></b></em>
<% end %>
<% if should_show_updated_on?(@article) %>
<span class="published-at updated-at"><em>Updated on <span itemprop="dateModified"><%= @article.edited_at&.strftime("%b %d, %Y") %></span> </em></span>
<span><em>Updated on <time itemprop="dateModified" datetime="<%= @article.edited_at&.utc&.iso8601 %>"><%= @article.edited_at&.strftime("%b %d, %Y") %></time></em></span>
<% elsif should_show_crossposted_on?(@article) %>
<span class="published-at updated-at">
<span>
<em>
Originally published at
<a href="<%= @article.canonical_url || @article.feed_source_url %>" style="color:#1395b8"><%= get_host_without_www(@article.canonical_url || @article.feed_source_url) %></a>
on
<span class="posted-date-inline"><%= (@article.originally_published_at || @article.published_at).strftime("%b %d, %Y") if @article.crossposted_at %></span>
<% if @article.crossposted_at %>
<time datetime="<%= (@article.originally_published_at || @article.published_at)&.utc&.iso8601 %>"><%= (@article.originally_published_at || @article.published_at)&.strftime("%b %d, %Y") %></time>
<% end %>
</em>
</span>
<% end %>
Expand Down
7 changes: 7 additions & 0 deletions app/views/comments/_comment_date.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div class="comment-date">
<a href="<%= decorated_comment.path %>">
<time datetime="<%= decorated_comment.published_timestamp %>">
<%= decorated_comment.readable_publish_date %>
</time>
</a>
</div>
4 changes: 1 addition & 3 deletions app/views/comments/_comment_proper.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@
<% if commentable_author_is_op?(commentable, comment) %>
<span class="op-marker"><%= get_ama_or_op_banner(commentable) %></span>
<% end %>
<div class="comment-date" data-published-timestamp="<%= decorated_comment.published_timestamp %>">
<a href="<%= comment.path %>"><%= comment.readable_publish_date %></a>
</div>
<%= render "comments/comment_date", decorated_comment: decorated_comment %>
<button class="dropbtn" aria-label="Toggle dropdown menu">
<%= image_tag("three-dots.svg", class: "dropdown-icon", alt: "Dropdown menu icon") %>
</button>
Expand Down
Loading

0 comments on commit 4e591fe

Please sign in to comment.