import Container from "../../ui/atoms/container";
import useSWR from "swr";
import { DocMetadata } from "../../../../libs/types/document";
import { FeatureId, MDN_PLUS_TITLE } from "../../constants";
import { useLocale, useScrollToTop, useViewedState } from "../../hooks";
import { Button } from "../../ui/atoms/button";
import { Icon } from "../../ui/atoms/icon";
import { Loading } from "../../ui/atoms/loading";
import Mandala from "../../ui/molecules/mandala";
import { Paginator } from "../../ui/molecules/paginator";
import BookmarkMenu from "../../ui/organisms/article-actions/bookmark-menu";
import { useUserData } from "../../user-context";
import { camelWrap, range } from "../../utils";
import { Event, Group, useUpdates } from "./api";
import "./index.scss";
import { useGleanClick } from "../../telemetry/glean-context";
import { PLUS_UPDATES } from "../../telemetry/constants";
import SearchFilter, { AnyFilter, AnySort } from "../search-filter";
import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { DataError } from "../common";
import { useCollections } from "../collections/api";
import React from "react";
const LazyCompatTable = React.lazy(
() => import("../../lit/compat/lazy-compat-table.js")
);
type EventWithStatus = Event & { status: Status };
type Status = "added" | "removed";
const CATEGORY_TO_NAME = {
api: "Web APIs",
css: "CSS",
html: "HTML",
http: "HTTP",
javascript: "JavaScript",
mathml: "MathML",
svg: "SVG",
webdriver: "WebDriver",
webextensions: "Web Extensions",
};
// At some point, these should come from the API
// or from @mdn/browser-compat-data directly.
const BROWSERS = {
chrome: "Chrome",
chrome_android: "Chrome Android",
deno: "Deno",
edge: "Edge",
firefox: "Firefox",
firefox_android: "Firefox for Android",
ie: "Internet Explorer",
nodejs: "Node.js",
opera: "Opera",
opera_android: "Opera Android",
safari: "Safari",
safari_ios: "Safari on iOS",
samsunginternet_android: "Samsung Internet",
webview_android: "WebView Android",
};
const FILTERS: AnyFilter[] = [
{
type: "select",
multiple: {
encode: (...values: string[]) => values.join(","),
decode: (value: string) => value.split(","),
},
label: "Browser",
key: "browsers",
options: Object.entries(BROWSERS).map(([value, label]) => ({
label,
value,
})),
},
{
type: "select",
multiple: {
encode: (...values: string[]) => values.join(","),
decode: (value: string) => value.split(","),
},
label: "Category",
key: "category",
options: Object.entries(CATEGORY_TO_NAME)
.sort(([, a], [, b]) => a.localeCompare(b))
.map(([value, label]) => ({
label,
value,
})),
},
{
type: "select",
label: "Collections",
key: "collections",
multiple: {
encode: (...values: string[]) => values.join(","),
decode: (value: string) => value.split(","),
},
options: [],
},
];
const SORTS: AnySort[] = [
{
label: "Newest",
param: "sort=desc",
isDefault: true,
},
{
label: "Oldest",
param: "sort=asc",
},
];
export default function Updates() {
return ;
}
const useFilters = (canFilter: boolean) => {
const [filters, setFilters] = useState(FILTERS);
const { data, isLoading, error } = useCollections();
useEffect(() => {
if (!isLoading && data?.length && !error) {
setFilters((old) =>
old.map((val) => {
if (val.key === "collections") {
return {
...val,
options: data
?.filter((collection) => collection.article_count > 0)
.map((info) => {
const label =
info.name === "Default" ? "Saved Articles" : info.name;
return {
label,
value: info.id,
};
}),
};
} else {
return val;
}
})
);
}
}, [isLoading, canFilter, data, error]);
return filters;
};
function UpdatesLayout() {
document.title = `Updates | ${MDN_PLUS_TITLE}`;
useScrollToTop();
const user = useUserData();
const { data, error } = useUpdates();
const gleanClick = useGleanClick();
const [searchParams, setSearchParams] = useSearchParams();
const { setViewed } = useViewedState();
useEffect(() => setViewed(FeatureId.PLUS_UPDATES_V2));
const hasFilters = [...searchParams.keys()].some((key) => key !== "page");
const canFilter = user?.isAuthenticated === true;
const filters = useFilters(canFilter);
return (
{canFilter && (
gleanClick(
`${PLUS_UPDATES.FILTER_CHANGE}_${key}: ${
oldValue ?? "(default)"
} -> ${newValue ?? "(default)"}`
)
}
/>
)}
{canFilter && hasFilters && (
)}
{error && }
{data ? (
<>
{data.data.length ? (
data.data.map((group) => (
))
) : (
{hasFilters
? "No updates match your filters."
: "No updates found."}
)}
gleanClick(`${PLUS_UPDATES.PAGE_CHANGE}: ${oldPage} -> ${page}`)
}
/>
>
) : (
)}
);
}
function GroupComponent({ group }: { group: Group }) {
const { release_date, events, browser, version, name } = group;
const length = events.added.length + events.removed.length;
const metadata = {
icon: browserToIconName(browser),
title: `${name} ${version}`,
};
const allEvents = [
...events.added.map((e) => ({ status: "added", ...e })),
...events.removed.map((e) => ({ status: "removed", ...e })),
].sort((a, b) => a.path.localeCompare(b.path)) as EventWithStatus[];
return metadata ? (
{metadata.title}
{length} {length === 1 ? "update" : "updates"}
{collapseEvents(allEvents).map((event) => (
))}
) : null;
}
function collapseEvents(events: T[]): T[] {
return events.filter(
(event) =>
events.findIndex(
(e) => e.path === event.path.split(".").slice(0, -1).join(".")
) === -1
);
}
function EventComponent({ event, status }: { event: Event; status: Status }) {
const [isOpen, setIsOpen] = useState(false);
const [category, ...displayPath] = event.path.split(".");
const engines = event.compat.engines;
const gleanClick = useGleanClick();
return (
{
if (target instanceof HTMLDetailsElement) {
setIsOpen(target.open);
const source = target.open
? PLUS_UPDATES.EVENT_EXPAND
: PLUS_UPDATES.EVENT_COLLAPSE;
gleanClick(source);
}
}}
>
{camelWrap(displayPath.join("."))}
{CATEGORY_TO_NAME[category]}
{Boolean(engines.length) && (
)}
{isOpen && }
);
}
function EventStatus({ status }: { status: Status }) {
return {status};
}
function EventInnerComponent({
event: {
path,
compat: { mdn_url },
},
}: {
event: Event;
}) {
const locale = useLocale();
return (
);
}
function ArticleActions({ path, mdn_url }: { path: string; mdn_url?: string }) {
const userData = useUserData();
const locale = useLocale();
const url = mdn_url?.replace("https://developer.mozilla.org", `/${locale}`);
const searchUrl = `/${locale}/search?sort=relevance&locale=en-US${
locale !== "en-US" ? `&locale=${locale}` : ""
}&q=${encodeURIComponent(path)}`;
const { data: doc } = useSWR(
() => userData?.isAuthenticated && url && `${url}/metadata.json`,
async (url) => {
const response = await fetch(url);
if (!response.ok) {
throw Error(response.statusText);
}
return (await response.json()) as DocMetadata;
},
{
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);
return (
);
}
function browserToIconName(browser: string) {
if (browser.startsWith("firefox")) {
return "simple-firefox";
} else if (browser === "webview_android") {
return "webview";
} else if (browser === "webview_ios") {
return "safari";
} else {
const browserStart = browser.split("_")[0];
return browserStart;
}
}