support for marking entries older than a threshold

This commit is contained in:
Athou
2022-08-15 20:47:46 +02:00
parent 11f5b22cb4
commit a8db632c4a
6 changed files with 125 additions and 74 deletions

View File

@@ -3,6 +3,8 @@ import { client } from "app/client"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { RootState } from "app/store" import { RootState } from "app/store"
import { Entries, Entry, MarkRequest } from "app/types" import { Entries, Entry, MarkRequest } from "app/types"
// eslint-disable-next-line import/no-cycle
import { reloadTree } from "./tree"
export type EntrySourceType = "category" | "feed" export type EntrySourceType = "category" | "feed"
export type EntrySource = { type: EntrySourceType; id: string } export type EntrySource = { type: EntrySourceType; id: string }
@@ -77,10 +79,14 @@ export const markEntry = createAsyncThunk(
condition: arg => arg.entry.read !== arg.read, condition: arg => arg.entry.read !== arg.read,
} }
) )
export const markAllEntries = createAsyncThunk("entries/entry/markAll", (arg: { sourceType: EntrySourceType; req: MarkRequest }) => { export const markAllEntries = createAsyncThunk<void, { sourceType: EntrySourceType; req: MarkRequest }, { state: RootState }>(
const endpoint = arg.sourceType === "category" ? client.category.markEntries : client.feed.markEntries "entries/entry/markAll",
endpoint(arg.req) async (arg, thunkApi) => {
}) const endpoint = arg.sourceType === "category" ? client.category.markEntries : client.feed.markEntries
await endpoint(arg.req)
thunkApi.dispatch(reloadTree())
}
)
export const selectEntry = createAsyncThunk<void, Entry, { state: RootState }>("entries/entry/select", (arg, thunkApi) => { export const selectEntry = createAsyncThunk<void, Entry, { state: RootState }>("entries/entry/select", (arg, thunkApi) => {
const state = thunkApi.getState() const state = thunkApi.getState()
const entry = state.entries.entries.find(e => e.id === arg.id) const entry = state.entries.entries.find(e => e.id === arg.id)
@@ -146,10 +152,12 @@ export const entriesSlice = createSlice({
e.read = action.meta.arg.read e.read = action.meta.arg.read
}) })
}) })
builder.addCase(markAllEntries.pending, state => { builder.addCase(markAllEntries.pending, (state, action) => {
state.entries.forEach(e => { state.entries
e.read = true .filter(e => (action.meta.arg.req.olderThan ? e.date < action.meta.arg.req.olderThan : true))
}) .forEach(e => {
e.read = true
})
}) })
builder.addCase(loadEntries.pending, (state, action) => { builder.addCase(loadEntries.pending, (state, action) => {
state.source = action.meta.arg state.source = action.meta.arg

View File

@@ -2,7 +2,8 @@ import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
import { client } from "app/client" import { client } from "app/client"
import { Category, CollapseRequest } from "app/types" import { Category, CollapseRequest } from "app/types"
import { visitCategoryTree } from "app/utils" import { visitCategoryTree } from "app/utils"
import { markAllEntries, markEntry } from "./entries" // eslint-disable-next-line import/no-cycle
import { markEntry } from "./entries"
import { redirectTo } from "./redirect" import { redirectTo } from "./redirect"
interface TreeState { interface TreeState {
@@ -47,26 +48,6 @@ export const treeSlice = createSlice({
}) })
) )
}) })
builder.addCase(markAllEntries.pending, (state, action) => {
if (!state.rootCategory) return
const { sourceType } = action.meta.arg
const sourceId = action.meta.arg.req.id
visitCategoryTree(state.rootCategory, c => {
if (sourceType === "category" && c.id === sourceId) {
visitCategoryTree(c, c2 =>
c2.feeds.forEach(f => {
f.unread = 0
})
)
} else if (sourceType === "feed") {
c.feeds
.filter(f => f.id === +sourceId)
.forEach(f => {
f.unread = 0
})
}
})
})
builder.addCase(redirectTo, state => { builder.addCase(redirectTo, state => {
state.mobileMenuOpen = false state.mobileMenuOpen = false
}) })

View File

@@ -1,12 +1,12 @@
import { t, Trans } from "@lingui/macro" import { t } from "@lingui/macro"
import { Center, Code, Divider, Group, Text } from "@mantine/core" import { Center, Divider, Group } from "@mantine/core"
import { openConfirmModal } from "@mantine/modals" import { reloadEntries } from "app/slices/entries"
import { markAllEntries, reloadEntries } from "app/slices/entries"
import { changeReadingMode, changeReadingOrder } from "app/slices/user" import { changeReadingMode, changeReadingOrder } from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { ActionButton } from "components/ActionButtton" import { ActionButton } from "components/ActionButtton"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
import { TbArrowDown, TbArrowUp, TbChecks, TbEye, TbEyeOff, TbRefresh, TbUser } from "react-icons/tb" import { TbArrowDown, TbArrowUp, TbEye, TbEyeOff, TbRefresh, TbUser } from "react-icons/tb"
import { MarkAllAsReadButton } from "./MarkAllAsReadButton"
import { ProfileMenu } from "./ProfileMenu" import { ProfileMenu } from "./ProfileMenu"
function HeaderDivider() { function HeaderDivider() {
@@ -14,46 +14,17 @@ function HeaderDivider() {
} }
const iconSize = 18 const iconSize = 18
export function Header() { export function Header() {
const source = useAppSelector(state => state.entries.source)
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
const entriesTimestamp = useAppSelector(state => state.entries.timestamp)
const settings = useAppSelector(state => state.user.settings) const settings = useAppSelector(state => state.user.settings)
const profile = useAppSelector(state => state.user.profile) const profile = useAppSelector(state => state.user.profile)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const openMarkAllEntriesModal = () =>
openConfirmModal({
title: t`Mark all entries as read`,
children: (
<Text size="sm">
<Trans>
Are you sure you want to mark all entries of <Code>{sourceLabel}</Code> as read?
</Trans>
</Text>
),
labels: { confirm: t`Confirm`, cancel: t`Cancel` },
confirmProps: { color: "red" },
onConfirm: () =>
dispatch(
markAllEntries({
sourceType: source.type,
req: {
id: source.id,
read: true,
olderThan: entriesTimestamp,
},
})
),
})
if (!settings) return <Loader /> if (!settings) return <Loader />
return ( return (
<Center> <Center>
<Group> <Group>
<ActionButton icon={<TbRefresh size={iconSize} />} label={t`Refresh`} onClick={() => dispatch(reloadEntries())} /> <ActionButton icon={<TbRefresh size={iconSize} />} label={t`Refresh`} onClick={() => dispatch(reloadEntries())} />
<ActionButton icon={<TbChecks size={iconSize} />} label={t`Mark all as read`} onClick={openMarkAllEntriesModal} /> <MarkAllAsReadButton iconSize={iconSize} />
<HeaderDivider /> <HeaderDivider />

View File

@@ -0,0 +1,83 @@
import { t, Trans } from "@lingui/macro"
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
import { markAllEntries } from "app/slices/entries"
import { useAppDispatch, useAppSelector } from "app/store"
import { ActionButton } from "components/ActionButtton"
import { useState } from "react"
import { TbChecks } from "react-icons/tb"
export function MarkAllAsReadButton(props: { iconSize: number }) {
const [opened, setOpened] = useState(false)
const [threshold, setThreshold] = useState(0)
const source = useAppSelector(state => state.entries.source)
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
const entriesTimestamp = useAppSelector(state => state.entries.timestamp) ?? Date.now()
const dispatch = useAppDispatch()
return (
<>
<Modal opened={opened} onClose={() => setOpened(false)} title={t`Mark all entries as read`}>
<Stack>
<Text size="sm">
{threshold === 0 && (
<Trans>
Are you sure you want to mark all entries of <Code>{sourceLabel}</Code> as read?
</Trans>
)}
{threshold > 0 && (
<Trans>
Are you sure you want to mark entries older than {threshold} days of <Code>{sourceLabel}</Code> as read?
</Trans>
)}
</Text>
<Slider
py="xl"
min={0}
max={28}
marks={[
{ value: 0, label: "0" },
{ value: 7, label: "7" },
{ value: 14, label: "14" },
{ value: 21, label: "21" },
{ value: 28, label: "28" },
]}
value={threshold}
onChange={setThreshold}
/>
<Group position="right">
<Button variant="default" onClick={() => setOpened(false)}>
<Trans>Cancel</Trans>
</Button>
<Button
color="red"
onClick={() => {
setOpened(false)
dispatch(
markAllEntries({
sourceType: source.type,
req: {
id: source.id,
read: true,
olderThan: entriesTimestamp - threshold * 24 * 60 * 60 * 1000,
},
})
)
}}
>
<Trans>Confirm</Trans>
</Button>
</Group>
</Stack>
</Modal>
<ActionButton
icon={<TbChecks size={props.iconSize} />}
label={t`Mark all as read`}
onClick={() => {
setThreshold(0)
setOpened(true)
}}
/>
</>
)
}

View File

@@ -86,10 +86,14 @@ msgstr "Are you sure you want to delete user <0>{userName}</0> ?"
msgid "Are you sure you want to delete your account? There's no turning back!" msgid "Are you sure you want to delete your account? There's no turning back!"
msgstr "Are you sure you want to delete your account? There's no turning back!" msgstr "Are you sure you want to delete your account? There's no turning back!"
#: src/components/header/Header.tsx #: src/components/header/MarkAllAsReadButton.tsx
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?" msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
msgstr "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?" msgstr "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
#: src/components/header/MarkAllAsReadButton.tsx
msgid "Are you sure you want to mark entries older than {threshold} days of <0>{sourceLabel}</0> as read?"
msgstr "Are you sure you want to mark entries older than {threshold} days of <0>{sourceLabel}</0> as read?"
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Are you sure you want to unsubscribe from <0>{feedName}</0>?" msgid "Are you sure you want to unsubscribe from <0>{feedName}</0>?"
msgstr "Are you sure you want to unsubscribe from <0>{feedName}</0>?" msgstr "Are you sure you want to unsubscribe from <0>{feedName}</0>?"
@@ -117,7 +121,7 @@ msgstr "Browser extentions"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
#: src/components/header/Header.tsx #: src/components/header/MarkAllAsReadButton.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -156,7 +160,7 @@ msgstr "CommaFeed next unread item"
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})"
msgstr "CommaFeed version {version} ({revision})" msgstr "CommaFeed version {version} ({revision})"
#: src/components/header/Header.tsx #: src/components/header/MarkAllAsReadButton.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -371,11 +375,11 @@ msgstr "Logout"
msgid "Manage users" msgid "Manage users"
msgstr "Manage users" msgstr "Manage users"
#: src/components/header/Header.tsx #: src/components/header/MarkAllAsReadButton.tsx
msgid "Mark all as read" msgid "Mark all as read"
msgstr "Mark all as read" msgstr "Mark all as read"
#: src/components/header/Header.tsx #: src/components/header/MarkAllAsReadButton.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Mark all entries as read" msgid "Mark all entries as read"
msgstr "Mark all entries as read" msgstr "Mark all entries as read"

View File

@@ -86,10 +86,14 @@ msgstr "Etes-vous sûr de vouloir supprimer l'utilisateur <0>{userName}</0> ?"
msgid "Are you sure you want to delete your account? There's no turning back!" msgid "Are you sure you want to delete your account? There's no turning back!"
msgstr "Êtes-vous sûr de vouloir supprimer définitivement votre compte ?" msgstr "Êtes-vous sûr de vouloir supprimer définitivement votre compte ?"
#: src/components/header/Header.tsx #: src/components/header/MarkAllAsReadButton.tsx
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?" msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
msgstr "Etes-vous sûr de vouloir marquer toutes les entrées de <0>{sourceLabel}</0> comme lues?" msgstr "Etes-vous sûr de vouloir marquer toutes les entrées de <0>{sourceLabel}</0> comme lues?"
#: src/components/header/MarkAllAsReadButton.tsx
msgid "Are you sure you want to mark entries older than {threshold} days of <0>{sourceLabel}</0> as read?"
msgstr "Etes-vous sûr de vouloir marquer les entrées de <0>{sourceLabel}</0> plus anciennes que {threshold} jours comme lues?"
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Are you sure you want to unsubscribe from <0>{feedName}</0>?" msgid "Are you sure you want to unsubscribe from <0>{feedName}</0>?"
msgstr "Etes-vous sûr de vouloir vous désabonner de <0>{feedName}</0>?" msgstr "Etes-vous sûr de vouloir vous désabonner de <0>{feedName}</0>?"
@@ -117,7 +121,7 @@ msgstr "Extensions pour navigateurs"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
#: src/components/header/Header.tsx #: src/components/header/MarkAllAsReadButton.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -156,7 +160,7 @@ msgstr "CommaFeed prochain article non lu"
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})"
msgstr "CommaFeed version {version} ({revision})" msgstr "CommaFeed version {version} ({revision})"
#: src/components/header/Header.tsx #: src/components/header/MarkAllAsReadButton.tsx
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
@@ -371,11 +375,11 @@ msgstr "Déconnexion"
msgid "Manage users" msgid "Manage users"
msgstr "Gestion des utilisateurs" msgstr "Gestion des utilisateurs"
#: src/components/header/Header.tsx #: src/components/header/MarkAllAsReadButton.tsx
msgid "Mark all as read" msgid "Mark all as read"
msgstr "Tout marquer comme lu" msgstr "Tout marquer comme lu"
#: src/components/header/Header.tsx #: src/components/header/MarkAllAsReadButton.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Mark all entries as read" msgid "Mark all entries as read"
msgstr "Marquer toutes les entrées comme lues" msgstr "Marquer toutes les entrées comme lues"