diff --git a/components.json b/components.json index c4f4502719..af85921aed 100644 --- a/components.json +++ b/components.json @@ -6,6 +6,7 @@ "src/components/views/rooms/RoomTile.tsx": "src/components/views/rooms/RoomTile.tsx", "src/components/views/elements/RoomName.tsx": "src/components/views/elements/RoomName.tsx", "src/components/views/dialogs/spotlight/PublicRoomResultDetails.tsx": "src/components/views/dialogs/spotlight/PublicRoomResultDetails.tsx", + "src/components/views/avatars/BaseAvatar.tsx": "src/components/views/avatars/BaseAvatar.tsx", "src/editor/commands.tsx": "src/editor/commands.tsx", "src/hooks/useRoomName.ts": "src/hooks/useRoomName.ts" } diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx new file mode 100644 index 0000000000..11c17d0115 --- /dev/null +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -0,0 +1,150 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019, 2020 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 React, { useCallback, useContext, useEffect, useState } from "react"; +import classNames from "classnames"; +import { ClientEvent } from "matrix-js-sdk/src/matrix"; +import { Avatar } from "@vector-im/compound-web"; +import SettingsStore from "matrix-react-sdk/src/settings/SettingsStore"; +import { ButtonEvent } from "matrix-react-sdk/src/components/views/elements/AccessibleButton"; +import RoomContext from "matrix-react-sdk/src/contexts/RoomContext"; +import MatrixClientContext from "matrix-react-sdk/src/contexts/MatrixClientContext"; +import { useTypedEventEmitter } from "matrix-react-sdk/src/hooks/useEventEmitter"; +import { _t } from "matrix-react-sdk/src/languageHandler"; + +import { getSafeRoomName } from "../../../hooks/useRoomName"; + +interface IProps { + name?: React.ComponentProps["name"]; // The name (first initial used as default) + idName?: React.ComponentProps["id"]; // ID for generating hash colours + title?: string; // onHover title text + url?: string | null; // highest priority of them all, shortcut to set in urls[0] + urls?: string[]; // [highest_priority, ... , lowest_priority] + type?: React.ComponentProps["type"]; + size: string; + onClick?: (ev: ButtonEvent) => void; + inputRef?: React.RefObject; + className?: string; + tabIndex?: number; + altText?: string; +} + +const calculateUrls = (url?: string | null, urls?: string[], lowBandwidth = false): string[] => { + // work out the full set of urls to try to load. This is formed like so: + // imageUrls: [ props.url, ...props.urls ] + + let _urls: string[] = []; + if (!lowBandwidth) { + _urls = urls || []; + + if (url) { + // copy urls and put url first + _urls = [url, ..._urls]; + } + } + + // deduplicate URLs + return Array.from(new Set(_urls)); +}; + +const useImageUrl = ({ url, urls }: { url?: string | null; urls?: string[] }): [string, () => void] => { + // Since this is a hot code path and the settings store can be slow, we + // use the cached lowBandwidth value from the room context if it exists + const roomContext = useContext(RoomContext); + const lowBandwidth = roomContext ? roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth"); + + const [imageUrls, setUrls] = useState(calculateUrls(url, urls, lowBandwidth)); + const [urlsIndex, setIndex] = useState(0); + + const onError = useCallback(() => { + setIndex((i) => i + 1); // try the next one + }, []); + + useEffect(() => { + setUrls(calculateUrls(url, urls, lowBandwidth)); + setIndex(0); + }, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps + + const cli = useContext(MatrixClientContext); + const onClientSync = useCallback((syncState, prevState) => { + // Consider the client reconnected if there is no error with syncing. + // This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP. + const reconnected = syncState !== "ERROR" && prevState !== syncState; + if (reconnected) { + setIndex(0); + } + }, []); + useTypedEventEmitter(cli, ClientEvent.Sync, onClientSync); + + const imageUrl = imageUrls[urlsIndex]; + return [imageUrl, onError]; +}; + +const BaseAvatar: React.FC = (props) => { + const { + name, + idName, + title, + url, + urls, + size = "40px", + onClick, + inputRef, + className, + type = "round", + altText = _t("common|avatar"), + ...otherProps + } = props; + + const [imageUrl, onError] = useImageUrl({ url, urls }); + + const extraProps: Partial> = {}; + + if (onClick) { + extraProps["aria-live"] = "off"; + extraProps["role"] = "button"; + } else if (!imageUrl) { + extraProps["role"] = "presentation"; + extraProps["aria-label"] = undefined; + } else { + extraProps["role"] = undefined; + } + + return ( + + ); +}; + +export default BaseAvatar; +export type BaseAvatarType = React.FC; diff --git a/src/hooks/useRoomName.ts b/src/hooks/useRoomName.ts index 01066f67c2..cf347292d4 100644 --- a/src/hooks/useRoomName.ts +++ b/src/hooks/useRoomName.ts @@ -4,6 +4,15 @@ import { _t } from "matrix-react-sdk/src/languageHandler"; import { IOOBData } from "matrix-react-sdk/src/stores/ThreepidInviteStore"; import { useMemo } from "react"; +/** + * Removes the [TG] prefix and leading whitespace from a room name + * @param roomName + * @returns {string} + */ +export function getSafeRoomName(roomName?: string): string { + return roomName?.replace(/^(\s|\[TG\])*/, "") || ""; +} + /** * Determines the room name from a combination of the room model and potential * @param room - The room model @@ -20,9 +29,9 @@ export function getRoomName(room?: Room | IPublicRoomsChunkRoom, oobName?: IOOBD ) || _t("common|unnamed_room"); - return (roomName || "") - .replace(":", ":\u200b") // add a zero-width space to allow linewrapping after the colon (matrix defaults) - .replace("[TG]", ""); + return getSafeRoomName( + (roomName || "").replace(":", ":\u200b"), // add a zero-width space to allow linewrapping after the colon (matrix defaults) + ); } /**