Skip to content

Commit 1696d36

Browse files
authored
Use countdown timer for better delivery estimates in Logs tab (#425)
1 parent 004368e commit 1696d36

File tree

11 files changed

+186
-55
lines changed

11 files changed

+186
-55
lines changed

services/backend-api/client/src/features/feed/components/UserFeedLogs/DeliveryPreview/ArticleDeliveryDetails.tsx

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,15 @@ import { pages } from "../../../../../constants";
2525
import { FeedConnectionType } from "../../../../../types";
2626
import { DeliveryChecksModal } from "./DeliveryChecksModal";
2727
import { getOutcomeColorScheme } from "./deliveryPreviewUtils";
28-
import { formatRefreshRateSeconds } from "../../../../../utils/formatRefreshRateSeconds";
28+
import {
29+
formatRefreshRateSeconds,
30+
getEffectiveRefreshRateSeconds,
31+
getNextCheckText,
32+
} from "../../../../../utils/formatRefreshRateSeconds";
2933

3034
interface Props {
3135
result: ArticleDeliveryResult;
36+
lastRequestAtUnix?: number;
3237
}
3338

3439
const getOutcomeLabel = (outcome: ArticleDeliveryOutcome): string => {
@@ -60,10 +65,18 @@ const getOutcomeLabel = (outcome: ArticleDeliveryOutcome): string => {
6065
}
6166
};
6267

63-
const getExplanationText = (outcome: ArticleDeliveryOutcome): string => {
68+
const getExplanationText = (
69+
outcome: ArticleDeliveryOutcome,
70+
refreshRateSeconds: number,
71+
lastRequestAtUnix?: number,
72+
): string => {
73+
const refreshRateText = `Your feed checks for new content every ${formatRefreshRateSeconds(refreshRateSeconds)}.`;
74+
const nextCheckText = getNextCheckText(lastRequestAtUnix, refreshRateSeconds);
75+
const timingText = nextCheckText ? `${refreshRateText} ${nextCheckText}` : refreshRateText;
76+
6477
switch (outcome) {
6578
case ArticleDeliveryOutcome.WouldDeliver:
66-
return "This article would be delivered to Discord when the feed is next processed.";
79+
return `This article would be delivered to Discord when the feed is next processed. ${timingText}`;
6780
case ArticleDeliveryOutcome.FirstRunBaseline:
6881
return "This feed is in its learning phase. MonitoRSS skips pre-existing articles to avoid flooding your channel with old content.";
6982
case ArticleDeliveryOutcome.DuplicateId:
@@ -79,7 +92,7 @@ const getExplanationText = (outcome: ArticleDeliveryOutcome): string => {
7992
case ArticleDeliveryOutcome.RateLimitedMedium:
8093
return "This connection has reached its delivery limit. The article will be delivered automatically once the limit resets.";
8194
case ArticleDeliveryOutcome.WouldDeliverPassingComparison:
82-
return "This article was seen before, but one of your Passing Comparison fields changed, so it will be re-delivered as an update.";
95+
return `This article was seen before, but one of your Passing Comparison fields changed, so it will be re-delivered as an update. ${timingText}`;
8396
case ArticleDeliveryOutcome.FeedUnchanged:
8497
return "The feed's content hasn't changed since it was last checked. MonitoRSS skips unchanged feeds to save resources. New articles will be detected automatically once the publisher has indicated that there are new changes.";
8598
case ArticleDeliveryOutcome.FeedError:
@@ -134,7 +147,7 @@ const ConnectionResultRow = ({ mediumResult }: ConnectionResultRowProps) => {
134147
);
135148
};
136149

137-
export const ArticleDeliveryDetails = ({ result }: Props) => {
150+
export const ArticleDeliveryDetails = ({ result, lastRequestAtUnix }: Props) => {
138151
const { userFeed } = useUserFeedContext();
139152
const { isOpen, onOpen, onClose } = useDisclosure();
140153

@@ -148,19 +161,23 @@ export const ArticleDeliveryDetails = ({ result }: Props) => {
148161
return uniqueOutcomes.size > 1;
149162
};
150163

164+
const effectiveRefreshRateSeconds = getEffectiveRefreshRateSeconds(userFeed);
165+
151166
const getDisplayText = () => {
152167
if (isLearningPhase) {
153168
const plural = connectionCount !== 1 ? "s" : "";
154-
const formattedTime = formatRefreshRateSeconds(userFeed.refreshRateSeconds);
169+
const formattedTime = formatRefreshRateSeconds(effectiveRefreshRateSeconds);
170+
const nextCheckText = getNextCheckText(lastRequestAtUnix, effectiveRefreshRateSeconds);
171+
const nextCheckSuffix = nextCheckText ? ` ${nextCheckText}` : "";
155172

156-
return `Skipped (Learning Phase): This article existed before the feed was added. MonitoRSS skips pre-existing articles to avoid flooding your channel with old content. New articles will be delivered to all ${connectionCount} connection${plural} once learning completes (within ${formattedTime}).`;
173+
return `Skipped (Learning Phase): This article existed before the feed was added. MonitoRSS skips pre-existing articles to avoid flooding your channel with old content. New articles will be delivered to all ${connectionCount} connection${plural} once learning completes (within ${formattedTime}).${nextCheckSuffix}`;
157174
}
158175

159176
if (hasPartialDelivery()) {
160177
return "This article would deliver to some connections but not others.";
161178
}
162179

163-
return getExplanationText(result.outcome);
180+
return getExplanationText(result.outcome, effectiveRefreshRateSeconds, lastRequestAtUnix);
164181
};
165182

166183
return (

services/backend-api/client/src/features/feed/components/UserFeedLogs/DeliveryPreview/DeliveryChecksModal.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ import {
3232
RepeatIcon,
3333
WarningIcon,
3434
} from "@chakra-ui/icons";
35-
import { formatRefreshRateSeconds } from "../../../../../utils/formatRefreshRateSeconds";
35+
import {
36+
formatRefreshRateSeconds,
37+
getEffectiveRefreshRateSeconds,
38+
} from "../../../../../utils/formatRefreshRateSeconds";
3639
import {
3740
ArticleDeliveryOutcome,
3841
ArticleDeliveryResult,
@@ -869,9 +872,7 @@ export const DeliveryChecksModal = ({ isOpen, onClose, result, initialMediumId }
869872
<ModalCloseButton />
870873
<ModalBody>
871874
{isLearningPhase && (
872-
<LearningPhaseContent
873-
refreshRateSeconds={userFeed.userRefreshRateSeconds || userFeed.refreshRateSeconds}
874-
/>
875+
<LearningPhaseContent refreshRateSeconds={getEffectiveRefreshRateSeconds(userFeed)} />
875876
)}
876877
{isFeedUnchanged && <FeedUnchangedContent />}
877878
{isFeedError && <FeedErrorContent outcomeReason={result.outcomeReason} />}

services/backend-api/client/src/features/feed/components/UserFeedLogs/DeliveryPreview/DeliveryPreviewAccordion.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,14 @@ const getStatusBorderColor = (outcome: ArticleDeliveryOutcome): string => {
4747
interface DeliveryPreviewAccordionItemProps {
4848
result: ArticleDeliveryResult;
4949
isFirst: boolean;
50+
lastRequestAtUnix?: number;
5051
}
5152

52-
const DeliveryPreviewAccordionItem = ({ result, isFirst }: DeliveryPreviewAccordionItemProps) => {
53+
const DeliveryPreviewAccordionItem = ({
54+
result,
55+
isFirst,
56+
lastRequestAtUnix,
57+
}: DeliveryPreviewAccordionItemProps) => {
5358
const displayOutcome = getOutcomeLabel(result.outcome);
5459
const colorScheme = getOutcomeColorScheme(result.outcome);
5560
const borderColor = getStatusBorderColor(result.outcome);
@@ -81,24 +86,29 @@ const DeliveryPreviewAccordionItem = ({ result, isFirst }: DeliveryPreviewAccord
8186
<AccordionIcon ml={2} />
8287
</AccordionButton>
8388
<AccordionPanel p={0}>
84-
<ArticleDeliveryDetails result={result} />
89+
<ArticleDeliveryDetails result={result} lastRequestAtUnix={lastRequestAtUnix} />
8590
</AccordionPanel>
8691
</AccordionItem>
8792
);
8893
};
8994

9095
interface DeliveryPreviewAccordionProps {
9196
results: ArticleDeliveryResult[];
97+
lastRequestAtUnix?: number;
9298
}
9399

94-
export const DeliveryPreviewAccordion = ({ results }: DeliveryPreviewAccordionProps) => (
100+
export const DeliveryPreviewAccordion = ({
101+
results,
102+
lastRequestAtUnix,
103+
}: DeliveryPreviewAccordionProps) => (
95104
<Box {...ARTICLE_LIST_CONTAINER_PROPS}>
96105
<Accordion allowMultiple>
97106
{results.map((result, index) => (
98107
<DeliveryPreviewAccordionItem
99108
key={result.articleId}
100109
result={result}
101110
isFirst={index === 0}
111+
lastRequestAtUnix={lastRequestAtUnix}
102112
/>
103113
))}
104114
</Accordion>

services/backend-api/client/src/features/feed/components/UserFeedLogs/DeliveryPreview/index.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,26 @@ import { useUserFeedContext } from "../../../../../contexts/UserFeedContext";
2121
import { pages } from "../../../../../constants";
2222
import { UserFeedTabSearchParam } from "../../../../../constants/userFeedTabSearchParam";
2323
import { useDeliveryPreviewWithPagination } from "../../../hooks/useDeliveryPreviewWithPagination";
24+
import { useUserFeedRequests } from "../../../hooks";
2425
import { InlineErrorAlert } from "../../../../../components";
2526
import {
2627
DeliveryPreviewAccordion,
2728
DeliveryPreviewAccordionSkeleton,
2829
} from "./DeliveryPreviewAccordion";
2930
import { ArticleDeliveryOutcome, ArticleDeliveryResult } from "../../../types/DeliveryPreview";
30-
import { formatRefreshRateSeconds } from "../../../../../utils/formatRefreshRateSeconds";
31+
import {
32+
formatRefreshRateSeconds,
33+
getEffectiveRefreshRateSeconds,
34+
getNextCheckText,
35+
} from "../../../../../utils/formatRefreshRateSeconds";
3136
import { FeedLevelStateDisplay, FeedState } from "./FeedLevelStateDisplay";
3237

3338
dayjs.extend(relativeTime);
3439

3540
export const getPatternAlert = (
3641
results: ArticleDeliveryResult[],
3742
refreshRateSeconds: number,
43+
lastRequestAtUnix?: number,
3844
): { type: "info" | "warning"; message: string } | null => {
3945
if (results.length === 0) return null;
4046

@@ -59,10 +65,12 @@ export const getPatternAlert = (
5965
// First run is a feed-level state - if any article has it, all do
6066
if (outcome === ArticleDeliveryOutcome.FirstRunBaseline && count >= 1) {
6167
const formattedTime = formatRefreshRateSeconds(refreshRateSeconds);
68+
const nextCheckText = getNextCheckText(lastRequestAtUnix, refreshRateSeconds);
69+
const nextCheckSuffix = nextCheckText ? ` ${nextCheckText}` : "";
6270

6371
return {
6472
type: "info",
65-
message: `This feed is in its learning phase. MonitoRSS is identifying existing articles so it only delivers new ones. This typically completes within ${formattedTime}.`,
73+
message: `This feed is in its learning phase. MonitoRSS is identifying existing articles so it only delivers new ones. This typically completes within ${formattedTime}.${nextCheckSuffix}`,
6674
};
6775
}
6876

@@ -105,6 +113,7 @@ export interface DeliveryPreviewPresentationalProps {
105113
feedState?: FeedState | null;
106114
feedId: string;
107115
refreshRateSeconds: number;
116+
lastRequestAtUnix?: number;
108117
addConnectionUrl: string;
109118
lastCheckedFormatted?: string;
110119
onRefresh?: () => void;
@@ -122,14 +131,15 @@ export const DeliveryPreviewPresentational = ({
122131
feedState = null,
123132
feedId,
124133
refreshRateSeconds,
134+
lastRequestAtUnix,
125135
addConnectionUrl,
126136
lastCheckedFormatted = "Never",
127137
onRefresh = () => {},
128138
onLoadMore = () => {},
129139
}: DeliveryPreviewPresentationalProps) => {
130140
const hasFeedLevelState = !!feedState;
131141
const hasNoData = results.length === 0 && !isLoading && !hasFeedLevelState;
132-
const patternAlert = getPatternAlert(results, refreshRateSeconds);
142+
const patternAlert = getPatternAlert(results, refreshRateSeconds, lastRequestAtUnix);
133143

134144
return (
135145
<Stack spacing={4} mb={8} border="solid 1px" borderColor="gray.700" borderRadius="md">
@@ -225,7 +235,7 @@ export const DeliveryPreviewPresentational = ({
225235
{isFetching && results.length === 0 ? (
226236
<DeliveryPreviewAccordionSkeleton />
227237
) : (
228-
<DeliveryPreviewAccordion results={results} />
238+
<DeliveryPreviewAccordion results={results} lastRequestAtUnix={lastRequestAtUnix} />
229239
)}
230240
<Flex justifyContent="space-between" alignItems="center">
231241
<Text fontSize="sm" color="whiteAlpha.600">
@@ -266,6 +276,13 @@ export const DeliveryPreview = () => {
266276
feedId: userFeed.id,
267277
});
268278

279+
const { data: requestsData } = useUserFeedRequests({
280+
feedId: userFeed.id,
281+
data: { skip: 0, limit: 1 },
282+
});
283+
284+
const lastRequestAtUnix = requestsData?.result.requests[0]?.createdAt;
285+
269286
const activeConnections = userFeed.connections.filter((c) => !c.disabledCode);
270287
const hasNoConnections = activeConnections.length === 0;
271288
const isLoading = status === "loading";
@@ -288,7 +305,8 @@ export const DeliveryPreview = () => {
288305
hasNoConnections={hasNoConnections}
289306
feedState={feedState}
290307
feedId={userFeed.id}
291-
refreshRateSeconds={userFeed.refreshRateSeconds}
308+
refreshRateSeconds={getEffectiveRefreshRateSeconds(userFeed)}
309+
lastRequestAtUnix={lastRequestAtUnix}
292310
addConnectionUrl={pages.userFeed(userFeed.id, { tab: UserFeedTabSearchParam.Connections })}
293311
lastCheckedFormatted={formatLastChecked()}
294312
onRefresh={refresh}

services/backend-api/client/src/features/feed/components/UserFeedsTable/components/FilteredEmptyState.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
1-
import { Button, Center, Heading, Stack, Text } from "@chakra-ui/react";
1+
import { Button, Center, Divider, Heading, Stack, Text } from "@chakra-ui/react";
22
import { SearchIcon } from "@chakra-ui/icons";
33

4+
const URL_PATTERN = /^https?:\/\//;
5+
46
interface FilteredEmptyStateProps {
57
onClearAllFilters: () => void;
8+
searchTerm?: string;
9+
onSearchForNewFeed?: (term: string) => void;
610
}
711

8-
export const FilteredEmptyState: React.FC<FilteredEmptyStateProps> = ({ onClearAllFilters }) => {
12+
export const FilteredEmptyState: React.FC<FilteredEmptyStateProps> = ({
13+
onClearAllFilters,
14+
searchTerm,
15+
onSearchForNewFeed,
16+
}) => {
17+
const trimmedSearch = searchTerm?.trim();
18+
const isUrl = trimmedSearch && URL_PATTERN.test(trimmedSearch);
19+
920
return (
1021
<Center py={16}>
1122
<Stack alignItems="center" spacing={4}>
@@ -21,6 +32,26 @@ export const FilteredEmptyState: React.FC<FilteredEmptyStateProps> = ({ onClearA
2132
<Button variant="outline" onClick={onClearAllFilters}>
2233
Clear all filters
2334
</Button>
35+
{trimmedSearch && onSearchForNewFeed && (
36+
<>
37+
<Divider borderColor="whiteAlpha.300" />
38+
<Stack alignItems="center" spacing={1}>
39+
<Text fontSize="sm" color="whiteAlpha.700">
40+
{isUrl
41+
? "This looks like a feed URL."
42+
: "Can\u2019t find what you\u2019re looking for?"}
43+
</Text>
44+
<Button
45+
variant="link"
46+
colorScheme="blue"
47+
size="sm"
48+
onClick={() => onSearchForNewFeed(trimmedSearch)}
49+
>
50+
{isUrl ? "Add it as a new feed \u2192" : "Search for new feeds to add \u2192"}
51+
</Button>
52+
</Stack>
53+
</>
54+
)}
2455
</Stack>
2556
</Center>
2657
);

services/backend-api/client/src/features/feed/components/UserFeedsTable/index.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Alert, AlertIcon, Box, Center, Stack, Table, Td, Thead, Tr, Text } from "@chakra-ui/react";
22
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
3+
import { useSearchParams } from "react-router-dom";
34
import {
45
OnChangeFn,
56
RowSelectionState,
@@ -80,6 +81,19 @@ export const UserFeedsTable: React.FC = () => {
8081
onSearchChange: useCallback((s: string) => setSearch(s), [setSearch]),
8182
});
8283

84+
const [, setSearchParams] = useSearchParams();
85+
86+
const handleSearchForNewFeed = useCallback(
87+
(term: string) => {
88+
setSearchParams((prev) => {
89+
const next = new URLSearchParams(prev);
90+
next.set("addFeed", term);
91+
return next;
92+
});
93+
},
94+
[setSearchParams],
95+
);
96+
8397
// Columns with search highlighting
8498
const columns = useMemo(() => createTableColumns(search), [search]);
8599

@@ -246,7 +260,13 @@ export const UserFeedsTable: React.FC = () => {
246260
<Text>Loading feeds...</Text>
247261
</Stack>
248262
</Center>
249-
{isFilteredEmpty && <FilteredEmptyState onClearAllFilters={handleClearAllFilters} />}
263+
{isFilteredEmpty && (
264+
<FilteredEmptyState
265+
onClearAllFilters={handleClearAllFilters}
266+
searchTerm={urlSearch}
267+
onSearchForNewFeed={handleSearchForNewFeed}
268+
/>
269+
)}
250270
<Stack hidden={isInitiallyLoading || isFilteredEmpty}>
251271
<Box
252272
boxShadow="lg"

services/backend-api/client/src/features/feedConnections/components/UserFeedMiscSettingsTabSection/index.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import {
7575
} from "../../../../contexts/PageAlertContext";
7676
import { UserFeedTabSearchParam } from "../../../../constants/userFeedTabSearchParam";
7777
import ApiAdapterError from "../../../../utils/ApiAdapterError";
78+
import { getEffectiveRefreshRateSeconds } from "../../../../utils/formatRefreshRateSeconds";
7879

7980
interface Props {
8081
feedId: string;
@@ -144,7 +145,7 @@ export const UserFeedMiscSettingsTabSection = ({ feedId }: Props) => {
144145
shareManageOptions: feed?.shareManageOptions || null,
145146
userRefreshRateMinutes:
146147
(
147-
Number((feed?.userRefreshRateSeconds || feed?.refreshRateSeconds || 0).toFixed(1)) / 60
148+
Number(getEffectiveRefreshRateSeconds(feed || { refreshRateSeconds: 0 }).toFixed(1)) / 60
148149
)?.toString() || "",
149150
},
150151
});
@@ -223,11 +224,9 @@ export const UserFeedMiscSettingsTabSection = ({ feedId }: Props) => {
223224
oldArticleDateDiffMsThreshold:
224225
updatedFeed.result.dateCheckOptions?.oldArticleDateDiffMsThreshold,
225226
shareManageOptions: updatedFeed.result.shareManageOptions || null,
226-
userRefreshRateMinutes:
227-
updatedFeed.result.userRefreshRateSeconds &&
228-
!Number.isNaN(updatedFeed.result.userRefreshRateSeconds)
229-
? (updatedFeed.result.userRefreshRateSeconds / 60).toFixed(1)
230-
: (updatedFeed.result.refreshRateSeconds / 60).toFixed(1),
227+
userRefreshRateMinutes: (getEffectiveRefreshRateSeconds(updatedFeed.result) / 60).toFixed(
228+
1,
229+
),
231230
});
232231
createSuccessAlert({
233232
title: "Successfully updated feed settings",

0 commit comments

Comments
 (0)