Add support for dots & pills for both news and blog posts. (#5768)

- If a returning user comes to the site, if there are unread
blog posts or news articles less than 15 quadrllion nanosecond
old will be treated as being unread. When there are unred articles,
the News or Blog link in the title bar will get a green dot indicating
articles are available. When clicking on News, then you'll get the
news categories with a pill showing how many articles are unread for
each category.

First-time visitors to the site will not get any dots or pills for
existing articles. These will only appear in subsequent visits for
new articles.

Due to the default behavior for new users, if you just look at the
preview, you will not see any pills or dots. To see what this actually
looks like, load up the preview, then go to the Chrome Developer Tools,
click on the Application tab, then on Local Storage, and then find the
visitedPages entry. Right click on the entry, select Edit Value,
and set the value to {}. Then refresh the page and you
should see some dots show up next to the Blog and News links in
the header.
This commit is contained in:
Martin Taillefer 2019-11-19 11:44:46 -08:00 committed by Istio Automation
parent 66f2d2a02c
commit 6cdafbeb3d
31 changed files with 371 additions and 29 deletions

View File

@ -26,14 +26,14 @@ disableAliases = true
latexDashes = true
extensions = [""]
[outputs]
home = ["HTML", "RSS", "REDIR"]
section = ["HTML", "RSS"]
[mediaTypes]
[mediaTypes."text/netlify"]
delimiter = ""
[outputs]
home = ["HTML", "RSS", "REDIR", "JSON"]
section = ["HTML", "RSS"]
[outputFormats]
[outputFormats.RSS]
baseName = "feed"
@ -42,6 +42,8 @@ disableAliases = true
baseName = "_redirects"
isPlainText = true
notAlternative = true
[outputFormats.JSON]
baseName = "_tracked_pages"
[related]
# Only include matches with rank >= threshold. This is a normalized rank between 0 and 100.

View File

@ -3,5 +3,6 @@ title: 2017 Posts
description: Blog posts for 2017.
weight: 20
icon: blog
decoration: dot
list_by_publishdate: true
---

View File

@ -3,5 +3,6 @@ title: 2018 Posts
description: Blog posts for 2018.
weight: 10
icon: blog
decoration: dot
list_by_publishdate: true
---

View File

@ -3,5 +3,6 @@ title: 2019 Posts
description: Blog posts for 2019.
weight: 9
icon: blog
decoration: dot
list_by_publishdate: true
---

View File

@ -4,6 +4,7 @@ description: Posts about using Istio.
linktitle: Blog
sidebar_multicard: true
icon: blog
decoration: pill
aliases:
- /blog/posts/index.html
---

View File

@ -4,4 +4,5 @@ description: Timely news about the Istio project.
linktitle: News
sidebar_multicard: true
icon: bullhorn
decoration: pill
---

View File

@ -4,4 +4,5 @@ description: Announcements for the early releases of Istio.
weight: 30
list_by_publishdate: true
layout: release-grid
decoration: dot
---

View File

@ -4,4 +4,5 @@ description: Announcements for the 1.0 release and its associated patch releases
weight: 29
list_by_publishdate: true
layout: release-grid
decoration: dot
---

View File

@ -4,4 +4,5 @@ description: Announcements for the 1.1 release and its associated patch releases
weight: 28
list_by_publishdate: true
layout: release-grid
decoration: dot
---

View File

@ -4,4 +4,5 @@ description: Announcements for the 1.2 release and its associated patch releases
weight: 27
list_by_publishdate: true
layout: release-grid
decoration: dot
---

View File

@ -4,4 +4,5 @@ description: Announcements for the 1.3 release and its associated patch releases
weight: 26
list_by_publishdate: true
layout: release-grid
decoration: dot
---

View File

@ -4,4 +4,5 @@ description: Announcements for the 1.4 release and its associated patch releases
weight: 25
list_by_publishdate: true
layout: release-grid
decoration: dot
---

View File

@ -4,4 +4,5 @@ description: Announcements for the 1.5 release and its associated patch releases
weight: 24
list_by_publishdate: true
layout: release-grid
decoration: dot
---

View File

@ -2,4 +2,5 @@
title: Release Announcements
description: Announcements for all of Istio's major releases and patch releases.
weight: 8
decoration: pill
---

View File

@ -4,4 +4,5 @@ description: Disclosed security vulnerabilities and their mitigation.
weight: 7
list_by_publishdate: true
layout: security-grid
decoration: dot
---

View File

@ -3,4 +3,5 @@ title: Support Announcements
description: Support window announcements.
weight: 15
list_by_publishdate: true
decoration: dot
---

View File

@ -243,3 +243,6 @@ other = "Related"
[security_date]
other = "Date"
[mark_all_as_read]
other = "Mark all as read"

View File

@ -33,6 +33,7 @@
{{ end }}
</ul>
{{ else }}
{{- $decoration := .Params.decoration -}}
{{ range $pages }}
{{ $pageLocation := (path.Dir (path.Dir .File.Dir)) }}
{{ if eq $parentDir $pageLocation }}
@ -40,6 +41,10 @@
<h5>
<a href="{{ .Permalink }}">
{{- if .Params.icon -}}<i class="page-icon">{{- partial "icon.html" .Params.icon -}}</i>{{- end -}}{{- .Title -}}
{{- if $decoration -}}
<i class="{{ $decoration }}" data-prefix="{{ .RelPermalink }}"></i>
{{- end -}}
</a>
</h5>
<p>{{ .Description }}</p>

View File

@ -15,11 +15,14 @@
<table>
<tbody>
{{ $decoration := .Params.decoration }}
{{ range $pages }}
{{ $pageLocation := (path.Dir (path.Dir .File.Dir)) }}
{{ if eq $parentDir $pageLocation }}
<tr>
<td><a href="{{ .Permalink }}">Istio {{ .LinkTitle }}</a></td>
<td><a href="{{ .Permalink }}">Istio {{ .LinkTitle }}
{{- if $decoration -}}<i class="{{ $decoration }}" data-prefix="{{ .RelPermalink }}"></i>{{- end -}}
</a></td>
<td>{{ .PublishDate.Format (i18n "page_publish_date_format") -}}</td>
</tr>
{{ end }}

View File

@ -22,11 +22,14 @@
</tr>
</thead>
<tbody>
{{ $decoration := .Params.decoration }}
{{ range $pages }}
{{ $pageLocation := (path.Dir (path.Dir .File.Dir)) }}
{{ if eq $parentDir $pageLocation }}
<tr>
<td><a href="{{ .Permalink }}">{{ .LinkTitle }}</a></td>
<td><a href="{{ .Permalink }}">{{ .LinkTitle }}
{{- if $decoration -}}<i class="{{ $decoration }}" data-prefix="{{ .RelPermalink }}"></i>{{- end -}}
</a></td>
{{ if .Params.cve }}
<td>
{{ $first := true }}

32
layouts/index.json Normal file
View File

@ -0,0 +1,32 @@
{{- $home := .Site.BaseURL -}}
{{- printf "{" }}
{{ $pages := (where .Site.Pages "Section" "news") -}}
{{- $now := now -}}
{{- $limit := $now.Add -15000000000000000 -}}
{{- $first := true -}}
{{- range $post := $pages -}}
{{- if and (not $post.Draft) $post.IsPage $post.PublishDate -}}
{{- if $limit.Before $post.PublishDate -}}
{{- if not $first -}},{{ end -}}
{{- $first = false }}
"{{ $post.URL }}": "{{ $post.PublishDate.Format "2006-01-02" }}"
{{- end -}}
{{- end -}}
{{- end -}}
{{- $pages := (where .Site.Pages "Section" "blog") -}}
{{- range $post := $pages -}}
{{- if and (not $post.Draft) $post.IsPage $post.PublishDate -}}
{{- if $limit.Before $post.PublishDate -}}
{{- if not $first -}},{{ end -}}
{{- $first = false }}
"{{ $post.URL }}": "{{ $post.PublishDate.Format "2006-01-02" }}"
{{- end -}}
{{- end -}}
{{- end }}
{{ printf "}" -}}

View File

@ -4,9 +4,6 @@
{{ $latest_post := index $posts 0 }}
{{ $blog_home := .Site.GetPage "section" "blog" }}
{{ $news := (where .Site.Pages "Section" "news").ByPublishDate.Reverse }}
{{ $news_home := .Site.GetPage "section" "news" }}
<header>
<nav >
<a id="brand" href="{{ $home.Permalink }}">
@ -39,17 +36,17 @@
{{ with $latest_post }}
{{ if eq $section "blog" }}
<span title="{{ $blog_home.Description }}">{{ $blog_home.LinkTitle }}</span>
<span title="{{ $blog_home.Description }}">{{ $blog_home.LinkTitle }}<i class="dot" data-prefix="/blog"></i></span>
{{ else }}
<a title="{{ $blog_home.Description }}" href="{{ .Permalink }}">{{ $blog_home.LinkTitle }}</a>
<a title="{{ $blog_home.Description }}" href="{{ $blog_home.Permalink }}/{{ $latest_post.PublishDate.Year }}">{{ $blog_home.LinkTitle }}<i class="dot" data-prefix="/blog"></i></a>
{{ end }}
{{ end }}
{{ with (.Site.GetPage "section" "news") }}
{{ if eq $section "news" }}
<span title="{{ $news_home.Description }}">{{ $news_home.LinkTitle }}</span>
<span title="{{ .Description }}">{{ .LinkTitle }}<i class="dot" data-prefix="/news"></i></span>
{{ else }}
<a title="{{ $news_home.Description }}" href="{{ .Permalink }}">{{ $news_home.LinkTitle }}</a>
<a title="{{ .Description }}" href="{{ .Permalink }}">{{ .LinkTitle }}<i class="dot" data-prefix="/news"></i></a>
{{ end }}
{{ end }}

View File

@ -36,10 +36,19 @@
</i>
{{- end -}}
<div>
<h1 id="title">
{{- .Title -}}
</h1>
<div style="width: 100%">
{{ if .Params.decoration }}
<div class="mark-all-read-container">
<h1 id="title">
{{- .Title -}}
</h1>
<button id="mark-all-read" data-prefix="{{ .RelPermalink }}" onclick="markPagesAsVisited('{{ .RelPermalink }}')">{{ i18n "mark_all_as_read" }}</button>
</div>
{{ else }}
<h1 id="title">
{{- .Title -}}
</h1>
{{ end }}
{{ if .Params.subtitle }}
{{ if (strings.HasSuffix .Params.subtitle ".") }}

View File

@ -36,20 +36,20 @@
{{ $pages := (where .Site.Pages "Section" "blog").ByPublishDate.Reverse }}
{{ range $post := $pages }}
{{ if and (not $post.Draft) $post.IsPage }}
<item>
<title>{{ $post.Title }}</title>
<description>{{ $post.Content | html }}</description>
<pubDate>{{ $post.PublishDate.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
<link>{{ $post.Permalink }}</link>
<author>{{ $post.Params.attribution }}</author>
<guid isPermaLink="true">{{ $post.Permalink }}</guid>
{{ if and (not $post.Draft) $post.IsPage }}
<item>
<title>{{ $post.Title }}</title>
<description>{{ $post.Content | html }}</description>
<pubDate>{{ $post.PublishDate.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
<link>{{ $post.Permalink }}</link>
<author>{{ $post.Params.attribution }}</author>
<guid isPermaLink="true">{{ $post.Permalink }}</guid>
{{ range $kw := $post.Keywords }}
<category>{{ $kw }}</category>
{{ range $kw := $post.Keywords }}
<category>{{ $kw }}</category>
{{ end }}
</item>
{{ end }}
</item>
{{ end }}
{{ end }}
</channel>
</rss>

View File

@ -39,6 +39,7 @@ babel --source-maps --minified --no-comments \
tmp/js/prism.js \
tmp/js/codeBlocks.js \
tmp/js/links.js \
tmp/js/readTracking.js \
tmp/js/scroll.js \
tmp/js/overlays.js \
tmp/js/lang.js \

View File

@ -36,6 +36,7 @@
@import "misc/popover";
@import "misc/primary";
@import "misc/promotion";
@import "misc/read-tracking";
@import "misc/relnote-actions";
@import "misc/search-results";
@import "misc/section-index";

View File

@ -0,0 +1,79 @@
.dot {
display: none;
}
.dot.visible {
@media screen {
display: inline-block;
width: 6px;
height: 6px;
margin-bottom: calc(1em / 2);
border-radius: 50%;
background: $pillBackgroundColor;
color: $pillTextColor;
}
}
.pill {
display: none;
}
.pill.visible {
@media screen {
display: inline-block;
position: relative;
top: -6px;
padding-left: 4px;
padding-right: 4px;
margin-left: 4px;
font-size: 75%;
border-radius: 50%;
background: $pillBackgroundColor;
color: $pillTextColor;
}
}
#mark-all-read {
display: none;
}
#mark-all-read.visible {
display: inline;
text-align: center;
white-space: nowrap;
vertical-align: middle;
user-select: none;
border: 1px solid transparent;
padding: .275rem .75rem;
font-size: .75rem;
line-height: 1;
border-radius: $border-radius;
background-color: $secondBrandColor;
color: $textBrandColor;
font-weight: $buttonWeight;
letter-spacing: 1px;
cursor: pointer;
box-shadow: 3px 3px 8px #a7a7a7;
margin-right: 10px;
&:hover {
background-color: $mainBrandColor;
color: $textBrandColor;
text-decoration: none;
}
&:active {
background-color: $buttonActiveColor;
color: $textBrandColor;
}
&:focus {
color: $textBrandColor;
}
}
.mark-all-read-container {
display: flex;
justify-content: space-between;
align-items: center;
}

View File

@ -80,6 +80,9 @@
--companyLogoBackgroundColor: lightgrey;
--companyLogoTaglineColor: #{$black};
--pillBackgroundColor: lightgreen;
--pillTextColor: black;
--textWeight: 300;
--linkWeight: 300;
--h1Weight: 400;

View File

@ -79,6 +79,9 @@ html {
--companyLogoBackgroundColor: var(--backgroundColor);
--companyLogoTaglineColor: var(--textColor);
--pillBackgroundColor: lightgreen;
--pillTextColor: black;
--textWeight: 400;
--linkWeight: 400;
--h1Weight: 400;

View File

@ -79,6 +79,9 @@ $floatingButtonHoverColor: var(--floatingButtonHoverColor);
$companyLogoBackgroundColor: var(--companyLogoBackgroundColor);
$companyLogoTaglineColor: var(--companyLogoTaglineColor);
$pillBackgroundColor: var(--pillBackgroundColor);
$pillTextColor: var(--pillTextColor);
$textWeight: var(--textWeight);
$linkWeight: var(--linkWeight);
$h1Weight: var(--h1Weight);

183
src/ts/readTracking.ts Normal file
View File

@ -0,0 +1,183 @@
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
let trackedPages: any = null;
let visitedPages: any = null;
function loadVisitedPages(): void {
const blob = localStorage.getItem("visitedPages");
if (blob != null) {
visitedPages = JSON.parse(blob);
}
}
function saveVisitedPages(): void {
localStorage.setItem("visitedPages", JSON.stringify(visitedPages));
}
function markPagesAsVisited(prefix: string): void {
if (trackedPages !== null) {
let dirty = false;
for (const trackedPage in trackedPages) {
if (trackedPages.hasOwnProperty(trackedPage)) {
if (trackedPage.startsWith(prefix)) {
visitedPages[trackedPage] = 1;
dirty = true;
}
}
}
if (dirty) {
saveVisitedPages();
setPills();
setDots();
setMarkAllRead();
}
}
}
function setPills(): void {
document.querySelectorAll<HTMLElement>(".pill").forEach(pill => {
const prefix = pill.dataset.prefix;
if (prefix === undefined) {
return;
}
const count = countUnvisited(prefix);
if (count > 0) {
pill.classList.add("visible");
pill.innerText = count.toString();
} else {
pill.classList.remove("visible");
}
});
}
function setDots(): void {
document.querySelectorAll<HTMLElement>(".dot").forEach(dot => {
const prefix = dot.dataset.prefix;
if (prefix === undefined) {
return;
}
const count = countUnvisited(prefix);
if (count > 0) {
dot.classList.add("visible");
} else {
dot.classList.remove("visible");
}
});
}
function setMarkAllRead(): void {
const button = document.getElementById("mark-all-read");
if (button != null) {
const prefix = button.dataset.prefix;
if (prefix === undefined) {
return;
}
const count = countUnvisited(prefix);
if (count > 0) {
button.classList.add("visible");
} else {
button.classList.remove("visible");
}
}
}
function countUnvisited(prefix: string): number {
let count = 0;
for (const trackedPage in trackedPages) {
if (trackedPages.hasOwnProperty(trackedPage)) {
if (trackedPage.startsWith(prefix)) {
let found = false;
for (const visitedPage in visitedPages) {
if (trackedPage === visitedPage) {
found = true;
break;
}
}
if (!found) {
count++;
}
}
}
}
return count;
}
function handleReadTracking(): void {
// Asynchronously loads the set of tracked pages, which are the pages we
// consider for pills
function fetchTrackedPagesIndex(): void {
fetch("/_tracked_pages.json")
.then(response => {
if (!response.ok) {
throw new Error("HTTP error " + response.status);
}
return response.json();
})
.then(json => {
trackedPages = json;
let dirty = false;
if (visitedPages === null) {
// if we didn't find any list of visited pages, initialize it to the set of tracked pages
visitedPages = new Map();
for (const trackedPage in trackedPages) {
if (trackedPages.hasOwnProperty(trackedPage)) {
visitedPages[trackedPage] = 1;
}
}
dirty = true;
} else {
// if we did find a list of visited pages, trim it to only contain what's in the tracked pages map
for (const visitedPage in visitedPages) {
if (visitedPages.hasOwnProperty(visitedPage)) {
if (trackedPages[visitedPage] === undefined) {
visitedPages.delete(visitedPage);
dirty = true;
}
}
}
}
// if the current page is being tracked, record that the user has visited this page
const page = window.location.pathname;
if (trackedPages[page] !== undefined) {
if (visitedPages[page] === undefined) {
visitedPages[page] = 1;
dirty = true;
}
}
if (dirty) {
saveVisitedPages();
}
setPills();
setDots();
setMarkAllRead();
});
}
loadVisitedPages();
fetchTrackedPagesIndex();
}
handleReadTracking();