Skip to content

next/news: add optional until field to automatically expire a story #8508

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/.claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
"WebFetch(domain:www.anthropic.com)",
"WebFetch(domain:mistral.ai)",
"Bash(pnpm i18n:*)",
"WebFetch(domain:simplelocalize.io)"
"WebFetch(domain:simplelocalize.io)",
"Bash(pnpm list:*)",
"Bash(pnpm why:*)",
"mcp__github__get_issue"
],
"deny": []
}
Expand Down
33 changes: 25 additions & 8 deletions src/packages/database/postgres/news.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ export function clearCache(): void {
const Q_FEED = `
SELECT
id, channel, title, text, url,
extract(epoch from date::timestamp)::integer as date
extract(epoch from date::timestamp)::integer as date,
extract(epoch from until::timestamp)::integer as until
FROM news
WHERE news.date <= NOW()
AND hide IS NOT TRUE
AND channel != '${EVENT_CHANNEL}'
AND (until IS NULL OR until > NOW())
ORDER BY date DESC
LIMIT 100`;

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

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

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

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

Expand All @@ -104,7 +111,9 @@ const Q_INDEX = `
SELECT
id, channel, title, text, url, hide, tags,
date >= NOW() as future,
extract(epoch from date::timestamptz)::INTEGER as date
until IS NOT NULL AND until <= NOW() as expired,
extract(epoch from date::timestamptz)::INTEGER as date,
extract(epoch from until::timestamptz)::INTEGER as until
FROM news
WHERE channel <> '${EVENT_CHANNEL}'
ORDER BY date DESC
Expand All @@ -122,11 +131,13 @@ export async function getIndex(
const Q_MOST_RECENT = `
SELECT
id, channel, title, tags,
extract(epoch from date::timestamptz)::INTEGER as date
extract(epoch from date::timestamptz)::INTEGER as date,
extract(epoch from until::timestamptz)::INTEGER as until
FROM news
WHERE date <= NOW()
AND hide IS NOT TRUE
AND channel != '${EVENT_CHANNEL}'
AND (until IS NULL OR until > NOW())
ORDER BY date DESC
LIMIT 1`;

Expand All @@ -137,11 +148,13 @@ export async function getMostRecentNews(): Promise<RecentHeadline | null> {
const Q_RECENT = `
SELECT
id, channel, title, tags,
extract(epoch from date::timestamptz)::INTEGER as date
extract(epoch from date::timestamptz)::INTEGER as date,
extract(epoch from until::timestamptz)::INTEGER as until
FROM news
WHERE date <= NOW()
AND channel != '${EVENT_CHANNEL}'
AND hide IS NOT TRUE
AND (until IS NULL OR until > NOW())
ORDER BY date DESC
LIMIT $1`;

Expand All @@ -158,11 +171,13 @@ export async function getRecentHeadlines(
const Q_UPCOMING_NEWS_CHANNEL_ITEMS = `
SELECT
id, channel, title, text, url, tags,
extract(epoch from date::timestamp)::integer as date
extract(epoch from date::timestamp)::integer as date,
extract(epoch from until::timestamp)::integer as until
FROM news
WHERE date >= NOW()
AND channel = $1
AND hide IS NOT TRUE
AND (until IS NULL OR until > NOW())
ORDER BY date
LIMIT 100`;

Expand All @@ -176,11 +191,13 @@ export async function getUpcomingNewsChannelItems(
const Q_PAST_NEWS_CHANNEL_ITEMS = `
SELECT
id, channel, title, text, url, tags,
extract(epoch from date::timestamp)::integer as date
extract(epoch from date::timestamp)::integer as date,
extract(epoch from until::timestamp)::integer as until
FROM news
WHERE date <= NOW()
AND channel = $1
AND hide IS NOT TRUE
AND (until IS NULL OR until > NOW())
ORDER BY date DESC
LIMIT 100`;

Expand Down
48 changes: 42 additions & 6 deletions src/packages/next/components/news/news.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import { Alert, Button, Card, Flex, Space, Tag, Tooltip } from "antd";
import { useRouter } from "next/router";
import { Fragment } from "react";
import TimeAgo from "timeago-react";

import { Icon, IconName } from "@cocalc/frontend/components/icon";
import Markdown from "@cocalc/frontend/editors/slate/static-markdown";
import { KUCALC_COCALC_COM } from "@cocalc/util/db-schema/site-defaults";
import {
capitalize,
getRandomColor,
Expand All @@ -24,20 +26,18 @@ import {
} from "@cocalc/util/types/news";
import { CSS, Paragraph, Text, Title } from "components/misc";
import A from "components/misc/A";
import TimeAgo from "timeago-react";
import { useDateStr } from "./useDateStr";
import { useCustomize } from "lib/customize";
import { KUCALC_COCALC_COM } from "@cocalc/util/db-schema/site-defaults";
import { SocialMediaShareLinks } from "../landing/social-media-share-links";
import { useDateStr } from "./useDateStr";

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

interface Props {
// NewsWithFuture with optional future property
news: NewsItem & { future?: boolean };
// NewsWithStatus with optional future and expired properties
news: NewsItem & { future?: boolean; expired?: boolean };
dns?: string;
showEdit?: boolean;
small?: boolean; // limit height, essentially
Expand All @@ -55,7 +55,19 @@ export function News(props: Props) {
historyMode = false,
onTagClick,
} = props;
const { id, url, tags, title, date, channel, text, future, hide } = news;
const {
id,
url,
tags,
title,
date,
channel,
text,
future,
hide,
expired,
until,
} = news;
const dateStr = useDateStr(news, historyMode);
const permalink = slugURL(news);
const { kucalc, siteURL } = useCustomize();
Expand Down Expand Up @@ -183,6 +195,28 @@ export function News(props: Props) {
}
}

function renderExpired() {
if (expired) {
return (
<Alert
banner
type="warning"
message={
<>
Expired news item, not shown to users.
{typeof until === "number" && (
<>
{" "}
Expired <TimeAgo datetime={new Date(1000 * until)} />.
</>
)}
</>
}
/>
);
}
}

function renderTags() {
return <TagList mode="news" tags={tags} onTagClick={onTagClick} />;
}
Expand Down Expand Up @@ -269,6 +303,7 @@ export function News(props: Props) {
</Title>
{renderFuture()}
{renderHidden()}
{renderExpired()}
<Markdown value={text} style={{ ...style, minHeight: "20vh" }} />

<Flex align="baseline" justify="space-between" wrap="wrap">
Expand Down Expand Up @@ -303,6 +338,7 @@ export function News(props: Props) {
>
{renderFuture()}
{renderHidden()}
{renderExpired()}
<Markdown value={text} style={style} />
</Card>
</>
Expand Down
3 changes: 2 additions & 1 deletion src/packages/next/components/news/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

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

export interface NewsWithFuture extends NewsItem {
export interface NewsWithStatus extends NewsItem {
future: boolean;
expired?: boolean;
}
9 changes: 5 additions & 4 deletions src/packages/next/pages/about/events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import IndexList, { DataSource } from "components/landing/index-list";
import { CSS } from "components/misc";
import A from "components/misc/A";
import { TagList } from "components/news/news";
import type { NewsWithFuture } from "components/news/types";
import type { NewsWithStatus } from "components/news/types";
import { useDateStr } from "components/news/useDateStr";

import { MAX_WIDTH } from "lib/config";
Expand All @@ -30,8 +30,9 @@ const BODY_STYLE: CSS = {
maxHeight: "max(300px, 75vh)",
overflowY: "auto",
} as const;

interface TitleComponentProps {
newsItem: NewsWithFuture;
newsItem: NewsWithStatus;
showHelpTicket?: boolean;
}

Expand Down Expand Up @@ -81,8 +82,8 @@ const TitleComponent = ({ newsItem, showHelpTicket }: TitleComponentProps) => (

interface EventsProps {
customize: CustomizeType;
upcomingEvents: NewsWithFuture[];
pastEvents: NewsWithFuture[];
upcomingEvents: NewsWithStatus[];
pastEvents: NewsWithStatus[];
}

export default function Events({
Expand Down
5 changes: 3 additions & 2 deletions src/packages/next/pages/api/v2/news/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export default async function handle(req: Request, res: Response) {
}

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

const account_id = await getAccountId(req);

Expand Down Expand Up @@ -55,5 +55,6 @@ async function doIt(req: Request) {
date: date ? new Date(1000 * date) : new Date(),
channel,
hide: !!hide,
until: until ? new Date(1000 * until) : undefined,
});
}
38 changes: 19 additions & 19 deletions src/packages/next/pages/news/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import Head from "components/landing/head";
import Header from "components/landing/header";
import A from "components/misc/A";
import { News } from "components/news/news";
import { NewsWithFuture } from "components/news/types";
import { NewsWithStatus } from "components/news/types";
import { useDateStr } from "components/news/useDateStr";
import Loading from "components/share/loading";
import { MAX_WIDTH, NOT_FOUND } from "lib/config";
Expand All @@ -32,7 +32,7 @@ import withCustomize from "lib/with-customize";

interface Props {
customize: CustomizeType;
news: NewsWithFuture;
news: NewsWithStatus;
prev?: NewsPrevNext;
next?: NewsPrevNext;
metadata: {
Expand All @@ -42,12 +42,14 @@ interface Props {
image: string;
published: string;
modified: string;
}
};
}

const formatNewsTime = (newsDate: NewsWithFuture['date']) => (
typeof newsDate === "number" ? dayjs.unix(newsDate) : dayjs(newsDate)
).toISOString();
const formatNewsTime = (newsDate: NewsWithStatus["date"]) =>
(typeof newsDate === "number"
? dayjs.unix(newsDate)
: dayjs(newsDate)
).toISOString();

export default function NewsPage(props: Props) {
const { customize, news, prev, next, metadata } = props;
Expand Down Expand Up @@ -146,17 +148,17 @@ export default function NewsPage(props: Props) {
<Customize value={customize}>
<Head title={title} />
<NextHead>
<meta property="og:type" content="article"/>
<meta property="og:type" content="article" />

<meta property="og:title" content={metadata.title}/>
<meta property="og:url" content={metadata.url}/>
<meta property="og:image" content={metadata.image}/>
<meta property="og:title" content={metadata.title} />
<meta property="og:url" content={metadata.url} />
<meta property="og:image" content={metadata.image} />

<meta property="article:published_time" content={metadata.published}/>
<meta property="article:modified_time" content={metadata.modified}/>
<meta property="article:published_time" content={metadata.published} />
<meta property="article:modified_time" content={metadata.modified} />
</NextHead>
<Layout>
<Header/>
<Header />
<Layout.Content
style={{
backgroundColor: "white",
Expand Down Expand Up @@ -197,10 +199,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
// Extract image URL from parsed Markdown. By converting to HTML first, we
// automatically add support for HTML that's been embedded into Markdown.
//
const $markdown = markdown_to_cheerio(news.text)
const imgSrc = $markdown('img')
.first()
.attr('src');
const $markdown = markdown_to_cheerio(news.text);
const imgSrc = $markdown("img").first().attr("src");

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

const metadata: Props['metadata'] = {
const metadata: Props["metadata"] = {
title: news.title,
url: `${siteURL}${slugURL(news)}`,
image: imgSrc || '',
image: imgSrc || "",
published: publishedTime,
modified: modifiedTime,
author: `${siteName}`,
Expand Down
Loading