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 (

Updates

Stay up-to-date with the latest browser features.
We'd love to hear your feedback!

{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) && ( {range(0, 3).map((n) => ( n ? "active" : undefined} /> ))} )} {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; } }