feat(releases-widget): add Mark as read action to mark releases as seen (#3676)

This commit is contained in:
Andre Silva
2025-08-01 13:02:07 +01:00
committed by GitHub
parent d66cccb0db
commit f63e64627c
3 changed files with 104 additions and 32 deletions

View File

@@ -2340,6 +2340,7 @@
"starsCount": "Stars", "starsCount": "Stars",
"forksCount": "Forks", "forksCount": "Forks",
"issuesCount": "Open Issues", "issuesCount": "Open Issues",
"markViewed": "Mark as viewed",
"openProjectPage": "Open Project Page", "openProjectPage": "Open Project Page",
"openReleasePage": "Open Release Page", "openReleasePage": "Open Release Page",
"releaseDescription": "Release Description", "releaseDescription": "Release Description",

View File

@@ -2,8 +2,10 @@
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { Button, Divider, Group, Stack, Text, Title, Tooltip } from "@mantine/core"; import { Button, Divider, Group, Stack, Text, Title, Tooltip } from "@mantine/core";
import { useLocalStorage } from "@mantine/hooks";
import { import {
IconArchive, IconArchive,
IconCheck,
IconCircleDot, IconCircleDot,
IconCircleFilled, IconCircleFilled,
IconExternalLink, IconExternalLink,
@@ -39,6 +41,11 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
const board = useRequiredBoard(); const board = useRequiredBoard();
const [expandedRepositoryId, setExpandedRepositoryId] = useState<string | null>(null); const [expandedRepositoryId, setExpandedRepositoryId] = useState<string | null>(null);
const hasIconColor = useMemo(() => board.iconColor !== null, [board.iconColor]); const hasIconColor = useMemo(() => board.iconColor !== null, [board.iconColor]);
const [releasesViewedList, setReleasesViewedList] = useLocalStorage<Record<string, string>>({
key: "releases-viewed-versions",
defaultValue: {},
});
const relativeDateOptions = useMemo( const relativeDateOptions = useMemo(
() => ({ () => ({
newReleaseWithin: formatRelativeDate(options.newReleaseWithin), newReleaseWithin: formatRelativeDate(options.newReleaseWithin),
@@ -125,6 +132,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
relativeDateOptions.staleReleaseWithin !== "" && response.latestReleaseAt relativeDateOptions.staleReleaseWithin !== "" && response.latestReleaseAt
? !isDateWithin(response.latestReleaseAt, relativeDateOptions.staleReleaseWithin) ? !isDateWithin(response.latestReleaseAt, relativeDateOptions.staleReleaseWithin)
: false, : false,
viewed: releasesViewedList[repository.id] === response.latestRelease,
}; };
}) })
.filter( .filter(
@@ -152,14 +160,23 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
options.topReleases, options.topReleases,
relativeDateOptions.newReleaseWithin, relativeDateOptions.newReleaseWithin,
relativeDateOptions.staleReleaseWithin, relativeDateOptions.staleReleaseWithin,
releasesViewedList,
]); ]);
const toggleExpandedRepository = useCallback( const toggleExpandedDisplay = useCallback(
(repository: ReleasesRepositoryResponse) => (repository: ReleasesRepositoryResponse) =>
setExpandedRepositoryId(expandedRepositoryId === repository.id ? "" : repository.id), setExpandedRepositoryId(expandedRepositoryId === repository.id ? null : repository.id),
[expandedRepositoryId], [expandedRepositoryId],
); );
const markReleaseViewed = useCallback(
(repository: ReleasesRepositoryResponse) => {
repository.viewed = true;
setReleasesViewedList((prev) => ({ ...prev, [repository.id]: repository.latestRelease ?? "" }));
},
[setReleasesViewedList],
);
return ( return (
<Stack gap={0} className="releases"> <Stack gap={0} className="releases">
{repositories.map((repository: ReleasesRepositoryResponse) => { {repositories.map((repository: ReleasesRepositoryResponse) => {
@@ -182,7 +199,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
[classes.active ?? ""]: isActive, [classes.active ?? ""]: isActive,
})} })}
p="xs" p="xs"
onClick={() => toggleExpandedRepository(repository)} onClick={() => toggleExpandedDisplay(repository)}
> >
<MaskedOrNormalImage <MaskedOrNormalImage
className="releases-repository-header-icon" className="releases-repository-header-icon"
@@ -232,7 +249,15 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
<Text <Text
className="releases-repository-header-releaseDate" className="releases-repository-header-releaseDate"
size="xs" size="xs"
c={repository.isNewRelease ? "primaryColor" : repository.isStaleRelease ? "secondaryColor" : "dimmed"} c={
repository.viewed
? "green"
: repository.isNewRelease
? "primaryColor"
: repository.isStaleRelease
? "secondaryColor"
: "dimmed"
}
> >
{repository.latestReleaseAt && {repository.latestReleaseAt &&
!hasError && !hasError &&
@@ -241,10 +266,22 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
style: "narrow", style: "narrow",
})} })}
</Text> </Text>
{!hasError ? ( {hasError ? (
<IconTriangleFilled
className="releases-repository-header-releaseDate-icon releases-repository-header-releaseDate-error"
size={10}
color="var(--mantine-color-red-filled)"
/>
) : repository.viewed ? (
<IconCheck
className="releases-repository-header-releaseDate-icon releases-repository-header-releaseDate-confirmed"
size={10}
color="green"
/>
) : (
(repository.isNewRelease || repository.isStaleRelease) && ( (repository.isNewRelease || repository.isStaleRelease) && (
<IconCircleFilled <IconCircleFilled
className="releases-repository-header-releaseDate-marker" className="releases-repository-header-releaseDate-icon releases-repository-header-releaseDate-marker"
size={10} size={10}
color={ color={
repository.isNewRelease repository.isNewRelease
@@ -253,19 +290,20 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
} }
/> />
) )
) : (
<IconTriangleFilled
className="releases-repository-header-releaseDate-error"
size={10}
color={"var(--mantine-color-red-filled)"}
/>
)} )}
</Group> </Group>
</Group> </Group>
{options.showDetails && ( {options.showDetails && (
<DetailsDisplay repository={repository} toggleExpandedRepository={toggleExpandedRepository} /> <DetailsDisplay repository={repository} toggleExpandedDisplay={toggleExpandedDisplay} />
)}
{isActive && (
<ExpandedDisplay
repository={repository}
hasIconColor={hasIconColor}
markReleaseViewed={markReleaseViewed}
toggleExpandedDisplay={toggleExpandedDisplay}
/>
)} )}
{isActive && <ExpandedDisplay repository={repository} hasIconColor={hasIconColor} />}
<Divider className="releases-repository-divider" /> <Divider className="releases-repository-divider" />
</Stack> </Stack>
); );
@@ -276,21 +314,21 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
interface DetailsDisplayProps { interface DetailsDisplayProps {
repository: ReleasesRepositoryResponse; 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 t = useScopedI18n("widget.releases");
const formatter = useFormatter(); const formatter = useFormatter();
return ( return (
<> <>
<Divider className="releases-repository-details-divider" onClick={() => toggleExpandedRepository(repository)} /> <Divider className="releases-repository-details-divider" onClick={() => toggleExpandedDisplay(repository)} />
<Group <Group
className={combineClasses("releases-repository-details", classes.releasesRepositoryDetails)} className={combineClasses("releases-repository-details", classes.releasesRepositoryDetails)}
justify="space-between" justify="space-between"
p={5} p={5}
onClick={() => toggleExpandedRepository(repository)} onClick={() => toggleExpandedDisplay(repository)}
> >
<Group className="releases-repository-details-icon-wrapper"> <Group className="releases-repository-details-icon-wrapper">
<Tooltip <Tooltip
@@ -484,9 +522,16 @@ const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplay
interface ExtendedDisplayProps { interface ExtendedDisplayProps {
repository: ReleasesRepositoryResponse; repository: ReleasesRepositoryResponse;
hasIconColor: boolean; hasIconColor: boolean;
markReleaseViewed: (repository: ReleasesRepositoryResponse) => void;
toggleExpandedDisplay: (repository: ReleasesRepositoryResponse) => void;
} }
const ExpandedDisplay = ({ repository, hasIconColor }: ExtendedDisplayProps) => { const ExpandedDisplay = ({
repository,
hasIconColor,
markReleaseViewed,
toggleExpandedDisplay,
}: ExtendedDisplayProps) => {
const t = useScopedI18n("widget.releases"); const t = useScopedI18n("widget.releases");
const now = useNow(); const now = useNow();
const formatter = useFormatter(); const formatter = useFormatter();
@@ -540,24 +585,48 @@ const ExpandedDisplay = ({ repository, hasIconColor }: ExtendedDisplayProps) =>
</Text> </Text>
</Text> </Text>
)} )}
<Divider className="releases-repository-expanded-actions-divider" mx="30%" />
<Button
className="releases-repository-expanded-markViewedButton"
disabled={repository.viewed}
color="green"
variant="light"
onClick={() => {
markReleaseViewed(repository);
toggleExpandedDisplay(repository);
}}
>
<Group
className="releases-repository-expanded-markViewedButton-wrapper"
gap={5}
justify="center"
align="center"
>
<IconCheck className="releases-repository-expanded-markViewedButton-icon" size="1.5em" />
<Text className="releases-repository-expanded-markViewedButton-text">{t("markViewed")}</Text>
</Group>
</Button>
{(repository.releaseUrl ?? repository.projectUrl) && ( {(repository.releaseUrl ?? repository.projectUrl) && (
<> <Button
<Divider className="releases-repository-expanded-openButton-divider" mx="30%" /> className="releases-repository-expanded-openButton"
<Button variant="light"
className="releases-repository-expanded-openButton" component="a"
variant="light" href={repository.releaseUrl ?? repository.projectUrl}
component="a" target="_blank"
href={repository.releaseUrl ?? repository.projectUrl} rel="noreferrer"
target="_blank" >
rel="noreferrer" <Group className="releases-repository-expanded-openButton-wrapper" gap={5} justify="center" align="center">
> <IconExternalLink className="releases-repository-expanded-openButton-icon" size="1.5em" />
<IconExternalLink className="releases-repository-expanded-openButton-icon" />
<Text className="releases-repository-expanded-openButton-text"> <Text className="releases-repository-expanded-openButton-text">
{repository.releaseUrl ? t("openReleasePage") : t("openProjectPage")} {repository.releaseUrl ? t("openReleasePage") : t("openProjectPage")}
</Text> </Text>
</Button> </Group>
</> </Button>
)} )}
{repository.error && ( {repository.error && (
<> <>
<Divider className="releases-repository-expanded-error-divider" mx="30%" /> <Divider className="releases-repository-expanded-error-divider" mx="30%" />

View File

@@ -37,5 +37,7 @@ export interface ReleasesRepositoryResponse extends ReleasesRepository {
iconUrl?: string; iconUrl?: string;
}; };
viewed: boolean;
error?: { code?: string; message?: string }; error?: { code?: string; message?: string };
} }