Skip to content

Commit 8e49cc5

Browse files
committed
feat: Add ability to see if entry was read
1 parent 4172ef4 commit 8e49cc5

File tree

12 files changed

+116
-0
lines changed

12 files changed

+116
-0
lines changed

MIGRATION.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
# Migration Guide
22
This document describes the changes that need to be made to migrate from one version of the blog to another.
33

4+
## 11.0 to 12.0
5+
6+
A new config has been added `ShowReadPostIndicator` in `appsettings.json`. The default value is `false`. If set to `true`, a subtle indicator will show which blog posts the user has already read. The read state is stored in the browser's localStorage.
7+
8+
```json
9+
{
10+
"ShowReadPostIndicator": true
11+
}
12+
```
13+
414
## 9.0 to 11.0
515

616
A new config has been added `UseMultiAuthorMode` in `appsettings.json`. The default value of this config is `false`. If set to `true` then author name will be associated with blog posts at the time of creation.

docs/Setup/Configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ The appsettings.json file has a lot of options to customize the content of the b
4747
"Shortname": "blog"
4848
},
4949
"ShowReadingIndicator": true,
50+
"ShowReadPostIndicator": true,
5051
"SimlarBlogPosts": "true",
5152
"SupportMe": {
5253
"KofiToken": "ABC123",
@@ -101,6 +102,7 @@ The appsettings.json file has a lot of options to customize the content of the b
101102
| [Giscus](./../Comments/Giscus.md) | node | Enables the comment section via giscus. If left empty the comment section will not be shown. |
102103
| [Disqus](./../Comments/Disqus.md) | node | Enables the comment section via disqus. If left empty the comment section will not be shown. |
103104
| ShowReadingIndicator | boolean | If set to `true` (default) a circle indicates the progress when a user reads a blog post (without comments). |
105+
| ShowReadPostIndicator | boolean | If set to `true` a subtle indicator shows which blog posts the user has already read. Read state is stored in the browser's localStorage. |
104106
| SimilarBlogPosts | boolean | If set to `true` (default) similar blog posts are shown at the end of a blog post. |
105107
| [SupportMe](./../Donations/Readme.md) | node | Donation sections configuration. If left empty no donation sections will not be shown. |
106108
| [ImageStorageProvider](./../Media/Readme.md) | string | Declares the type of the image storage provider (currently only `Azure`). |

src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public sealed record ApplicationConfiguration
2222

2323
public bool ShowReadingIndicator { get; init; }
2424

25+
public bool ShowReadPostIndicator { get; init; }
26+
2527
public bool ShowSimilarPosts { get; init; }
2628

2729
public bool UseMultiAuthorMode { get; init; }
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<span class="read-post-indicator text-success"
2+
aria-label="Previously read"
3+
title="You have read this article">
4+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
5+
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z"/>
6+
</svg>
7+
<span class="ms-1">Read</span>
8+
</span>
9+
10+
@code {
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.Collections.Generic;
2+
using System.Threading.Tasks;
3+
4+
namespace LinkDotNet.Blog.Web.Features.Bookmarks;
5+
6+
public interface IReadStateService
7+
{
8+
Task<bool> IsRead(string postId);
9+
Task<IReadOnlyCollection<string>> GetReadPostIds();
10+
Task MarkAsRead(string postId);
11+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using LinkDotNet.Blog.Web.Features.Services;
5+
6+
namespace LinkDotNet.Blog.Web.Features.Bookmarks;
7+
8+
public class ReadStateService : IReadStateService
9+
{
10+
private const string StorageKey = "readPosts";
11+
private readonly ILocalStorageService localStorageService;
12+
13+
public ReadStateService(ILocalStorageService localStorageService)
14+
{
15+
this.localStorageService = localStorageService;
16+
}
17+
18+
public async Task<bool> IsRead(string postId)
19+
{
20+
ArgumentException.ThrowIfNullOrEmpty(postId);
21+
await InitializeIfNotExists();
22+
var readPosts = await localStorageService.GetItemAsync<HashSet<string>>(StorageKey);
23+
24+
return readPosts.Contains(postId);
25+
}
26+
27+
public async Task<IReadOnlyCollection<string>> GetReadPostIds()
28+
{
29+
await InitializeIfNotExists();
30+
return await localStorageService.GetItemAsync<IReadOnlyCollection<string>>(StorageKey);
31+
}
32+
33+
public async Task MarkAsRead(string postId)
34+
{
35+
ArgumentException.ThrowIfNullOrEmpty(postId);
36+
await InitializeIfNotExists();
37+
38+
var readPosts = await localStorageService.GetItemAsync<HashSet<string>>(StorageKey);
39+
readPosts.Add(postId);
40+
41+
await localStorageService.SetItemAsync(StorageKey, readPosts);
42+
}
43+
44+
private async Task InitializeIfNotExists()
45+
{
46+
if (!await localStorageService.ContainsKeyAsync(StorageKey))
47+
{
48+
await localStorageService.SetItemAsync(StorageKey, Array.Empty<string>());
49+
}
50+
}
51+
}

src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
@using LinkDotNet.Blog.Web.Features.Bookmarks
33
@using LinkDotNet.Blog.Web.Features.Bookmarks.Components
44
@inject IBookmarkService BookmarkService
5+
@inject IReadStateService ReadStateService
56
@inject IOptions<ApplicationConfiguration> AppConfiguration
67

78
<article>
@@ -38,6 +39,12 @@
3839
{
3940
<li class="me-4"><i class="user-tie"></i> @BlogPost.AuthorName</li>
4041
}
42+
@if (AppConfiguration.Value.ShowReadPostIndicator && isRead)
43+
{
44+
<li class="me-4">
45+
<ReadPostIndicator />
46+
</li>
47+
}
4148
</ul>
4249
</div>
4350
<div class="description">
@@ -58,6 +65,7 @@
5865
public required BlogPost BlogPost { get; set; }
5966

6067
private bool isBookmarked = false;
68+
private bool isRead = false;
6169

6270
[Parameter]
6371
public bool UseAlternativeStyle { get; set; }
@@ -100,6 +108,10 @@
100108
if (firstRender)
101109
{
102110
isBookmarked = await BookmarkService.IsBookmarked(BlogPost.Id);
111+
if (AppConfiguration.Value.ShowReadPostIndicator)
112+
{
113+
isRead = await ReadStateService.IsRead(BlogPost.Id);
114+
}
103115
StateHasChanged();
104116
}
105117
}

src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
@inject IOptions<ProfileInformation> ProfileInformation
1818
@inject IOptions<SupportMeConfiguration> SupportConfiguration
1919
@inject IBookmarkService BookmarkService
20+
@inject IReadStateService ReadStateService
2021

2122
@if (isLoading)
2223
{
@@ -149,6 +150,10 @@ else if (BlogPost is not null)
149150
if (BlogPost is not null && firstRender)
150151
{
151152
isBookmarked = await BookmarkService.IsBookmarked(BlogPost.Id);
153+
if (AppConfiguration.Value.ShowReadPostIndicator)
154+
{
155+
await ReadStateService.MarkAsRead(BlogPost.Id);
156+
}
152157
StateHasChanged();
153158
}
154159
}

src/LinkDotNet.Blog.Web/ServiceExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection
2121
services.AddScoped<ISortOrderCalculator, SortOrderCalculator>();
2222
services.AddScoped<IUserRecordService, UserRecordService>();
2323
services.AddScoped<IBookmarkService, BookmarkService>();
24+
services.AddScoped<IReadStateService, ReadStateService>();
2425
services.AddScoped<ISitemapService, SitemapService>();
2526
services.AddScoped<IXmlWriter, XmlWriter>();
2627
services.AddScoped<IFileProcessor, FileProcessor>();

src/LinkDotNet.Blog.Web/appsettings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"CdnEndpoint": ""
4747
},
4848
"ShowReadingIndicator": true,
49+
"ShowReadPostIndicator": true,
4950
"ShowSimilarPosts": true,
5051
"UseMultiAuthorMode": false
5152
}

0 commit comments

Comments
 (0)