diff --git a/src/components/views/beacon/BeaconMarker.tsx b/src/components/views/beacon/BeaconMarker.tsx index 217be7351e7..6ac29c8a24e 100644 --- a/src/components/views/beacon/BeaconMarker.tsx +++ b/src/components/views/beacon/BeaconMarker.tsx @@ -20,7 +20,7 @@ import { Beacon, BeaconEvent, LocationAssetType } from "matrix-js-sdk/src/matrix import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; -import SmartMarker from "../location/SmartMarker"; +import { SmartMarker } from "../location"; interface Props { map: maplibregl.Map; diff --git a/src/components/views/beacon/BeaconViewDialog.tsx b/src/components/views/beacon/BeaconViewDialog.tsx index a5d79a472f0..227ba221b7e 100644 --- a/src/components/views/beacon/BeaconViewDialog.tsx +++ b/src/components/views/beacon/BeaconViewDialog.tsx @@ -36,7 +36,7 @@ import MapFallback from "../location/MapFallback"; import { MapError } from "../location/MapError"; import { LocationShareError } from "../../../utils/location"; -interface IProps { +export interface IProps { roomId: Room["roomId"]; matrixClient: MatrixClient; // open the map centered on this beacon's location diff --git a/src/components/views/beacon/index.tsx b/src/components/views/beacon/index.tsx new file mode 100644 index 00000000000..77119edbdd4 --- /dev/null +++ b/src/components/views/beacon/index.tsx @@ -0,0 +1,31 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Exports beacon components which touch maplibre-gs wrapped in React Suspense to enable code splitting + +import React, { ComponentProps, lazy, Suspense } from "react"; + +import Spinner from "../elements/Spinner"; + +const BeaconViewDialogComponent = lazy(() => import("./BeaconViewDialog")); + +export function BeaconViewDialog(props: ComponentProps): JSX.Element { + return ( + }> + + + ); +} diff --git a/src/components/views/elements/SyntaxHighlight.tsx b/src/components/views/elements/SyntaxHighlight.tsx index 3262ce5d6c0..caa7ceb51c8 100644 --- a/src/components/views/elements/SyntaxHighlight.tsx +++ b/src/components/views/elements/SyntaxHighlight.tsx @@ -16,22 +16,23 @@ limitations under the License. */ import React from "react"; -import hljs from "highlight.js"; -interface IProps { +import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; + +interface Props { language?: string; children: string; } -export default class SyntaxHighlight extends React.PureComponent { - public render(): React.ReactNode { - const { children: content, language } = this.props; - const highlighted = language ? hljs.highlight(content, { language }) : hljs.highlightAuto(content); - - return ( -
-                
-            
- ); - } +export default function SyntaxHighlight({ children, language }: Props): JSX.Element { + const highlighted = useAsyncMemo(async () => { + const { default: highlight } = await import("highlight.js"); + return language ? highlight.highlight(children, { language }) : highlight.highlightAuto(children); + }, [language, children]); + + return ( +
+            {highlighted ?  : children}
+        
+ ); } diff --git a/src/components/views/location/LocationButton.tsx b/src/components/views/location/LocationButton.tsx index fe9cf056b4e..aab595e1f97 100644 --- a/src/components/views/location/LocationButton.tsx +++ b/src/components/views/location/LocationButton.tsx @@ -24,7 +24,7 @@ import { aboveLeftOf, useContextMenu, MenuProps } from "../../structures/Context import { OverflowMenuContext } from "../rooms/MessageComposerButtons"; import LocationShareMenu from "./LocationShareMenu"; -interface IProps { +export interface IProps { roomId: string; sender: RoomMember; menuPosition?: MenuProps; diff --git a/src/components/views/location/Map.tsx b/src/components/views/location/Map.tsx index 8eda492b302..7d726da494b 100644 --- a/src/components/views/location/Map.tsx +++ b/src/components/views/location/Map.tsx @@ -139,7 +139,7 @@ const onGeolocateError = (e: GeolocationPositionError): void => { }); }; -interface MapProps { +export interface MapProps { id: string; interactive?: boolean; /** diff --git a/src/components/views/location/SmartMarker.tsx b/src/components/views/location/SmartMarker.tsx index f082bce8d80..27f6eb5d032 100644 --- a/src/components/views/location/SmartMarker.tsx +++ b/src/components/views/location/SmartMarker.tsx @@ -18,7 +18,8 @@ import React, { ReactNode, useCallback, useEffect, useState } from "react"; import * as maplibregl from "maplibre-gl"; import { RoomMember } from "matrix-js-sdk/src/matrix"; -import { createMarker, parseGeoUri } from "../../../utils/location"; +import { parseGeoUri } from "../../../utils/location"; +import { createMarker } from "../../../utils/location/map"; import Marker from "./Marker"; const useMapMarker = ( @@ -66,7 +67,7 @@ const useMapMarker = ( }; }; -interface SmartMarkerProps { +export interface SmartMarkerProps { map: maplibregl.Map; geoUri: string; id?: string; diff --git a/src/components/views/location/index.tsx b/src/components/views/location/index.tsx new file mode 100644 index 00000000000..3df2289d64b --- /dev/null +++ b/src/components/views/location/index.tsx @@ -0,0 +1,71 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Exports location components which touch maplibre-gs wrapped in React Suspense to enable code splitting + +import React, { ComponentProps, lazy, Suspense } from "react"; + +import Spinner from "../elements/Spinner"; + +const MapComponent = lazy(() => import("./Map")); + +export function Map(props: ComponentProps): JSX.Element { + return ( + }> + + + ); +} + +const LocationPickerComponent = lazy(() => import("./LocationPicker")); + +export function LocationPicker(props: ComponentProps): JSX.Element { + return ( + }> + + + ); +} + +const SmartMarkerComponent = lazy(() => import("./SmartMarker")); + +export function SmartMarker(props: ComponentProps): JSX.Element { + return ( + }> + + + ); +} + +const LocationButtonComponent = lazy(() => import("./LocationButton")); + +export function LocationButton(props: ComponentProps): JSX.Element { + return ( + }> + + + ); +} + +const LocationViewDialogComponent = lazy(() => import("./LocationViewDialog")); + +export function LocationViewDialog(props: ComponentProps): JSX.Element { + return ( + }> + + + ); +} diff --git a/src/components/views/messages/MBeaconBody.tsx b/src/components/views/messages/MBeaconBody.tsx index d5a8609376f..16f85559d3f 100644 --- a/src/components/views/messages/MBeaconBody.tsx +++ b/src/components/views/messages/MBeaconBody.tsx @@ -38,12 +38,11 @@ import { isSelfLocation, LocationShareError } from "../../../utils/location"; import { BeaconDisplayStatus, getBeaconDisplayStatus } from "../beacon/displayStatus"; import BeaconStatus from "../beacon/BeaconStatus"; import OwnBeaconStatus from "../beacon/OwnBeaconStatus"; -import Map from "../location/Map"; +import { Map, SmartMarker } from "../location"; import { MapError } from "../location/MapError"; import MapFallback from "../location/MapFallback"; -import SmartMarker from "../location/SmartMarker"; import { GetRelationsForEvent } from "../rooms/EventTile"; -import BeaconViewDialog from "../beacon/BeaconViewDialog"; +import { BeaconViewDialog } from "../beacon"; import { IBodyProps } from "./IBodyProps"; const useBeaconState = ( diff --git a/src/components/views/messages/MLocationBody.tsx b/src/components/views/messages/MLocationBody.tsx index 8b2dc2e95d2..29c1c97e1a5 100644 --- a/src/components/views/messages/MLocationBody.tsx +++ b/src/components/views/messages/MLocationBody.tsx @@ -29,9 +29,7 @@ import { import MatrixClientContext from "../../../contexts/MatrixClientContext"; import TooltipTarget from "../elements/TooltipTarget"; import { Alignment } from "../elements/Tooltip"; -import LocationViewDialog from "../location/LocationViewDialog"; -import Map from "../location/Map"; -import SmartMarker from "../location/SmartMarker"; +import { SmartMarker, Map, LocationViewDialog } from "../location"; import { IBodyProps } from "./IBodyProps"; import { createReconnectedListener } from "../../../utils/connection"; diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index cbb23f791d2..a3bac0b9cd7 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -16,7 +16,6 @@ limitations under the License. import React, { createRef, SyntheticEvent, MouseEvent } from "react"; import ReactDOM from "react-dom"; -import highlight from "highlight.js"; import { MsgType } from "matrix-js-sdk/src/matrix"; import { TooltipProvider } from "@vector-im/compound-web"; @@ -238,7 +237,9 @@ export default class TextualBody extends React.Component { pre.append(document.createElement("span")); } - private highlightCode(code: HTMLElement): void { + private async highlightCode(code: HTMLElement): Promise { + const { default: highlight } = await import("highlight.js"); + if (code.textContent && code.textContent.length > MAX_HIGHLIGHT_LENGTH) { console.log( "Code block is bigger than highlight limit (" + diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index d9d364a2110..7f23efbce2b 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -24,7 +24,7 @@ import { CollapsibleButton } from "./CollapsibleButton"; import { MenuProps } from "../../structures/ContextMenu"; import dis from "../../../dispatcher/dispatcher"; import ErrorDialog from "../dialogs/ErrorDialog"; -import LocationButton from "../location/LocationButton"; +import { LocationButton } from "../location"; import Modal from "../../../Modal"; import PollCreateDialog from "../elements/PollCreateDialog"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; diff --git a/src/utils/location/index.ts b/src/utils/location/index.ts index 035fe526942..8a107451c6f 100644 --- a/src/utils/location/index.ts +++ b/src/utils/location/index.ts @@ -18,6 +18,6 @@ export * from "./findMapStyleUrl"; export * from "./isSelfLocation"; export * from "./locationEventGeoUri"; export * from "./LocationShareErrors"; -export * from "./map"; +export * from "./links"; export * from "./parseGeoUri"; export * from "./positionFailureMessage"; diff --git a/src/utils/location/links.ts b/src/utils/location/links.ts new file mode 100644 index 00000000000..cafae1ae1a9 --- /dev/null +++ b/src/utils/location/links.ts @@ -0,0 +1,47 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixEvent, M_LOCATION } from "matrix-js-sdk/src/matrix"; + +import { parseGeoUri } from "./parseGeoUri"; + +export const makeMapSiteLink = (coords: GeolocationCoordinates): string => { + return ( + "https://www.openstreetmap.org/" + + `?mlat=${coords.latitude}` + + `&mlon=${coords.longitude}` + + `#map=16/${coords.latitude}/${coords.longitude}` + ); +}; + +export const createMapSiteLinkFromEvent = (event: MatrixEvent): string | null => { + const content = event.getContent(); + const mLocation = content[M_LOCATION.name]; + if (mLocation !== undefined) { + const uri = mLocation["uri"]; + if (uri !== undefined) { + const geoCoords = parseGeoUri(uri); + return geoCoords ? makeMapSiteLink(geoCoords) : null; + } + } else { + const geoUri = content["geo_uri"]; + if (geoUri) { + const geoCoords = parseGeoUri(geoUri); + return geoCoords ? makeMapSiteLink(geoCoords) : null; + } + } + return null; +}; diff --git a/src/utils/location/map.ts b/src/utils/location/map.ts index 78f17c9868a..707d703bea5 100644 --- a/src/utils/location/map.ts +++ b/src/utils/location/map.ts @@ -15,11 +15,10 @@ limitations under the License. */ import * as maplibregl from "maplibre-gl"; -import { MatrixClient, MatrixEvent, M_LOCATION } from "matrix-js-sdk/src/matrix"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../languageHandler"; -import { parseGeoUri } from "./parseGeoUri"; import { findMapStyleUrl } from "./findMapStyleUrl"; import { LocationShareError } from "./LocationShareErrors"; @@ -75,31 +74,3 @@ export const createMarker = (coords: GeolocationCoordinates, element: HTMLElemen }).setLngLat({ lon: coords.longitude, lat: coords.latitude }); return marker; }; - -export const makeMapSiteLink = (coords: GeolocationCoordinates): string => { - return ( - "https://www.openstreetmap.org/" + - `?mlat=${coords.latitude}` + - `&mlon=${coords.longitude}` + - `#map=16/${coords.latitude}/${coords.longitude}` - ); -}; - -export const createMapSiteLinkFromEvent = (event: MatrixEvent): string | null => { - const content = event.getContent(); - const mLocation = content[M_LOCATION.name]; - if (mLocation !== undefined) { - const uri = mLocation["uri"]; - if (uri !== undefined) { - const geoCoords = parseGeoUri(uri); - return geoCoords ? makeMapSiteLink(geoCoords) : null; - } - } else { - const geoUri = content["geo_uri"]; - if (geoUri) { - const geoCoords = parseGeoUri(geoUri); - return geoCoords ? makeMapSiteLink(geoCoords) : null; - } - } - return null; -}; diff --git a/src/utils/location/useMap.ts b/src/utils/location/useMap.ts index f6fc0aa62d0..98ec53ffde1 100644 --- a/src/utils/location/useMap.ts +++ b/src/utils/location/useMap.ts @@ -15,8 +15,8 @@ limitations under the License. */ import { useEffect, useState } from "react"; -import { Map as MapLibreMap } from "maplibre-gl"; +import type { Map as MapLibreMap } from "maplibre-gl"; import { createMap } from "./map"; import { useMatrixClientContext } from "../../contexts/MatrixClientContext"; diff --git a/test/components/views/elements/SyntaxHighlight-test.tsx b/test/components/views/elements/SyntaxHighlight-test.tsx index bdd3e50cf02..2f8c751fd7c 100644 --- a/test/components/views/elements/SyntaxHighlight-test.tsx +++ b/test/components/views/elements/SyntaxHighlight-test.tsx @@ -15,22 +15,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { render } from "@testing-library/react"; +import { render, waitFor } from "@testing-library/react"; import hljs, { type HighlightOptions } from "highlight.js"; import React from "react"; import SyntaxHighlight from "../../../../src/components/views/elements/SyntaxHighlight"; describe("", () => { - it("renders", () => { + it("renders", async () => { const { container } = render(console.log("Hello, World!");); + await waitFor(() => expect(container.querySelector(".language-arcade")).toBeTruthy()); expect(container).toMatchSnapshot(); }); - it.each(["json", "javascript", "css"])("uses the provided language", (lang) => { + it.each(["json", "javascript", "css"])("uses the provided language", async (lang) => { const mock = jest.spyOn(hljs, "highlight"); - render(// Hello, World); + const { container } = render(// Hello, World); + await waitFor(() => expect(container.querySelector(`.language-${lang}`)).toBeTruthy()); const [_lang, opts] = mock.mock.lastCall!; expect((opts as HighlightOptions)["language"]).toBe(lang);