From f63e64627c79004b1e1bbb5a525fdde58f803da7 Mon Sep 17 00:00:00 2001 From: Andre Silva <32734153+Aandree5@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:02:07 +0100 Subject: [PATCH] feat(releases-widget): add `Mark as read` action to mark releases as seen (#3676) --- packages/translation/src/lang/en.json | 1 + packages/widgets/src/releases/component.tsx | 133 +++++++++++++----- .../src/releases/releases-repository.ts | 2 + 3 files changed, 104 insertions(+), 32 deletions(-) diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index dfc20d1ff..1b69d9fd4 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -2340,6 +2340,7 @@ "starsCount": "Stars", "forksCount": "Forks", "issuesCount": "Open Issues", + "markViewed": "Mark as viewed", "openProjectPage": "Open Project Page", "openReleasePage": "Open Release Page", "releaseDescription": "Release Description", diff --git a/packages/widgets/src/releases/component.tsx b/packages/widgets/src/releases/component.tsx index 72be89f42..51cefebd0 100644 --- a/packages/widgets/src/releases/component.tsx +++ b/packages/widgets/src/releases/component.tsx @@ -2,8 +2,10 @@ import { useCallback, useMemo, useState } from "react"; import { Button, Divider, Group, Stack, Text, Title, Tooltip } from "@mantine/core"; +import { useLocalStorage } from "@mantine/hooks"; import { IconArchive, + IconCheck, IconCircleDot, IconCircleFilled, IconExternalLink, @@ -39,6 +41,11 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas const board = useRequiredBoard(); const [expandedRepositoryId, setExpandedRepositoryId] = useState(null); const hasIconColor = useMemo(() => board.iconColor !== null, [board.iconColor]); + const [releasesViewedList, setReleasesViewedList] = useLocalStorage>({ + key: "releases-viewed-versions", + defaultValue: {}, + }); + const relativeDateOptions = useMemo( () => ({ newReleaseWithin: formatRelativeDate(options.newReleaseWithin), @@ -125,6 +132,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas relativeDateOptions.staleReleaseWithin !== "" && response.latestReleaseAt ? !isDateWithin(response.latestReleaseAt, relativeDateOptions.staleReleaseWithin) : false, + viewed: releasesViewedList[repository.id] === response.latestRelease, }; }) .filter( @@ -152,14 +160,23 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas options.topReleases, relativeDateOptions.newReleaseWithin, relativeDateOptions.staleReleaseWithin, + releasesViewedList, ]); - const toggleExpandedRepository = useCallback( + const toggleExpandedDisplay = useCallback( (repository: ReleasesRepositoryResponse) => - setExpandedRepositoryId(expandedRepositoryId === repository.id ? "" : repository.id), + setExpandedRepositoryId(expandedRepositoryId === repository.id ? null : repository.id), [expandedRepositoryId], ); + const markReleaseViewed = useCallback( + (repository: ReleasesRepositoryResponse) => { + repository.viewed = true; + setReleasesViewedList((prev) => ({ ...prev, [repository.id]: repository.latestRelease ?? "" })); + }, + [setReleasesViewedList], + ); + return ( {repositories.map((repository: ReleasesRepositoryResponse) => { @@ -182,7 +199,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas [classes.active ?? ""]: isActive, })} p="xs" - onClick={() => toggleExpandedRepository(repository)} + onClick={() => toggleExpandedDisplay(repository)} > {repository.latestReleaseAt && !hasError && @@ -241,10 +266,22 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas style: "narrow", })} - {!hasError ? ( + {hasError ? ( + + ) : repository.viewed ? ( + + ) : ( (repository.isNewRelease || repository.isStaleRelease) && ( ) - ) : ( - )} {options.showDetails && ( - + + )} + {isActive && ( + )} - {isActive && } ); @@ -276,21 +314,21 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas interface DetailsDisplayProps { repository: ReleasesRepositoryResponse; - toggleExpandedRepository: (repository: ReleasesRepositoryResponse) => void; + toggleExpandedDisplay: (repository: ReleasesRepositoryResponse) => void; } -const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplayProps) => { +const DetailsDisplay = ({ repository, toggleExpandedDisplay }: DetailsDisplayProps) => { const t = useScopedI18n("widget.releases"); const formatter = useFormatter(); return ( <> - toggleExpandedRepository(repository)} /> + toggleExpandedDisplay(repository)} /> toggleExpandedRepository(repository)} + onClick={() => toggleExpandedDisplay(repository)} > void; + toggleExpandedDisplay: (repository: ReleasesRepositoryResponse) => void; } -const ExpandedDisplay = ({ repository, hasIconColor }: ExtendedDisplayProps) => { +const ExpandedDisplay = ({ + repository, + hasIconColor, + markReleaseViewed, + toggleExpandedDisplay, +}: ExtendedDisplayProps) => { const t = useScopedI18n("widget.releases"); const now = useNow(); const formatter = useFormatter(); @@ -540,24 +585,48 @@ const ExpandedDisplay = ({ repository, hasIconColor }: ExtendedDisplayProps) => )} + + + + + {(repository.releaseUrl ?? repository.projectUrl) && ( - <> - - - + + )} + {repository.error && ( <> diff --git a/packages/widgets/src/releases/releases-repository.ts b/packages/widgets/src/releases/releases-repository.ts index 31b8c6ad2..157d9a146 100644 --- a/packages/widgets/src/releases/releases-repository.ts +++ b/packages/widgets/src/releases/releases-repository.ts @@ -37,5 +37,7 @@ export interface ReleasesRepositoryResponse extends ReleasesRepository { iconUrl?: string; }; + viewed: boolean; + error?: { code?: string; message?: string }; }