Skip to content

Commit 8b88c68

Browse files
committed
Merge branch 'feature_mark_as_read' of https://github.com/imhariprakash/hackertab.dev into imhariprakash-feature_mark_as_read2
2 parents aa84ffb + 2184806 commit 8b88c68

File tree

6 files changed

+185
-9
lines changed

6 files changed

+185
-9
lines changed

src/assets/App.css

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ a {
5151
align-items: center;
5252
}
5353

54+
.readPostWrapper {
55+
position: relative;
56+
}
57+
58+
.readPostWrapper .subTitle::after {
59+
content: ' \2705';
60+
margin-left: 0.2em;
61+
font-size: 0.85em;
62+
}
63+
5464
.loadingIcon {
5565
color: var(--app-name-text-color);
5666
}
@@ -201,6 +211,7 @@ a {
201211
box-sizing: border-box;
202212
scroll-snap-align: start;
203213
width: 100%;
214+
position: relative;
204215
}
205216

206217
.blockContent {
@@ -467,7 +478,7 @@ a {
467478
padding: 0;
468479
pointer-events: all;
469480
text-align: center;
470-
transition: opacity 0.3s ease, right 0.3s ease;
481+
transition: opacity 0.3s ease, right 0.3s ease, transform 0.1s ease, background-color 0.15s ease, color 0.15s ease;
471482
width: 28px;
472483
margin-bottom: 6px;
473484
margin-right: 6px;
@@ -490,6 +501,59 @@ a {
490501
}
491502
}
492503

504+
.markAsReadAction {
505+
position: relative;
506+
}
507+
508+
.markAsReadTooltip {
509+
position: absolute;
510+
bottom: 100%;
511+
right: 5px;
512+
margin-bottom: 6px;
513+
padding: 4px 8px;
514+
background-color: var(--card-action-button-background);
515+
color: var(--card-action-button-color);
516+
font-size: 12px;
517+
white-space: nowrap;
518+
border-radius: 4px;
519+
opacity: 0;
520+
transition: opacity 0.2s ease;
521+
}
522+
523+
.markAsReadAction:hover .markAsReadTooltip {
524+
opacity: 1;
525+
}
526+
527+
.centerMessageWrapper.cardLoading {
528+
position: absolute;
529+
top: 0;
530+
left: 0;
531+
right: 0;
532+
bottom: 0;
533+
}
534+
535+
.centerMessageIcon {
536+
display: block;
537+
font-size: 36px;
538+
margin-bottom: 12px;
539+
}
540+
541+
.centerMessage p {
542+
margin: 0;
543+
color: var(--primary-text-color);
544+
}
545+
546+
.centerMessage p:first-of-type {
547+
font-size: 16px;
548+
line-height: 24px;
549+
margin-bottom: 6px;
550+
}
551+
552+
.centerMessageSubtext {
553+
font-size: 14px;
554+
opacity: 0.7;
555+
}
556+
493557
/* Card (element) */
494558
.blockRow {
495559
padding: 8px 16px;

src/components/Elements/CardWithActions/CardItemWithActions.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import React, { useCallback, useEffect, useState } from 'react'
2-
import { BiBookmarkMinus, BiBookmarkPlus, BiShareAlt } from 'react-icons/bi'
2+
import { BiBookmarkMinus, BiBookmarkPlus, BiCheckCircle, BiShareAlt } from 'react-icons/bi'
33
import { ShareModal } from 'src/features/shareModal'
44
import { ShareModalData } from 'src/features/shareModal/types'
55
import { Attributes, trackLinkBookmark, trackLinkUnBookmark } from 'src/lib/analytics'
66
import { useBookmarks } from 'src/stores/bookmarks'
7+
import { useReadPosts } from 'src/stores/readPosts'
78

89
type CardItemWithActionsProps = {
910
item: {
@@ -32,10 +33,20 @@ export const CardItemWithActions = ({
3233
const [shareModalData, setShareModalData] = useState<ShareModalData>()
3334

3435
const { bookmarkPost, unbookmarkPost, userBookmarks } = useBookmarks()
36+
const { readPostIds, markAsRead, markAsUnread } = useReadPosts()
37+
const isRead = readPostIds.includes(item.id)
3538
const [isBookmarked, setIsBookmarked] = useState(
3639
userBookmarks.some((bm) => bm.source === source && bm.url === item.url)
3740
)
3841

42+
const onMarkAsReadClick = useCallback(() => {
43+
if (isRead) {
44+
markAsUnread(item.id)
45+
} else {
46+
markAsRead(item.id)
47+
}
48+
}, [isRead, markAsRead, markAsUnread, item.id])
49+
3950
const onBookmarkClick = useCallback(() => {
4051
const itemToBookmark = {
4152
title: item.title,
@@ -100,6 +111,16 @@ export const CardItemWithActions = ({
100111
{!isBookmarked ? <BiBookmarkPlus /> : <BiBookmarkMinus />}
101112
</button>
102113
)}
114+
115+
<span className="markAsReadAction">
116+
<span className="markAsReadTooltip">{isRead ? 'Mark as unread' : 'Mark as read'}</span>
117+
<button
118+
className="blockActionButton"
119+
onClick={onMarkAsReadClick}
120+
aria-label={isRead ? 'Mark as unread' : 'Mark as read'}>
121+
<BiCheckCircle />
122+
</button>
123+
</span>
103124
</div>
104125
</div>
105126
)

src/components/List/ListComponent.tsx

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React, { memo, ReactNode, useMemo } from 'react'
22
import { Placeholder } from 'src/components/placeholders'
33
import { MAX_ITEMS_PER_CARD } from 'src/config'
4+
import { useUserPreferences } from 'src/stores/preferences'
5+
import { useReadPosts } from 'src/stores/readPosts'
46

57
type PlaceholdersProps = {
68
placeholder: ReactNode
@@ -42,13 +44,23 @@ export function ListComponent<T extends any>(props: ListComponentPropsType<T>) {
4244
limit = MAX_ITEMS_PER_CARD,
4345
} = props
4446

47+
const { readPostIds } = useReadPosts()
48+
const { showReadPosts } = useUserPreferences()
49+
50+
const filteredItems = useMemo(() => {
51+
if (!items || items.length === 0) return []
52+
if (showReadPosts) return items
53+
const readSet = new Set(readPostIds)
54+
return items.filter((item: any) => !readSet.has(item.id))
55+
}, [items, readPostIds, showReadPosts])
56+
4557
const sortedData = useMemo(() => {
46-
if (!items || items.length == 0) return []
47-
if (!sortBy) return items
58+
if (!filteredItems || filteredItems.length == 0) return []
59+
if (!sortBy) return filteredItems
4860

4961
const result = sortFn
50-
? [...items].sort(sortFn)
51-
: [...items].sort((a, b) => {
62+
? [...filteredItems].sort(sortFn)
63+
: [...filteredItems].sort((a, b) => {
5264
const aVal = a[sortBy]
5365
const bVal = b[sortBy]
5466
if (typeof aVal === 'number' && typeof bVal === 'number') return bVal - aVal
@@ -57,7 +69,9 @@ export function ListComponent<T extends any>(props: ListComponentPropsType<T>) {
5769
})
5870

5971
return result
60-
}, [sortBy, sortFn, items])
72+
}, [sortBy, sortFn, filteredItems])
73+
74+
const readSet = useMemo(() => new Set(readPostIds), [readPostIds])
6175

6276
const enrichedItems = useMemo(() => {
6377
if (!sortedData || sortedData.length === 0) {
@@ -66,7 +80,15 @@ export function ListComponent<T extends any>(props: ListComponentPropsType<T>) {
6680

6781
try {
6882
return sortedData.slice(0, limit).map((item, index) => {
69-
let content: ReactNode[] = [renderItem(item, index)]
83+
const isRead = readSet.has((item as any).id)
84+
const itemNode = isRead ? (
85+
<div key={(item as any).id} className="readPostWrapper" aria-label="Read">
86+
{renderItem(item, index)}
87+
</div>
88+
) : (
89+
renderItem(item, index)
90+
)
91+
let content: ReactNode[] = [itemNode]
7092
if (header && index === 0) {
7193
content.unshift(header)
7294
}
@@ -76,7 +98,7 @@ export function ListComponent<T extends any>(props: ListComponentPropsType<T>) {
7698
} catch (e) {
7799
return []
78100
}
79-
}, [sortedData, header, renderItem, limit])
101+
}, [sortedData, header, renderItem, limit, readSet])
80102

81103
if (isLoading) {
82104
return <Placeholders placeholder={placeholder} />
@@ -93,5 +115,17 @@ export function ListComponent<T extends any>(props: ListComponentPropsType<T>) {
93115
)
94116
}
95117

118+
if (items && items.length > 0 && filteredItems.length === 0) {
119+
return (
120+
<div className="centerMessageWrapper cardLoading">
121+
<div className="centerMessage errorMsg">
122+
<span className="centerMessageIcon"></span>
123+
<p><b>You're all caught up!</b></p>
124+
<p className="centerMessageSubtext">Check back later for fresh content.</p>
125+
</div>
126+
</div>
127+
)
128+
}
129+
96130
return <>{enrichedItems}</>
97131
}

src/features/settings/components/GeneralSettings/GeneralSettings.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ export const GeneralSettings = () => {
2525
listingMode,
2626
theme,
2727
maxVisibleCards,
28+
showReadPosts,
2829
setTheme,
2930
setListingMode,
3031
setMaxVisibleCards,
3132
setOpenLinksNewTab,
33+
setShowReadPosts,
3234
} = useUserPreferences()
3335

3436
const onOpenLinksNewTabChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -52,6 +54,10 @@ export const GeneralSettings = () => {
5254
identifyUserTheme(newTheme)
5355
}
5456

57+
const onShowReadPostsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
58+
setShowReadPosts(e.target.checked)
59+
}
60+
5561
return (
5662
<SettingsContentLayout
5763
title="General Settings"
@@ -88,6 +94,17 @@ export const GeneralSettings = () => {
8894
</div>
8995
</div>
9096

97+
<div className="settingRow">
98+
<p className="settingTitle">Display read posts</p>
99+
<div className="settingContent">
100+
<Toggle
101+
checked={showReadPosts}
102+
icons={false}
103+
onChange={onShowReadPostsChange}
104+
/>
105+
</div>
106+
</div>
107+
91108
<DNDSettings />
92109

93110
<DeleteAccount />

src/stores/preferences.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export type UserPreferencesState = {
3636
userCustomCards: SupportedCardType[]
3737
advStatus: boolean
3838
DNDDuration: DNDDuration
39+
showReadPosts: boolean
3940
}
4041

4142
type UserPreferencesStoreActions = {
@@ -61,6 +62,7 @@ type UserPreferencesStoreActions = {
6162
addSearchEngine: (searchEngine: SearchEngineType) => void
6263
removeSearchEngine: (searchEngineUrl: string) => void
6364
setAdvStatus: (status: boolean) => void
65+
setShowReadPosts: (value: boolean) => void
6466
}
6567

6668
export const useUserPreferences = create(
@@ -92,6 +94,7 @@ export const useUserPreferences = create(
9294
userCustomCards: [],
9395
DNDDuration: 'never',
9496
advStatus: false,
97+
showReadPosts: false,
9598
setLayout: (layout) => set({ layout }),
9699
setPromptEngine: (promptEngine: string) => set({ promptEngine }),
97100
setListingMode: (listingMode: ListingMode) => set({ listingMode }),
@@ -166,6 +169,7 @@ export const useUserPreferences = create(
166169
}
167170
}),
168171
setAdvStatus: (status) => set({ advStatus: status }),
172+
setShowReadPosts: (value) => set({ showReadPosts: value }),
169173
removeCard: (cardName: string) =>
170174
set((state) => {
171175
return {

src/stores/readPosts.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { create } from 'zustand'
2+
import { persist } from 'zustand/middleware'
3+
4+
const MAX_READ_POST_IDS = 5000
5+
6+
type ReadPostsStore = {
7+
readPostIds: string[]
8+
markAsRead: (postId: string) => void
9+
markAsUnread: (postId: string) => void
10+
}
11+
12+
export const useReadPosts = create<ReadPostsStore>()(
13+
persist(
14+
(set) => ({
15+
readPostIds: [],
16+
17+
markAsRead: (postId) =>
18+
set((state) => {
19+
if (state.readPostIds.includes(postId)) return state
20+
21+
const next =
22+
state.readPostIds.length >= MAX_READ_POST_IDS
23+
? [...state.readPostIds.slice(1), postId]
24+
: [...state.readPostIds, postId]
25+
26+
return { readPostIds: next }
27+
}),
28+
29+
markAsUnread: (postId) =>
30+
set((state) => ({
31+
readPostIds: state.readPostIds.filter((id) => id !== postId),
32+
})),
33+
}),
34+
{ name: 'read_post_ids' }
35+
)
36+
)

0 commit comments

Comments
 (0)