Skip to content

Commit 810fb6e

Browse files
committed
next/news: add optional until field to automatically expire a story -- fixes #8485
1 parent 9ee6185 commit 810fb6e

File tree

13 files changed

+148
-72
lines changed

13 files changed

+148
-72
lines changed

src/.claude/settings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@
2727
"WebFetch(domain:www.anthropic.com)",
2828
"WebFetch(domain:mistral.ai)",
2929
"Bash(pnpm i18n:*)",
30-
"WebFetch(domain:simplelocalize.io)"
30+
"WebFetch(domain:simplelocalize.io)",
31+
"Bash(pnpm list:*)",
32+
"Bash(pnpm why:*)",
33+
"mcp__github__get_issue"
3134
],
3235
"deny": []
3336
}

src/packages/database/postgres/news.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ export function clearCache(): void {
2323
const Q_FEED = `
2424
SELECT
2525
id, channel, title, text, url,
26-
extract(epoch from date::timestamp)::integer as date
26+
extract(epoch from date::timestamp)::integer as date,
27+
extract(epoch from until::timestamp)::integer as until
2728
FROM news
2829
WHERE news.date <= NOW()
2930
AND hide IS NOT TRUE
3031
AND channel != '${EVENT_CHANNEL}'
32+
AND (until IS NULL OR until > NOW())
3133
ORDER BY date DESC
3234
LIMIT 100`;
3335

@@ -40,7 +42,8 @@ export async function getFeedData(): Promise<NewsItem[]> {
4042
const Q_BY_ID = `
4143
SELECT
4244
id, channel, title, text, url, hide, tags,
43-
extract(epoch from date::timestamptz)::INTEGER as date
45+
extract(epoch from date::timestamptz)::INTEGER as date,
46+
extract(epoch from until::timestamptz)::INTEGER as until
4447
FROM news
4548
WHERE id = $1`;
4649

@@ -56,7 +59,9 @@ const Q_BY_ID_USER = `
5659
SELECT
5760
id, channel, title, text, url, hide, tags, history,
5861
date >= NOW() as future,
59-
extract(epoch from date::timestamptz)::INTEGER as date
62+
until IS NOT NULL AND until <= NOW() as expired,
63+
extract(epoch from date::timestamptz)::INTEGER as date,
64+
extract(epoch from until::timestamptz)::INTEGER as until
6065
FROM news
6166
WHERE id = $1`;
6267

@@ -68,6 +73,7 @@ WHERE date >= (SELECT date FROM news WHERE id = $1)
6873
AND hide IS NOT TRUE
6974
AND date < NOW()
7075
AND channel != '${EVENT_CHANNEL}'
76+
AND (until IS NULL OR until > NOW())
7177
ORDER BY date ASC, id ASC
7278
LIMIT 1`;
7379

@@ -79,6 +85,7 @@ WHERE date <= (SELECT date FROM news WHERE id = $1)
7985
AND hide IS NOT TRUE
8086
AND date < NOW()
8187
AND channel != '${EVENT_CHANNEL}'
88+
AND (until IS NULL OR until > NOW())
8289
ORDER BY date DESC, id DESC
8390
LIMIT 1`;
8491

@@ -104,7 +111,9 @@ const Q_INDEX = `
104111
SELECT
105112
id, channel, title, text, url, hide, tags,
106113
date >= NOW() as future,
107-
extract(epoch from date::timestamptz)::INTEGER as date
114+
until IS NOT NULL AND until <= NOW() as expired,
115+
extract(epoch from date::timestamptz)::INTEGER as date,
116+
extract(epoch from until::timestamptz)::INTEGER as until
108117
FROM news
109118
WHERE channel <> '${EVENT_CHANNEL}'
110119
ORDER BY date DESC
@@ -122,11 +131,13 @@ export async function getIndex(
122131
const Q_MOST_RECENT = `
123132
SELECT
124133
id, channel, title, tags,
125-
extract(epoch from date::timestamptz)::INTEGER as date
134+
extract(epoch from date::timestamptz)::INTEGER as date,
135+
extract(epoch from until::timestamptz)::INTEGER as until
126136
FROM news
127137
WHERE date <= NOW()
128138
AND hide IS NOT TRUE
129139
AND channel != '${EVENT_CHANNEL}'
140+
AND (until IS NULL OR until > NOW())
130141
ORDER BY date DESC
131142
LIMIT 1`;
132143

@@ -137,11 +148,13 @@ export async function getMostRecentNews(): Promise<RecentHeadline | null> {
137148
const Q_RECENT = `
138149
SELECT
139150
id, channel, title, tags,
140-
extract(epoch from date::timestamptz)::INTEGER as date
151+
extract(epoch from date::timestamptz)::INTEGER as date,
152+
extract(epoch from until::timestamptz)::INTEGER as until
141153
FROM news
142154
WHERE date <= NOW()
143155
AND channel != '${EVENT_CHANNEL}'
144156
AND hide IS NOT TRUE
157+
AND (until IS NULL OR until > NOW())
145158
ORDER BY date DESC
146159
LIMIT $1`;
147160

@@ -158,11 +171,13 @@ export async function getRecentHeadlines(
158171
const Q_UPCOMING_NEWS_CHANNEL_ITEMS = `
159172
SELECT
160173
id, channel, title, text, url, tags,
161-
extract(epoch from date::timestamp)::integer as date
174+
extract(epoch from date::timestamp)::integer as date,
175+
extract(epoch from until::timestamp)::integer as until
162176
FROM news
163177
WHERE date >= NOW()
164178
AND channel = $1
165179
AND hide IS NOT TRUE
180+
AND (until IS NULL OR until > NOW())
166181
ORDER BY date
167182
LIMIT 100`;
168183

@@ -176,11 +191,13 @@ export async function getUpcomingNewsChannelItems(
176191
const Q_PAST_NEWS_CHANNEL_ITEMS = `
177192
SELECT
178193
id, channel, title, text, url, tags,
179-
extract(epoch from date::timestamp)::integer as date
194+
extract(epoch from date::timestamp)::integer as date,
195+
extract(epoch from until::timestamp)::integer as until
180196
FROM news
181197
WHERE date <= NOW()
182198
AND channel = $1
183199
AND hide IS NOT TRUE
200+
AND (until IS NULL OR until > NOW())
184201
ORDER BY date DESC
185202
LIMIT 100`;
186203

src/packages/next/components/news/news.tsx

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
import { Alert, Button, Card, Flex, Space, Tag, Tooltip } from "antd";
77
import { useRouter } from "next/router";
88
import { Fragment } from "react";
9+
import TimeAgo from "timeago-react";
910

1011
import { Icon, IconName } from "@cocalc/frontend/components/icon";
1112
import Markdown from "@cocalc/frontend/editors/slate/static-markdown";
13+
import { KUCALC_COCALC_COM } from "@cocalc/util/db-schema/site-defaults";
1214
import {
1315
capitalize,
1416
getRandomColor,
@@ -24,20 +26,18 @@ import {
2426
} from "@cocalc/util/types/news";
2527
import { CSS, Paragraph, Text, Title } from "components/misc";
2628
import A from "components/misc/A";
27-
import TimeAgo from "timeago-react";
28-
import { useDateStr } from "./useDateStr";
2929
import { useCustomize } from "lib/customize";
30-
import { KUCALC_COCALC_COM } from "@cocalc/util/db-schema/site-defaults";
3130
import { SocialMediaShareLinks } from "../landing/social-media-share-links";
31+
import { useDateStr } from "./useDateStr";
3232

3333
const STYLE: CSS = {
3434
borderColor: COLORS.GRAY_M,
3535
boxShadow: "0 0 0 1px rgba(0,0,0,.1), 0 3px 3px rgba(0,0,0,.3)",
3636
} as const;
3737

3838
interface Props {
39-
// NewsWithFuture with optional future property
40-
news: NewsItem & { future?: boolean };
39+
// NewsWithStatus with optional future and expired properties
40+
news: NewsItem & { future?: boolean; expired?: boolean };
4141
dns?: string;
4242
showEdit?: boolean;
4343
small?: boolean; // limit height, essentially
@@ -55,7 +55,19 @@ export function News(props: Props) {
5555
historyMode = false,
5656
onTagClick,
5757
} = props;
58-
const { id, url, tags, title, date, channel, text, future, hide } = news;
58+
const {
59+
id,
60+
url,
61+
tags,
62+
title,
63+
date,
64+
channel,
65+
text,
66+
future,
67+
hide,
68+
expired,
69+
until,
70+
} = news;
5971
const dateStr = useDateStr(news, historyMode);
6072
const permalink = slugURL(news);
6173
const { kucalc, siteURL } = useCustomize();
@@ -183,6 +195,28 @@ export function News(props: Props) {
183195
}
184196
}
185197

198+
function renderExpired() {
199+
if (expired) {
200+
return (
201+
<Alert
202+
banner
203+
type="warning"
204+
message={
205+
<>
206+
Expired news item, not shown to users.
207+
{typeof until === "number" && (
208+
<>
209+
{" "}
210+
Expired <TimeAgo datetime={new Date(1000 * until)} />.
211+
</>
212+
)}
213+
</>
214+
}
215+
/>
216+
);
217+
}
218+
}
219+
186220
function renderTags() {
187221
return <TagList mode="news" tags={tags} onTagClick={onTagClick} />;
188222
}
@@ -269,6 +303,7 @@ export function News(props: Props) {
269303
</Title>
270304
{renderFuture()}
271305
{renderHidden()}
306+
{renderExpired()}
272307
<Markdown value={text} style={{ ...style, minHeight: "20vh" }} />
273308

274309
<Flex align="baseline" justify="space-between" wrap="wrap">
@@ -303,6 +338,7 @@ export function News(props: Props) {
303338
>
304339
{renderFuture()}
305340
{renderHidden()}
341+
{renderExpired()}
306342
<Markdown value={text} style={style} />
307343
</Card>
308344
</>

src/packages/next/components/news/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { NewsItem } from "@cocalc/util/types/news";
77

8-
export interface NewsWithFuture extends NewsItem {
8+
export interface NewsWithStatus extends NewsItem {
99
future: boolean;
10+
expired?: boolean;
1011
}

src/packages/next/pages/about/events.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import IndexList, { DataSource } from "components/landing/index-list";
1818
import { CSS } from "components/misc";
1919
import A from "components/misc/A";
2020
import { TagList } from "components/news/news";
21-
import type { NewsWithFuture } from "components/news/types";
21+
import type { NewsWithStatus } from "components/news/types";
2222
import { useDateStr } from "components/news/useDateStr";
2323

2424
import { MAX_WIDTH } from "lib/config";
@@ -31,7 +31,7 @@ const BODY_STYLE: CSS = {
3131
overflowY: "auto",
3232
} as const;
3333
interface TitleComponentProps {
34-
newsItem: NewsWithFuture;
34+
newsItem: NewsWithStatus;
3535
showHelpTicket?: boolean;
3636
}
3737

@@ -81,8 +81,8 @@ const TitleComponent = ({ newsItem, showHelpTicket }: TitleComponentProps) => (
8181

8282
interface EventsProps {
8383
customize: CustomizeType;
84-
upcomingEvents: NewsWithFuture[];
85-
pastEvents: NewsWithFuture[];
84+
upcomingEvents: NewsWithStatus[];
85+
pastEvents: NewsWithStatus[];
8686
}
8787

8888
export default function Events({

src/packages/next/pages/api/v2/news/edit.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export default async function handle(req: Request, res: Response) {
2525
}
2626

2727
async function doIt(req: Request) {
28-
// date is unix timestamp in seconds
29-
const { id, title, text, date, channel, url, tags, hide } = getParams(req);
28+
// date and until are unix timestamps in seconds
29+
const { id, title, text, date, channel, url, tags, hide, until } = getParams(req);
3030

3131
const account_id = await getAccountId(req);
3232

@@ -55,5 +55,6 @@ async function doIt(req: Request) {
5555
date: date ? new Date(1000 * date) : new Date(),
5656
channel,
5757
hide: !!hide,
58+
until: until ? new Date(1000 * until) : undefined,
5859
});
5960
}

src/packages/next/pages/news/[id].tsx

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import Head from "components/landing/head";
2121
import Header from "components/landing/header";
2222
import A from "components/misc/A";
2323
import { News } from "components/news/news";
24-
import { NewsWithFuture } from "components/news/types";
24+
import { NewsWithStatus } from "components/news/types";
2525
import { useDateStr } from "components/news/useDateStr";
2626
import Loading from "components/share/loading";
2727
import { MAX_WIDTH, NOT_FOUND } from "lib/config";
@@ -32,7 +32,7 @@ import withCustomize from "lib/with-customize";
3232

3333
interface Props {
3434
customize: CustomizeType;
35-
news: NewsWithFuture;
35+
news: NewsWithStatus;
3636
prev?: NewsPrevNext;
3737
next?: NewsPrevNext;
3838
metadata: {
@@ -42,12 +42,14 @@ interface Props {
4242
image: string;
4343
published: string;
4444
modified: string;
45-
}
45+
};
4646
}
4747

48-
const formatNewsTime = (newsDate: NewsWithFuture['date']) => (
49-
typeof newsDate === "number" ? dayjs.unix(newsDate) : dayjs(newsDate)
50-
).toISOString();
48+
const formatNewsTime = (newsDate: NewsWithStatus["date"]) =>
49+
(typeof newsDate === "number"
50+
? dayjs.unix(newsDate)
51+
: dayjs(newsDate)
52+
).toISOString();
5153

5254
export default function NewsPage(props: Props) {
5355
const { customize, news, prev, next, metadata } = props;
@@ -146,17 +148,17 @@ export default function NewsPage(props: Props) {
146148
<Customize value={customize}>
147149
<Head title={title} />
148150
<NextHead>
149-
<meta property="og:type" content="article"/>
151+
<meta property="og:type" content="article" />
150152

151-
<meta property="og:title" content={metadata.title}/>
152-
<meta property="og:url" content={metadata.url}/>
153-
<meta property="og:image" content={metadata.image}/>
153+
<meta property="og:title" content={metadata.title} />
154+
<meta property="og:url" content={metadata.url} />
155+
<meta property="og:image" content={metadata.image} />
154156

155-
<meta property="article:published_time" content={metadata.published}/>
156-
<meta property="article:modified_time" content={metadata.modified}/>
157+
<meta property="article:published_time" content={metadata.published} />
158+
<meta property="article:modified_time" content={metadata.modified} />
157159
</NextHead>
158160
<Layout>
159-
<Header/>
161+
<Header />
160162
<Layout.Content
161163
style={{
162164
backgroundColor: "white",
@@ -197,10 +199,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
197199
// Extract image URL from parsed Markdown. By converting to HTML first, we
198200
// automatically add support for HTML that's been embedded into Markdown.
199201
//
200-
const $markdown = markdown_to_cheerio(news.text)
201-
const imgSrc = $markdown('img')
202-
.first()
203-
.attr('src');
202+
const $markdown = markdown_to_cheerio(news.text);
203+
const imgSrc = $markdown("img").first().attr("src");
204204

205205
// Format published time
206206
//
@@ -219,10 +219,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
219219
? formatNewsTime(newsModificationTimestamps[0])
220220
: publishedTime;
221221

222-
const metadata: Props['metadata'] = {
222+
const metadata: Props["metadata"] = {
223223
title: news.title,
224224
url: `${siteURL}${slugURL(news)}`,
225-
image: imgSrc || '',
225+
image: imgSrc || "",
226226
published: publishedTime,
227227
modified: modifiedTime,
228228
author: `${siteName}`,

src/packages/next/pages/news/[id]/[timestamp].tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import Head from "components/landing/head";
1717
import Header from "components/landing/header";
1818
import A from "components/misc/A";
1919
import { News } from "components/news/news";
20-
import { NewsWithFuture } from "components/news/types";
20+
import { NewsWithStatus } from "components/news/types";
2121
import { useDateStr } from "components/news/useDateStr";
2222
import { MAX_WIDTH, NOT_FOUND } from "lib/config";
2323
import { Customize, CustomizeType } from "lib/customize";
@@ -27,7 +27,7 @@ import withCustomize from "lib/with-customize";
2727

2828
interface Props {
2929
customize: CustomizeType;
30-
news: NewsWithFuture;
30+
news: NewsWithStatus;
3131
timestamp: number; // unix epoch in seconds
3232
prev?: number;
3333
next?: number;

0 commit comments

Comments
 (0)