Replace usage of forwardRef with React 19 ref prop (#29803)
* Replace usage of `forwardRef` with React 19 ref prop Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add lint rule Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
committed by
GitHub
parent
5e7b58a722
commit
22d5c00174
@@ -30,6 +30,10 @@ module.exports = {
|
|||||||
["window.innerHeight", "window.innerWidth", "window.visualViewport"],
|
["window.innerHeight", "window.innerWidth", "window.visualViewport"],
|
||||||
"Use UIStore to access window dimensions instead.",
|
"Use UIStore to access window dimensions instead.",
|
||||||
),
|
),
|
||||||
|
...buildRestrictedPropertiesOptions(
|
||||||
|
["React.forwardRef", "*.forwardRef", "forwardRef"],
|
||||||
|
"Use ref props instead.",
|
||||||
|
),
|
||||||
...buildRestrictedPropertiesOptions(
|
...buildRestrictedPropertiesOptions(
|
||||||
["*.mxcUrlToHttp", "*.getHttpUriForMxc"],
|
["*.mxcUrlToHttp", "*.getHttpUriForMxc"],
|
||||||
"Use Media helper instead to centralise access for customisation.",
|
"Use Media helper instead to centralise access for customisation.",
|
||||||
@@ -55,6 +59,11 @@ module.exports = {
|
|||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
paths: [
|
paths: [
|
||||||
|
{
|
||||||
|
name: "react",
|
||||||
|
importNames: ["forwardRef"],
|
||||||
|
message: "Use ref props instead.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "@testing-library/react",
|
name: "@testing-library/react",
|
||||||
message: "Please use jest-matrix-react instead",
|
message: "Please use jest-matrix-react instead",
|
||||||
|
|||||||
9
src/@types/react.d.ts
vendored
9
src/@types/react.d.ts
vendored
@@ -6,16 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type PropsWithChildren } from "react";
|
import { type ComponentType } from "react";
|
||||||
|
|
||||||
import type React from "react";
|
|
||||||
|
|
||||||
declare module "react" {
|
declare module "react" {
|
||||||
// Fix forwardRef types for Generic components - https://stackoverflow.com/a/58473012
|
|
||||||
function forwardRef<T, P extends object>(
|
|
||||||
render: (props: PropsWithChildren<P>, ref: React.ForwardedRef<T>) => React.ReactElement | null,
|
|
||||||
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
|
|
||||||
|
|
||||||
// Fix lazy types - https://stackoverflow.com/a/71017028
|
// Fix lazy types - https://stackoverflow.com/a/71017028
|
||||||
function lazy<T extends ComponentType<any>>(factory: () => Promise<{ default: T }>): T;
|
function lazy<T extends ComponentType<any>>(factory: () => Promise<{ default: T }>): T;
|
||||||
|
|
||||||
|
|||||||
@@ -6,18 +6,20 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { forwardRef } from "react";
|
import React, { type Ref, type JSX } from "react";
|
||||||
|
|
||||||
import { RovingTabIndexProvider } from "./RovingTabIndex";
|
import { RovingTabIndexProvider } from "./RovingTabIndex";
|
||||||
import { getKeyBindingsManager } from "../KeyBindingsManager";
|
import { getKeyBindingsManager } from "../KeyBindingsManager";
|
||||||
import { KeyBindingAction } from "./KeyboardShortcuts";
|
import { KeyBindingAction } from "./KeyboardShortcuts";
|
||||||
|
|
||||||
interface IProps extends Omit<React.HTMLProps<HTMLDivElement>, "onKeyDown"> {}
|
interface IProps extends Omit<React.HTMLProps<HTMLDivElement>, "onKeyDown"> {
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
// This component implements the Toolbar design pattern from the WAI-ARIA Authoring Practices guidelines.
|
// This component implements the Toolbar design pattern from the WAI-ARIA Authoring Practices guidelines.
|
||||||
// https://www.w3.org/TR/wai-aria-practices-1.1/#toolbar
|
// https://www.w3.org/TR/wai-aria-practices-1.1/#toolbar
|
||||||
// All buttons passed in children must use RovingTabIndex to set `onFocus`, `isActive`, `ref`
|
// All buttons passed in children must use RovingTabIndex to set `onFocus`, `isActive`, `ref`
|
||||||
const Toolbar = forwardRef<HTMLDivElement, IProps>(({ children, ...props }, ref) => {
|
const Toolbar = ({ children, ref, ...props }: IProps): JSX.Element => {
|
||||||
const onKeyDown = (ev: React.KeyboardEvent): void => {
|
const onKeyDown = (ev: React.KeyboardEvent): void => {
|
||||||
const target = ev.target as HTMLElement;
|
const target = ev.target as HTMLElement;
|
||||||
// Don't interfere with input default keydown behaviour
|
// Don't interfere with input default keydown behaviour
|
||||||
@@ -55,6 +57,6 @@ const Toolbar = forwardRef<HTMLDivElement, IProps>(({ children, ...props }, ref)
|
|||||||
)}
|
)}
|
||||||
</RovingTabIndexProvider>
|
</RovingTabIndexProvider>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
export default Toolbar;
|
export default Toolbar;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { forwardRef, type Ref } from "react";
|
import React, { type Ref, type JSX } from "react";
|
||||||
|
|
||||||
import AccessibleButton, { type ButtonProps } from "../../components/views/elements/AccessibleButton";
|
import AccessibleButton, { type ButtonProps } from "../../components/views/elements/AccessibleButton";
|
||||||
|
|
||||||
@@ -16,13 +16,19 @@ type Props<T extends keyof HTMLElementTagNameMap> = ButtonProps<T> & {
|
|||||||
label?: string;
|
label?: string;
|
||||||
// whether the context menu is currently open
|
// whether the context menu is currently open
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
|
ref?: Ref<HTMLElementTagNameMap[T]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Semantic component for representing the AccessibleButton which launches a <ContextMenu />
|
// Semantic component for representing the AccessibleButton which launches a <ContextMenu />
|
||||||
export const ContextMenuButton = forwardRef(function <T extends keyof HTMLElementTagNameMap>(
|
export const ContextMenuButton = function <T extends keyof HTMLElementTagNameMap>({
|
||||||
{ label, isExpanded, children, onClick, onContextMenu, ...props }: Props<T>,
|
label,
|
||||||
ref: Ref<HTMLElementTagNameMap[T]>,
|
isExpanded,
|
||||||
) {
|
children,
|
||||||
|
onClick,
|
||||||
|
onContextMenu,
|
||||||
|
ref,
|
||||||
|
...props
|
||||||
|
}: Props<T>): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
{...props}
|
{...props}
|
||||||
@@ -36,4 +42,4 @@ export const ContextMenuButton = forwardRef(function <T extends keyof HTMLElemen
|
|||||||
{children}
|
{children}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { forwardRef, type Ref } from "react";
|
import React, { type JSX } from "react";
|
||||||
|
|
||||||
import AccessibleButton, { type ButtonProps } from "../../components/views/elements/AccessibleButton";
|
import AccessibleButton, { type ButtonProps } from "../../components/views/elements/AccessibleButton";
|
||||||
|
|
||||||
@@ -18,10 +18,14 @@ type Props<T extends keyof HTMLElementTagNameMap> = ButtonProps<T> & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Semantic component for representing the AccessibleButton which launches a <ContextMenu />
|
// Semantic component for representing the AccessibleButton which launches a <ContextMenu />
|
||||||
export const ContextMenuTooltipButton = forwardRef(function <T extends keyof HTMLElementTagNameMap>(
|
export const ContextMenuTooltipButton = function <T extends keyof HTMLElementTagNameMap>({
|
||||||
{ isExpanded, children, onClick, onContextMenu, ...props }: Props<T>,
|
isExpanded,
|
||||||
ref: Ref<HTMLElementTagNameMap[T]>,
|
children,
|
||||||
) {
|
onClick,
|
||||||
|
onContextMenu,
|
||||||
|
ref,
|
||||||
|
...props
|
||||||
|
}: Props<T>): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
{...props}
|
{...props}
|
||||||
@@ -35,4 +39,4 @@ export const ContextMenuTooltipButton = forwardRef(function <T extends keyof HTM
|
|||||||
{children}
|
{children}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { forwardRef } from "react";
|
import React, { type Ref, type JSX } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
/* These were earlier stateless functional components but had to be converted
|
/* These were earlier stateless functional components but had to be converted
|
||||||
@@ -16,14 +16,24 @@ presumably wrap them in a <div> before rendering but I think this is the better
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
interface ITextualCompletionProps {
|
interface ITextualCompletionProps {
|
||||||
title?: string;
|
"title"?: string;
|
||||||
subtitle?: string;
|
"subtitle"?: string;
|
||||||
description?: string;
|
"description"?: string;
|
||||||
className?: string;
|
"className"?: string;
|
||||||
|
"aria-selected"?: boolean;
|
||||||
|
"ref"?: Ref<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextualCompletion = forwardRef<ITextualCompletionProps, any>((props, ref) => {
|
export const TextualCompletion = (props: ITextualCompletionProps): JSX.Element => {
|
||||||
const { title, subtitle, description, className, "aria-selected": ariaSelectedAttribute, ...restProps } = props;
|
const {
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
description,
|
||||||
|
className,
|
||||||
|
"aria-selected": ariaSelectedAttribute,
|
||||||
|
ref,
|
||||||
|
...restProps
|
||||||
|
} = props;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...restProps}
|
{...restProps}
|
||||||
@@ -37,13 +47,13 @@ export const TextualCompletion = forwardRef<ITextualCompletionProps, any>((props
|
|||||||
<span className="mx_Autocomplete_Completion_description">{description}</span>
|
<span className="mx_Autocomplete_Completion_description">{description}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
interface IPillCompletionProps extends ITextualCompletionProps {
|
interface IPillCompletionProps extends ITextualCompletionProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PillCompletion = forwardRef<IPillCompletionProps, any>((props, ref) => {
|
export const PillCompletion = (props: IPillCompletionProps): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
@@ -51,6 +61,7 @@ export const PillCompletion = forwardRef<IPillCompletionProps, any>((props, ref)
|
|||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
"aria-selected": ariaSelectedAttribute,
|
"aria-selected": ariaSelectedAttribute,
|
||||||
|
ref,
|
||||||
...restProps
|
...restProps
|
||||||
} = props;
|
} = props;
|
||||||
return (
|
return (
|
||||||
@@ -67,4 +78,4 @@ export const PillCompletion = forwardRef<IPillCompletionProps, any>((props, ref)
|
|||||||
<span className="mx_Autocomplete_Completion_description">{description}</span>
|
<span className="mx_Autocomplete_Completion_description">{description}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export default class UserProvider extends AutocompleteProvider {
|
|||||||
suffix: selection.beginning && range!.start === 0 ? ": " : " ",
|
suffix: selection.beginning && range!.start === 0 ? ": " : " ",
|
||||||
href: makeUserPermalink(user.userId),
|
href: makeUserPermalink(user.userId),
|
||||||
component: (
|
component: (
|
||||||
<PillCompletion title={displayName} description={description}>
|
<PillCompletion title={displayName} description={description ?? undefined}>
|
||||||
<MemberAvatar member={user} size="24px" />
|
<MemberAvatar member={user} size="24px" />
|
||||||
</PillCompletion>
|
</PillCompletion>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type PropsWithChildren, useEffect, useState } from "react";
|
import React, { type PropsWithChildren, useEffect, useState, type JSX } from "react";
|
||||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto-api";
|
import { CryptoEvent } from "matrix-js-sdk/src/crypto-api";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
@@ -85,7 +85,7 @@ interface Props {
|
|||||||
* A React component which exposes a {@link MatrixClientContext} and a {@link LocalDeviceVerificationStateContext}
|
* A React component which exposes a {@link MatrixClientContext} and a {@link LocalDeviceVerificationStateContext}
|
||||||
* to its children.
|
* to its children.
|
||||||
*/
|
*/
|
||||||
export function MatrixClientContextProvider(props: PropsWithChildren<Props>): React.JSX.Element {
|
export function MatrixClientContextProvider(props: PropsWithChildren<Props>): JSX.Element {
|
||||||
const verificationState = useLocalVerificationState(props.client);
|
const verificationState = useLocalVerificationState(props.client);
|
||||||
return (
|
return (
|
||||||
<MatrixClientContext.Provider value={props.client}>
|
<MatrixClientContext.Provider value={props.client}>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
import React, { type JSX, type Ref, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
type ISearchResults,
|
type ISearchResults,
|
||||||
type IThreadBundledRelationship,
|
type IThreadBundledRelationship,
|
||||||
@@ -44,269 +44,275 @@ interface Props {
|
|||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
className: string;
|
className: string;
|
||||||
onUpdate(inProgress: boolean, results: ISearchResults | null, error: Error | null): void;
|
onUpdate(inProgress: boolean, results: ISearchResults | null, error: Error | null): void;
|
||||||
|
ref?: Ref<ScrollPanel>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: todo: merge overlapping results somehow?
|
// XXX: todo: merge overlapping results somehow?
|
||||||
// XXX: why doesn't searching on name work?
|
// XXX: why doesn't searching on name work?
|
||||||
export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
export const RoomSearchView = ({
|
||||||
({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => {
|
term,
|
||||||
const client = useContext(MatrixClientContext);
|
scope,
|
||||||
const roomContext = useScopedRoomContext("showHiddenEvents");
|
promise,
|
||||||
const [highlights, setHighlights] = useState<string[] | null>(null);
|
abortController,
|
||||||
const [results, setResults] = useState<ISearchResults | null>(null);
|
resizeNotifier,
|
||||||
const aborted = useRef(false);
|
className,
|
||||||
// A map from room ID to permalink creator
|
onUpdate,
|
||||||
const permalinkCreators = useMemo(() => new Map<string, RoomPermalinkCreator>(), []);
|
inProgress,
|
||||||
const innerRef = useRef<ScrollPanel>(null);
|
ref,
|
||||||
|
}: Props): JSX.Element => {
|
||||||
|
const client = useContext(MatrixClientContext);
|
||||||
|
const roomContext = useScopedRoomContext("showHiddenEvents");
|
||||||
|
const [highlights, setHighlights] = useState<string[] | null>(null);
|
||||||
|
const [results, setResults] = useState<ISearchResults | null>(null);
|
||||||
|
const aborted = useRef(false);
|
||||||
|
// A map from room ID to permalink creator
|
||||||
|
const permalinkCreators = useMemo(() => new Map<string, RoomPermalinkCreator>(), []);
|
||||||
|
const innerRef = useRef<ScrollPanel>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
permalinkCreators.forEach((pc) => pc.stop());
|
permalinkCreators.forEach((pc) => pc.stop());
|
||||||
permalinkCreators.clear();
|
permalinkCreators.clear();
|
||||||
};
|
};
|
||||||
}, [permalinkCreators]);
|
}, [permalinkCreators]);
|
||||||
|
|
||||||
const handleSearchResult = useCallback(
|
const handleSearchResult = useCallback(
|
||||||
(searchPromise: Promise<ISearchResults>): Promise<boolean> => {
|
(searchPromise: Promise<ISearchResults>): Promise<boolean> => {
|
||||||
onUpdate(true, null, null);
|
onUpdate(true, null, null);
|
||||||
|
|
||||||
return searchPromise.then(
|
return searchPromise.then(
|
||||||
async (results): Promise<boolean> => {
|
async (results): Promise<boolean> => {
|
||||||
debuglog("search complete");
|
debuglog("search complete");
|
||||||
if (aborted.current) {
|
if (aborted.current) {
|
||||||
logger.error("Discarding stale search results");
|
logger.error("Discarding stale search results");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// postgres on synapse returns us precise details of the strings
|
// postgres on synapse returns us precise details of the strings
|
||||||
// which actually got matched for highlighting.
|
// which actually got matched for highlighting.
|
||||||
//
|
//
|
||||||
// In either case, we want to highlight the literal search term
|
// In either case, we want to highlight the literal search term
|
||||||
// whether it was used by the search engine or not.
|
// whether it was used by the search engine or not.
|
||||||
|
|
||||||
let highlights = results.highlights;
|
let highlights = results.highlights;
|
||||||
if (!highlights.includes(term)) {
|
if (!highlights.includes(term)) {
|
||||||
highlights = highlights.concat(term);
|
highlights = highlights.concat(term);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For overlapping highlights,
|
// For overlapping highlights,
|
||||||
// favour longer (more specific) terms first
|
// favour longer (more specific) terms first
|
||||||
highlights = highlights.sort(function (a, b) {
|
highlights = highlights.sort(function (a, b) {
|
||||||
return b.length - a.length;
|
return b.length - a.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const result of results.results) {
|
for (const result of results.results) {
|
||||||
for (const event of result.context.getTimeline()) {
|
for (const event of result.context.getTimeline()) {
|
||||||
const bundledRelationship =
|
const bundledRelationship = event.getServerAggregatedRelation<IThreadBundledRelationship>(
|
||||||
event.getServerAggregatedRelation<IThreadBundledRelationship>(
|
THREAD_RELATION_TYPE.name,
|
||||||
THREAD_RELATION_TYPE.name,
|
);
|
||||||
);
|
if (!bundledRelationship || event.getThread()) continue;
|
||||||
if (!bundledRelationship || event.getThread()) continue;
|
const room = client.getRoom(event.getRoomId());
|
||||||
const room = client.getRoom(event.getRoomId());
|
const thread = room?.findThreadForEvent(event);
|
||||||
const thread = room?.findThreadForEvent(event);
|
if (thread) {
|
||||||
if (thread) {
|
event.setThread(thread);
|
||||||
event.setThread(thread);
|
} else {
|
||||||
} else {
|
room?.createThread(event.getId()!, event, [], true);
|
||||||
room?.createThread(event.getId()!, event, [], true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setHighlights(highlights);
|
setHighlights(highlights);
|
||||||
setResults({ ...results }); // copy to force a refresh
|
setResults({ ...results }); // copy to force a refresh
|
||||||
onUpdate(false, results, null);
|
onUpdate(false, results, null);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (aborted.current) {
|
||||||
|
logger.error("Discarding stale search results");
|
||||||
return false;
|
return false;
|
||||||
},
|
}
|
||||||
(error) => {
|
logger.error("Search failed", error);
|
||||||
if (aborted.current) {
|
onUpdate(false, null, error);
|
||||||
logger.error("Discarding stale search results");
|
return false;
|
||||||
return false;
|
},
|
||||||
}
|
|
||||||
logger.error("Search failed", error);
|
|
||||||
onUpdate(false, null, error);
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[client, term, onUpdate],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mount & unmount effect
|
|
||||||
useEffect(() => {
|
|
||||||
aborted.current = false;
|
|
||||||
handleSearchResult(promise);
|
|
||||||
return () => {
|
|
||||||
aborted.current = true;
|
|
||||||
abortController?.abort();
|
|
||||||
};
|
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
// show searching spinner
|
|
||||||
if (results === null) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="mx_RoomView_messagePanel mx_RoomView_messagePanelSearchSpinner"
|
|
||||||
data-testid="messagePanelSearchSpinner"
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
[client, term, onUpdate],
|
||||||
|
);
|
||||||
|
|
||||||
const onSearchResultsFillRequest = async (backwards: boolean): Promise<boolean> => {
|
// Mount & unmount effect
|
||||||
if (!backwards) {
|
useEffect(() => {
|
||||||
return false;
|
aborted.current = false;
|
||||||
}
|
handleSearchResult(promise);
|
||||||
|
return () => {
|
||||||
if (!results.next_batch) {
|
aborted.current = true;
|
||||||
debuglog("no more search results");
|
abortController?.abort();
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
debuglog("requesting more search results");
|
|
||||||
const searchPromise = searchPagination(client, results);
|
|
||||||
return handleSearchResult(searchPromise);
|
|
||||||
};
|
};
|
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const ret: JSX.Element[] = [];
|
// show searching spinner
|
||||||
|
if (results === null) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="mx_RoomView_messagePanel mx_RoomView_messagePanelSearchSpinner"
|
||||||
|
data-testid="messagePanelSearchSpinner"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (inProgress) {
|
const onSearchResultsFillRequest = async (backwards: boolean): Promise<boolean> => {
|
||||||
ret.push(
|
if (!backwards) {
|
||||||
<li key="search-spinner">
|
return false;
|
||||||
<Spinner />
|
|
||||||
</li>,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!results.next_batch) {
|
if (!results.next_batch) {
|
||||||
if (!results?.results?.length) {
|
debuglog("no more search results");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
debuglog("requesting more search results");
|
||||||
|
const searchPromise = searchPagination(client, results);
|
||||||
|
return handleSearchResult(searchPromise);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ret: JSX.Element[] = [];
|
||||||
|
|
||||||
|
if (inProgress) {
|
||||||
|
ret.push(
|
||||||
|
<li key="search-spinner">
|
||||||
|
<Spinner />
|
||||||
|
</li>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!results.next_batch) {
|
||||||
|
if (!results?.results?.length) {
|
||||||
|
ret.push(
|
||||||
|
<li key="search-top-marker">
|
||||||
|
<h2 className="mx_RoomView_topMarker">{_t("common|no_results")}</h2>
|
||||||
|
</li>,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ret.push(
|
||||||
|
<li key="search-top-marker">
|
||||||
|
<h2 className="mx_RoomView_topMarker">{_t("no_more_results")}</h2>
|
||||||
|
</li>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRef = (e: ScrollPanel | null): void => {
|
||||||
|
if (typeof ref === "function") {
|
||||||
|
ref(e);
|
||||||
|
} else if (!!ref) {
|
||||||
|
ref.current = e;
|
||||||
|
}
|
||||||
|
innerRef.current = e;
|
||||||
|
};
|
||||||
|
|
||||||
|
let lastRoomId: string | undefined;
|
||||||
|
let mergedTimeline: MatrixEvent[] = [];
|
||||||
|
let ourEventsIndexes: number[] = [];
|
||||||
|
|
||||||
|
for (let i = (results?.results?.length || 0) - 1; i >= 0; i--) {
|
||||||
|
const result = results.results[i];
|
||||||
|
|
||||||
|
const mxEv = result.context.getEvent();
|
||||||
|
const roomId = mxEv.getRoomId()!;
|
||||||
|
const room = client.getRoom(roomId);
|
||||||
|
if (!room) {
|
||||||
|
// if we do not have the room in js-sdk stores then hide it as we cannot easily show it
|
||||||
|
// As per the spec, an all rooms search can create this condition,
|
||||||
|
// it happens with Seshat but not Synapse.
|
||||||
|
// It will make the result count not match the displayed count.
|
||||||
|
logger.log("Hiding search result from an unknown room", roomId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!haveRendererForEvent(mxEv, client, roomContext.showHiddenEvents)) {
|
||||||
|
// XXX: can this ever happen? It will make the result count
|
||||||
|
// not match the displayed count.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope === SearchScope.All) {
|
||||||
|
if (roomId !== lastRoomId) {
|
||||||
ret.push(
|
ret.push(
|
||||||
<li key="search-top-marker">
|
<li key={mxEv.getId() + "-room"}>
|
||||||
<h2 className="mx_RoomView_topMarker">{_t("common|no_results")}</h2>
|
<h2>
|
||||||
</li>,
|
{_t("common|room")}: {room.name}
|
||||||
);
|
</h2>
|
||||||
} else {
|
|
||||||
ret.push(
|
|
||||||
<li key="search-top-marker">
|
|
||||||
<h2 className="mx_RoomView_topMarker">{_t("no_more_results")}</h2>
|
|
||||||
</li>,
|
</li>,
|
||||||
);
|
);
|
||||||
|
lastRoomId = roomId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onRef = (e: ScrollPanel | null): void => {
|
const resultLink = "#/room/" + roomId + "/" + mxEv.getId();
|
||||||
if (typeof ref === "function") {
|
|
||||||
ref(e);
|
|
||||||
} else if (!!ref) {
|
|
||||||
ref.current = e;
|
|
||||||
}
|
|
||||||
innerRef.current = e;
|
|
||||||
};
|
|
||||||
|
|
||||||
let lastRoomId: string | undefined;
|
// merging two successive search result if the query is present in both of them
|
||||||
let mergedTimeline: MatrixEvent[] = [];
|
const currentTimeline = result.context.getTimeline();
|
||||||
let ourEventsIndexes: number[] = [];
|
const nextTimeline = i > 0 ? results.results[i - 1].context.getTimeline() : [];
|
||||||
|
|
||||||
for (let i = (results?.results?.length || 0) - 1; i >= 0; i--) {
|
|
||||||
const result = results.results[i];
|
|
||||||
|
|
||||||
const mxEv = result.context.getEvent();
|
|
||||||
const roomId = mxEv.getRoomId()!;
|
|
||||||
const room = client.getRoom(roomId);
|
|
||||||
if (!room) {
|
|
||||||
// if we do not have the room in js-sdk stores then hide it as we cannot easily show it
|
|
||||||
// As per the spec, an all rooms search can create this condition,
|
|
||||||
// it happens with Seshat but not Synapse.
|
|
||||||
// It will make the result count not match the displayed count.
|
|
||||||
logger.log("Hiding search result from an unknown room", roomId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!haveRendererForEvent(mxEv, client, roomContext.showHiddenEvents)) {
|
|
||||||
// XXX: can this ever happen? It will make the result count
|
|
||||||
// not match the displayed count.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scope === SearchScope.All) {
|
|
||||||
if (roomId !== lastRoomId) {
|
|
||||||
ret.push(
|
|
||||||
<li key={mxEv.getId() + "-room"}>
|
|
||||||
<h2>
|
|
||||||
{_t("common|room")}: {room.name}
|
|
||||||
</h2>
|
|
||||||
</li>,
|
|
||||||
);
|
|
||||||
lastRoomId = roomId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultLink = "#/room/" + roomId + "/" + mxEv.getId();
|
|
||||||
|
|
||||||
// merging two successive search result if the query is present in both of them
|
|
||||||
const currentTimeline = result.context.getTimeline();
|
|
||||||
const nextTimeline = i > 0 ? results.results[i - 1].context.getTimeline() : [];
|
|
||||||
|
|
||||||
if (i > 0 && currentTimeline[currentTimeline.length - 1].getId() == nextTimeline[0].getId()) {
|
|
||||||
// if this is the first searchResult we merge then add all values of the current searchResult
|
|
||||||
if (mergedTimeline.length == 0) {
|
|
||||||
for (let j = mergedTimeline.length == 0 ? 0 : 1; j < result.context.getTimeline().length; j++) {
|
|
||||||
mergedTimeline.push(currentTimeline[j]);
|
|
||||||
}
|
|
||||||
ourEventsIndexes.push(result.context.getOurEventIndex());
|
|
||||||
}
|
|
||||||
|
|
||||||
// merge the events of the next searchResult
|
|
||||||
for (let j = 1; j < nextTimeline.length; j++) {
|
|
||||||
mergedTimeline.push(nextTimeline[j]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the index of the matching event of the next searchResult
|
|
||||||
ourEventsIndexes.push(
|
|
||||||
ourEventsIndexes[ourEventsIndexes.length - 1] +
|
|
||||||
results.results[i - 1].context.getOurEventIndex() +
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (i > 0 && currentTimeline[currentTimeline.length - 1].getId() == nextTimeline[0].getId()) {
|
||||||
|
// if this is the first searchResult we merge then add all values of the current searchResult
|
||||||
if (mergedTimeline.length == 0) {
|
if (mergedTimeline.length == 0) {
|
||||||
mergedTimeline = result.context.getTimeline();
|
for (let j = mergedTimeline.length == 0 ? 0 : 1; j < result.context.getTimeline().length; j++) {
|
||||||
ourEventsIndexes = [];
|
mergedTimeline.push(currentTimeline[j]);
|
||||||
|
}
|
||||||
ourEventsIndexes.push(result.context.getOurEventIndex());
|
ourEventsIndexes.push(result.context.getOurEventIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
let permalinkCreator = permalinkCreators.get(roomId);
|
// merge the events of the next searchResult
|
||||||
if (!permalinkCreator) {
|
for (let j = 1; j < nextTimeline.length; j++) {
|
||||||
permalinkCreator = new RoomPermalinkCreator(room);
|
mergedTimeline.push(nextTimeline[j]);
|
||||||
permalinkCreator.start();
|
|
||||||
permalinkCreators.set(roomId, permalinkCreator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.push(
|
// add the index of the matching event of the next searchResult
|
||||||
<SearchResultTile
|
ourEventsIndexes.push(
|
||||||
key={mxEv.getId()}
|
ourEventsIndexes[ourEventsIndexes.length - 1] + results.results[i - 1].context.getOurEventIndex() + 1,
|
||||||
timeline={mergedTimeline}
|
|
||||||
ourEventsIndexes={ourEventsIndexes}
|
|
||||||
searchHighlights={highlights ?? []}
|
|
||||||
resultLink={resultLink}
|
|
||||||
permalinkCreator={permalinkCreator}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ourEventsIndexes = [];
|
continue;
|
||||||
mergedTimeline = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (mergedTimeline.length == 0) {
|
||||||
<ScrollPanel
|
mergedTimeline = result.context.getTimeline();
|
||||||
ref={onRef}
|
ourEventsIndexes = [];
|
||||||
className={"mx_RoomView_searchResultsPanel " + className}
|
ourEventsIndexes.push(result.context.getOurEventIndex());
|
||||||
onFillRequest={onSearchResultsFillRequest}
|
}
|
||||||
resizeNotifier={resizeNotifier}
|
|
||||||
>
|
let permalinkCreator = permalinkCreators.get(roomId);
|
||||||
<li className="mx_RoomView_scrollheader" />
|
if (!permalinkCreator) {
|
||||||
{ret}
|
permalinkCreator = new RoomPermalinkCreator(room);
|
||||||
</ScrollPanel>
|
permalinkCreator.start();
|
||||||
|
permalinkCreators.set(roomId, permalinkCreator);
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.push(
|
||||||
|
<SearchResultTile
|
||||||
|
key={mxEv.getId()}
|
||||||
|
timeline={mergedTimeline}
|
||||||
|
ourEventsIndexes={ourEventsIndexes}
|
||||||
|
searchHighlights={highlights ?? []}
|
||||||
|
resultLink={resultLink}
|
||||||
|
permalinkCreator={permalinkCreator}
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
ourEventsIndexes = [];
|
||||||
|
mergedTimeline = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollPanel
|
||||||
|
ref={onRef}
|
||||||
|
className={"mx_RoomView_searchResultsPanel " + className}
|
||||||
|
onFillRequest={onSearchResultsFillRequest}
|
||||||
|
resizeNotifier={resizeNotifier}
|
||||||
|
>
|
||||||
|
<li className="mx_RoomView_scrollheader" />
|
||||||
|
{ret}
|
||||||
|
</ScrollPanel>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { type JSX } from "react";
|
||||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto-api";
|
import { CryptoEvent } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
|
||||||
@@ -43,13 +43,13 @@ type MigrationState = {
|
|||||||
/**
|
/**
|
||||||
* The view that is displayed after we have logged in, before the first /sync is completed.
|
* The view that is displayed after we have logged in, before the first /sync is completed.
|
||||||
*/
|
*/
|
||||||
export function LoginSplashView(props: Props): React.JSX.Element {
|
export function LoginSplashView(props: Props): JSX.Element {
|
||||||
const migrationState = useTypedEventEmitterState(
|
const migrationState = useTypedEventEmitterState(
|
||||||
props.matrixClient,
|
props.matrixClient,
|
||||||
CryptoEvent.LegacyCryptoStoreMigrationProgress,
|
CryptoEvent.LegacyCryptoStoreMigrationProgress,
|
||||||
(progress?: number, total?: number): MigrationState => ({ progress: progress ?? -1, totalSteps: total ?? -1 }),
|
(progress?: number, total?: number): MigrationState => ({ progress: progress ?? -1, totalSteps: total ?? -1 }),
|
||||||
);
|
);
|
||||||
let errorBox: React.JSX.Element | undefined;
|
let errorBox: JSX.Element | undefined;
|
||||||
if (props.syncError) {
|
if (props.syncError) {
|
||||||
errorBox = <div className="mx_LoginSplashView_syncError">{messageForSyncError(props.syncError)}</div>;
|
errorBox = <div className="mx_LoginSplashView_syncError">{messageForSyncError(props.syncError)}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type AriaRole, forwardRef, useCallback, useContext, useEffect, useState } from "react";
|
import React, { type AriaRole, type JSX, type Ref, useCallback, useContext, useEffect, useState } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { ClientEvent, type SyncState } from "matrix-js-sdk/src/matrix";
|
import { ClientEvent, type SyncState } from "matrix-js-sdk/src/matrix";
|
||||||
import { Avatar } from "@vector-im/compound-web";
|
import { Avatar } from "@vector-im/compound-web";
|
||||||
@@ -34,6 +34,7 @@ interface IProps {
|
|||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
altText?: string;
|
altText?: string;
|
||||||
role?: AriaRole;
|
role?: AriaRole;
|
||||||
|
ref?: Ref<HTMLElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculateUrls = (url?: string | null, urls?: string[], lowBandwidth = false): string[] => {
|
const calculateUrls = (url?: string | null, urls?: string[], lowBandwidth = false): string[] => {
|
||||||
@@ -87,7 +88,7 @@ const useImageUrl = ({ url, urls }: { url?: string | null; urls?: string[] }): [
|
|||||||
return [imageUrl, onError];
|
return [imageUrl, onError];
|
||||||
};
|
};
|
||||||
|
|
||||||
const BaseAvatar = forwardRef<HTMLElement, IProps>((props, ref) => {
|
const BaseAvatar = (props: IProps): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
idName,
|
idName,
|
||||||
@@ -99,6 +100,7 @@ const BaseAvatar = forwardRef<HTMLElement, IProps>((props, ref) => {
|
|||||||
className,
|
className,
|
||||||
type = "round",
|
type = "round",
|
||||||
altText = _t("common|avatar"),
|
altText = _t("common|avatar"),
|
||||||
|
ref,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -134,7 +136,7 @@ const BaseAvatar = forwardRef<HTMLElement, IProps>((props, ref) => {
|
|||||||
data-testid="avatar-img"
|
data-testid="avatar-img"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
export default BaseAvatar;
|
export default BaseAvatar;
|
||||||
export type BaseAvatarType = React.FC<IProps>;
|
export type BaseAvatarType = React.FC<IProps>;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, forwardRef, type ReactNode, type Ref, useContext } from "react";
|
import React, { type JSX, type ReactNode, type Ref, useContext } from "react";
|
||||||
import { type RoomMember, type ResizeMethod } from "matrix-js-sdk/src/matrix";
|
import { type RoomMember, type ResizeMethod } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
@@ -33,21 +33,20 @@ interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" |
|
|||||||
forceHistorical?: boolean; // true to deny `useOnlyCurrentProfiles` usage. Default false.
|
forceHistorical?: boolean; // true to deny `useOnlyCurrentProfiles` usage. Default false.
|
||||||
hideTitle?: boolean;
|
hideTitle?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
ref?: Ref<HTMLElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MemberAvatar(
|
export default function MemberAvatar({
|
||||||
{
|
size,
|
||||||
size,
|
resizeMethod = "crop",
|
||||||
resizeMethod = "crop",
|
viewUserOnClick,
|
||||||
viewUserOnClick,
|
forceHistorical,
|
||||||
forceHistorical,
|
fallbackUserId,
|
||||||
fallbackUserId,
|
hideTitle,
|
||||||
hideTitle,
|
member: propsMember,
|
||||||
member: propsMember,
|
ref,
|
||||||
...props
|
...props
|
||||||
}: IProps,
|
}: IProps): JSX.Element {
|
||||||
ref: Ref<HTMLElement>,
|
|
||||||
): JSX.Element {
|
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const card = useContext(CardContext);
|
const card = useContext(CardContext);
|
||||||
|
|
||||||
@@ -101,5 +100,3 @@ function MemberAvatar(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default forwardRef(MemberAvatar);
|
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import React, {
|
|||||||
type JSX,
|
type JSX,
|
||||||
type ComponentProps,
|
type ComponentProps,
|
||||||
type ComponentPropsWithoutRef,
|
type ComponentPropsWithoutRef,
|
||||||
forwardRef,
|
|
||||||
type FunctionComponent,
|
|
||||||
type ReactElement,
|
type ReactElement,
|
||||||
type KeyboardEvent,
|
type KeyboardEvent,
|
||||||
type Ref,
|
type Ref,
|
||||||
@@ -100,6 +98,8 @@ type Props<T extends ElementType = "div"> = {
|
|||||||
* Whether the tooltip should be disabled.
|
* Whether the tooltip should be disabled.
|
||||||
*/
|
*/
|
||||||
disableTooltip?: TooltipProps["disabled"];
|
disableTooltip?: TooltipProps["disabled"];
|
||||||
|
|
||||||
|
ref?: Ref<HTMLElementTagNameMap[T]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ButtonProps<T extends ElementType> = Props<T> & Omit<ComponentPropsWithoutRef<T>, keyof Props<T>>;
|
export type ButtonProps<T extends ElementType> = Props<T> & Omit<ComponentPropsWithoutRef<T>, keyof Props<T>>;
|
||||||
@@ -119,28 +119,26 @@ type RenderedElementProps<T extends ElementType> = React.InputHTMLAttributes<Ele
|
|||||||
* @param {Object} props react element properties
|
* @param {Object} props react element properties
|
||||||
* @returns {Object} rendered react
|
* @returns {Object} rendered react
|
||||||
*/
|
*/
|
||||||
const AccessibleButton = forwardRef(function <T extends ElementType = typeof defaultElement>(
|
const AccessibleButton = function AccessibleButton<T extends ElementType = typeof defaultElement>({
|
||||||
{
|
element,
|
||||||
element,
|
onClick,
|
||||||
onClick,
|
children,
|
||||||
children,
|
kind,
|
||||||
kind,
|
disabled,
|
||||||
disabled,
|
className,
|
||||||
className,
|
onKeyDown,
|
||||||
onKeyDown,
|
onKeyUp,
|
||||||
onKeyUp,
|
triggerOnMouseDown,
|
||||||
triggerOnMouseDown,
|
title,
|
||||||
title,
|
caption,
|
||||||
caption,
|
placement = "right",
|
||||||
placement = "right",
|
onTooltipOpenChange,
|
||||||
onTooltipOpenChange,
|
disableTooltip,
|
||||||
disableTooltip,
|
role = "button",
|
||||||
role = "button",
|
tabIndex = 0,
|
||||||
tabIndex = 0,
|
ref,
|
||||||
...restProps
|
...restProps
|
||||||
}: ButtonProps<T>,
|
}: ButtonProps<T>): JSX.Element {
|
||||||
ref: Ref<HTMLElementTagNameMap[T]>,
|
|
||||||
): JSX.Element {
|
|
||||||
const newProps = {
|
const newProps = {
|
||||||
...restProps,
|
...restProps,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
@@ -226,10 +224,7 @@ const AccessibleButton = forwardRef(function <T extends ElementType = typeof def
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return button;
|
return button;
|
||||||
});
|
};
|
||||||
|
|
||||||
// Type assertion required due to forwardRef type workaround in react.d.ts
|
|
||||||
(AccessibleButton as FunctionComponent).displayName = "AccessibleButton";
|
|
||||||
|
|
||||||
interface RefProp<T extends ElementType> {
|
interface RefProp<T extends ElementType> {
|
||||||
ref?: Ref<HTMLElementTagNameMap[T]>;
|
ref?: Ref<HTMLElementTagNameMap[T]>;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type ReactNode, useState } from "react";
|
import React, { type JSX, type ReactNode, type Ref, useState } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { type RoomMember } from "matrix-js-sdk/src/matrix";
|
import { type RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
import LocationIcon from "@vector-im/compound-design-tokens/assets/web/icons/location-pin-solid";
|
import LocationIcon from "@vector-im/compound-design-tokens/assets/web/icons/location-pin-solid";
|
||||||
@@ -21,6 +21,7 @@ interface Props {
|
|||||||
// use member text color as background
|
// use member text color as background
|
||||||
useMemberColor?: boolean;
|
useMemberColor?: boolean;
|
||||||
tooltip?: ReactNode;
|
tooltip?: ReactNode;
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,7 +56,7 @@ const OptionalTooltip: React.FC<{
|
|||||||
/**
|
/**
|
||||||
* Generic location marker
|
* Generic location marker
|
||||||
*/
|
*/
|
||||||
const Marker = React.forwardRef<HTMLDivElement, Props>(({ id, roomMember, useMemberColor, tooltip }, ref) => {
|
const Marker = ({ id, roomMember, useMemberColor, tooltip, ref }: Props): JSX.Element => {
|
||||||
const memberColorClass = useMemberColor && roomMember ? getUserNameColorClass(roomMember.userId) : "";
|
const memberColorClass = useMemberColor && roomMember ? getUserNameColorClass(roomMember.userId) : "";
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -82,6 +83,6 @@ const Marker = React.forwardRef<HTMLDivElement, Props>(({ id, roomMember, useMem
|
|||||||
</OptionalTooltip>
|
</OptionalTooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
export default Marker;
|
export default Marker;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { forwardRef, useCallback, useContext, useMemo } from "react";
|
import React, { type Ref, useCallback, useContext, useMemo, type JSX } from "react";
|
||||||
|
|
||||||
import type { MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix";
|
import type { MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
import { ConnectionState, type ElementCall } from "../../../models/Call";
|
import { ConnectionState, type ElementCall } from "../../../models/Call";
|
||||||
@@ -37,60 +37,69 @@ interface ActiveCallEventProps {
|
|||||||
buttonKind: AccessibleButtonKind;
|
buttonKind: AccessibleButtonKind;
|
||||||
buttonDisabledTooltip?: string;
|
buttonDisabledTooltip?: string;
|
||||||
onButtonClick: ((ev: ButtonEvent) => void) | null;
|
onButtonClick: ((ev: ButtonEvent) => void) | null;
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActiveCallEvent = forwardRef<any, ActiveCallEventProps>(
|
const ActiveCallEvent = ({
|
||||||
({ mxEvent, call, participatingMembers, buttonText, buttonKind, buttonDisabledTooltip, onButtonClick }, ref) => {
|
mxEvent,
|
||||||
const senderName = useMemo(() => mxEvent.sender?.name ?? mxEvent.getSender(), [mxEvent]);
|
call,
|
||||||
|
participatingMembers,
|
||||||
|
buttonText,
|
||||||
|
buttonKind,
|
||||||
|
buttonDisabledTooltip,
|
||||||
|
onButtonClick,
|
||||||
|
ref,
|
||||||
|
}: ActiveCallEventProps): JSX.Element => {
|
||||||
|
const senderName = useMemo(() => mxEvent.sender?.name ?? mxEvent.getSender(), [mxEvent]);
|
||||||
|
|
||||||
const facePileMembers = useMemo(() => participatingMembers.slice(0, MAX_FACES), [participatingMembers]);
|
const facePileMembers = useMemo(() => participatingMembers.slice(0, MAX_FACES), [participatingMembers]);
|
||||||
const facePileOverflow = participatingMembers.length > facePileMembers.length;
|
const facePileOverflow = participatingMembers.length > facePileMembers.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_wrapper" ref={ref}>
|
<div className="mx_CallEvent_wrapper" ref={ref}>
|
||||||
<div className="mx_CallEvent mx_CallEvent_active">
|
<div className="mx_CallEvent mx_CallEvent_active">
|
||||||
<MemberAvatar
|
<MemberAvatar
|
||||||
member={mxEvent.sender}
|
member={mxEvent.sender}
|
||||||
fallbackUserId={mxEvent.getSender()}
|
fallbackUserId={mxEvent.getSender()}
|
||||||
viewUserOnClick
|
viewUserOnClick
|
||||||
size="24px"
|
size="24px"
|
||||||
/>
|
/>
|
||||||
<div className="mx_CallEvent_columns">
|
<div className="mx_CallEvent_columns">
|
||||||
<div className="mx_CallEvent_details">
|
<div className="mx_CallEvent_details">
|
||||||
<span className="mx_CallEvent_title">
|
<span className="mx_CallEvent_title">
|
||||||
{_t("timeline|m.call|video_call_started_text", { name: senderName })}
|
{_t("timeline|m.call|video_call_started_text", { name: senderName })}
|
||||||
</span>
|
</span>
|
||||||
<LiveContentSummary
|
<LiveContentSummary
|
||||||
type={LiveContentType.Video}
|
type={LiveContentType.Video}
|
||||||
text={_t("voip|video_call")}
|
text={_t("voip|video_call")}
|
||||||
active={false}
|
active={false}
|
||||||
participantCount={participatingMembers.length}
|
participantCount={participatingMembers.length}
|
||||||
/>
|
/>
|
||||||
<FacePile members={facePileMembers} size="24px" overflow={facePileOverflow} />
|
<FacePile members={facePileMembers} size="24px" overflow={facePileOverflow} />
|
||||||
</div>
|
|
||||||
{call && <SessionDuration session={call.session} />}
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_CallEvent_button"
|
|
||||||
kind={buttonKind}
|
|
||||||
disabled={onButtonClick === null || buttonDisabledTooltip !== undefined}
|
|
||||||
onClick={onButtonClick}
|
|
||||||
title={buttonDisabledTooltip}
|
|
||||||
>
|
|
||||||
{buttonText}
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
{call && <SessionDuration session={call.session} />}
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_CallEvent_button"
|
||||||
|
kind={buttonKind}
|
||||||
|
disabled={onButtonClick === null || buttonDisabledTooltip !== undefined}
|
||||||
|
onClick={onButtonClick}
|
||||||
|
title={buttonDisabledTooltip}
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
},
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
interface ActiveLoadedCallEventProps {
|
interface ActiveLoadedCallEventProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
call: ElementCall;
|
call: ElementCall;
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActiveLoadedCallEvent = forwardRef<any, ActiveLoadedCallEventProps>(({ mxEvent, call }, ref) => {
|
const ActiveLoadedCallEvent = ({ mxEvent, call, ref }: ActiveLoadedCallEventProps): JSX.Element => {
|
||||||
const connectionState = useConnectionState(call);
|
const connectionState = useConnectionState(call);
|
||||||
const participatingMembers = useParticipatingMembers(call);
|
const participatingMembers = useParticipatingMembers(call);
|
||||||
const joinCallButtonDisabledTooltip = useJoinCallButtonDisabledTooltip(call);
|
const joinCallButtonDisabledTooltip = useJoinCallButtonDisabledTooltip(call);
|
||||||
@@ -141,16 +150,17 @@ const ActiveLoadedCallEvent = forwardRef<any, ActiveLoadedCallEventProps>(({ mxE
|
|||||||
onButtonClick={onButtonClick}
|
onButtonClick={onButtonClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
interface CallEventProps {
|
interface CallEventProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event tile representing an active or historical Element call.
|
* An event tile representing an active or historical Element call.
|
||||||
*/
|
*/
|
||||||
export const CallEvent = forwardRef<any, CallEventProps>(({ mxEvent }, ref) => {
|
export const CallEvent = ({ mxEvent, ref }: CallEventProps): JSX.Element => {
|
||||||
const client = useContext(MatrixClientContext);
|
const client = useContext(MatrixClientContext);
|
||||||
const call = useCall(mxEvent.getRoomId()!);
|
const call = useCall(mxEvent.getRoomId()!);
|
||||||
const latestEvent = client
|
const latestEvent = client
|
||||||
@@ -187,4 +197,4 @@ export const CallEvent = forwardRef<any, CallEventProps>(({ mxEvent }, ref) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <ActiveLoadedCallEvent mxEvent={mxEvent} call={call as ElementCall} ref={ref} />;
|
return <ActiveLoadedCallEvent mxEvent={mxEvent} call={call as ElementCall} ref={ref} />;
|
||||||
});
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React, { forwardRef, type ForwardRefExoticComponent, useContext } from "react";
|
import React, { type JSX, useContext } from "react";
|
||||||
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api";
|
import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api";
|
||||||
import { BlockIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
import { BlockIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
@@ -16,7 +16,7 @@ import { _t } from "../../../languageHandler";
|
|||||||
import { type IBodyProps } from "./IBodyProps";
|
import { type IBodyProps } from "./IBodyProps";
|
||||||
import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext";
|
import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext";
|
||||||
|
|
||||||
function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): string | React.JSX.Element {
|
function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): string | JSX.Element {
|
||||||
switch (mxEvent.decryptionFailureReason) {
|
switch (mxEvent.decryptionFailureReason) {
|
||||||
case DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE:
|
case DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE:
|
||||||
return _t("timeline|decryption_failure|blocked");
|
return _t("timeline|decryption_failure|blocked");
|
||||||
@@ -72,7 +72,7 @@ function errorClassName(mxEvent: MatrixEvent): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A placeholder element for messages that could not be decrypted
|
// A placeholder element for messages that could not be decrypted
|
||||||
export const DecryptionFailureBody = forwardRef<HTMLDivElement, IBodyProps>(({ mxEvent }, ref): React.JSX.Element => {
|
export const DecryptionFailureBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => {
|
||||||
const verificationState = useContext(LocalDeviceVerificationStateContext);
|
const verificationState = useContext(LocalDeviceVerificationStateContext);
|
||||||
const classes = classNames("mx_DecryptionFailureBody", "mx_EventTile_content", errorClassName(mxEvent));
|
const classes = classNames("mx_DecryptionFailureBody", "mx_EventTile_content", errorClassName(mxEvent));
|
||||||
|
|
||||||
@@ -81,4 +81,4 @@ export const DecryptionFailureBody = forwardRef<HTMLDivElement, IBodyProps>(({ m
|
|||||||
{getErrorMessage(mxEvent, verificationState)}
|
{getErrorMessage(mxEvent, verificationState)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}) as ForwardRefExoticComponent<IBodyProps>;
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, forwardRef } from "react";
|
import React, { type JSX, type Ref, type ReactNode } from "react";
|
||||||
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import type { RoomEncryptionEventContent } from "matrix-js-sdk/src/types";
|
import type { RoomEncryptionEventContent } from "matrix-js-sdk/src/types";
|
||||||
@@ -22,9 +22,10 @@ import { useIsEncrypted } from "../../../hooks/useIsEncrypted.ts";
|
|||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
timestamp?: JSX.Element;
|
timestamp?: JSX.Element;
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EncryptionEvent = forwardRef<HTMLDivElement, IProps>(({ mxEvent, timestamp }, ref) => {
|
const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => {
|
||||||
const cli = useMatrixClientContext();
|
const cli = useMatrixClientContext();
|
||||||
const roomId = mxEvent.getRoomId()!;
|
const roomId = mxEvent.getRoomId()!;
|
||||||
const isRoomEncrypted = useIsEncrypted(cli, cli.getRoom(roomId) || undefined);
|
const isRoomEncrypted = useIsEncrypted(cli, cli.getRoom(roomId) || undefined);
|
||||||
@@ -80,6 +81,6 @@ const EncryptionEvent = forwardRef<HTMLDivElement, IProps>(({ mxEvent, timestamp
|
|||||||
timestamp={timestamp}
|
timestamp={timestamp}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
export default EncryptionEvent;
|
export default EncryptionEvent;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { memo, forwardRef, useContext, useMemo } from "react";
|
import React, { memo, useContext, useMemo, type Ref } from "react";
|
||||||
import { type IContent, type MatrixEvent, MsgType, PushRuleKind } from "matrix-js-sdk/src/matrix";
|
import { type IContent, type MatrixEvent, MsgType, PushRuleKind } from "matrix-js-sdk/src/matrix";
|
||||||
import parse from "html-react-parser";
|
import parse from "html-react-parser";
|
||||||
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
|
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
|
||||||
@@ -139,6 +139,7 @@ interface Props extends ReplacerOptions {
|
|||||||
* Whether to include the `dir="auto"` attribute on the rendered element
|
* Whether to include the `dir="auto"` attribute on the rendered element
|
||||||
*/
|
*/
|
||||||
includeDir?: boolean;
|
includeDir?: boolean;
|
||||||
|
ref?: Ref<HTMLElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,52 +149,50 @@ interface Props extends ReplacerOptions {
|
|||||||
* Returns a div or span depending on `as`, the `dir` on a `div` is always set to `"auto"` but set by `includeDir` otherwise.
|
* Returns a div or span depending on `as`, the `dir` on a `div` is always set to `"auto"` but set by `includeDir` otherwise.
|
||||||
*/
|
*/
|
||||||
const EventContentBody = memo(
|
const EventContentBody = memo(
|
||||||
forwardRef<HTMLElement, Props>(
|
({ as, mxEvent, stripReply, content, linkify, highlights, includeDir = true, ref, ...options }: Props) => {
|
||||||
({ as, mxEvent, stripReply, content, linkify, highlights, includeDir = true, ...options }, ref) => {
|
const enableBigEmoji = useSettingValue("TextualBody.enableBigEmoji");
|
||||||
const enableBigEmoji = useSettingValue("TextualBody.enableBigEmoji");
|
const [mediaIsVisible] = useMediaVisible(mxEvent?.getId(), mxEvent?.getRoomId());
|
||||||
const [mediaIsVisible] = useMediaVisible(mxEvent?.getId(), mxEvent?.getRoomId());
|
|
||||||
|
|
||||||
const replacer = useReplacer(content, mxEvent, options);
|
const replacer = useReplacer(content, mxEvent, options);
|
||||||
const linkifyOptions = useMemo(
|
const linkifyOptions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
render: replacerToRenderFunction(replacer),
|
render: replacerToRenderFunction(replacer),
|
||||||
|
}),
|
||||||
|
[replacer],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isEmote = content.msgtype === MsgType.Emote;
|
||||||
|
|
||||||
|
const { strippedBody, formattedBody, emojiBodyElements, className } = useMemo(
|
||||||
|
() =>
|
||||||
|
bodyToNode(content, highlights, {
|
||||||
|
disableBigEmoji: isEmote || !enableBigEmoji,
|
||||||
|
// Part of Replies fallback support
|
||||||
|
stripReplyFallback: stripReply,
|
||||||
|
mediaIsVisible,
|
||||||
}),
|
}),
|
||||||
[replacer],
|
[content, mediaIsVisible, enableBigEmoji, highlights, isEmote, stripReply],
|
||||||
);
|
);
|
||||||
|
|
||||||
const isEmote = content.msgtype === MsgType.Emote;
|
if (as === "div") includeDir = true; // force dir="auto" on divs
|
||||||
|
|
||||||
const { strippedBody, formattedBody, emojiBodyElements, className } = useMemo(
|
const As = as;
|
||||||
() =>
|
const body = formattedBody ? (
|
||||||
bodyToNode(content, highlights, {
|
<As ref={ref as any} className={className} dir={includeDir ? "auto" : undefined}>
|
||||||
disableBigEmoji: isEmote || !enableBigEmoji,
|
{parse(formattedBody, {
|
||||||
// Part of Replies fallback support
|
replace: replacer,
|
||||||
stripReplyFallback: stripReply,
|
})}
|
||||||
mediaIsVisible,
|
</As>
|
||||||
}),
|
) : (
|
||||||
[content, mediaIsVisible, enableBigEmoji, highlights, isEmote, stripReply],
|
<As ref={ref as any} className={className} dir={includeDir ? "auto" : undefined}>
|
||||||
);
|
{applyReplacerOnString(emojiBodyElements || strippedBody, replacer)}
|
||||||
|
</As>
|
||||||
|
);
|
||||||
|
|
||||||
if (as === "div") includeDir = true; // force dir="auto" on divs
|
if (!linkify) return body;
|
||||||
|
|
||||||
const As = as;
|
return <Linkify options={linkifyOptions}>{body}</Linkify>;
|
||||||
const body = formattedBody ? (
|
},
|
||||||
<As ref={ref as any} className={className} dir={includeDir ? "auto" : undefined}>
|
|
||||||
{parse(formattedBody, {
|
|
||||||
replace: replacer,
|
|
||||||
})}
|
|
||||||
</As>
|
|
||||||
) : (
|
|
||||||
<As ref={ref as any} className={className} dir={includeDir ? "auto" : undefined}>
|
|
||||||
{applyReplacerOnString(emojiBodyElements || strippedBody, replacer)}
|
|
||||||
</As>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!linkify) return body;
|
|
||||||
|
|
||||||
return <Linkify options={linkifyOptions}>{body}</Linkify>;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default EventContentBody;
|
export default EventContentBody;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, forwardRef, type ReactNode } from "react";
|
import React, { type JSX, type ReactNode, type Ref } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
@@ -15,19 +15,18 @@ interface IProps {
|
|||||||
timestamp?: JSX.Element;
|
timestamp?: JSX.Element;
|
||||||
subtitle?: ReactNode;
|
subtitle?: ReactNode;
|
||||||
children?: JSX.Element;
|
children?: JSX.Element;
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EventTileBubble = forwardRef<HTMLDivElement, IProps>(
|
const EventTileBubble = ({ className, title, timestamp, subtitle, children, ref }: IProps): JSX.Element => {
|
||||||
({ className, title, timestamp, subtitle, children }, ref) => {
|
return (
|
||||||
return (
|
<div className={classNames("mx_EventTileBubble", className)} ref={ref}>
|
||||||
<div className={classNames("mx_EventTileBubble", className)} ref={ref}>
|
<div className="mx_EventTileBubble_title">{title}</div>
|
||||||
<div className="mx_EventTileBubble_title">{title}</div>
|
{subtitle && <div className="mx_EventTileBubble_subtitle">{subtitle}</div>}
|
||||||
{subtitle && <div className="mx_EventTileBubble_subtitle">{subtitle}</div>}
|
{children}
|
||||||
{children}
|
{timestamp}
|
||||||
{timestamp}
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
};
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export default EventTileBubble;
|
export default EventTileBubble;
|
||||||
|
|||||||
@@ -6,23 +6,18 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { type JSX } from "react";
|
||||||
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { type IBodyProps } from "./IBodyProps";
|
import { type IBodyProps } from "./IBodyProps";
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
mxEvent: MatrixEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A message hidden from the user pending moderation.
|
* A message hidden from the user pending moderation.
|
||||||
*
|
*
|
||||||
* Note: This component must not be used when the user is the author of the message
|
* Note: This component must not be used when the user is the author of the message
|
||||||
* or has a sufficient powerlevel to see the message.
|
* or has a sufficient powerlevel to see the message.
|
||||||
*/
|
*/
|
||||||
const HiddenBody = React.forwardRef<any, IProps | IBodyProps>(({ mxEvent }, ref) => {
|
const HiddenBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => {
|
||||||
let text;
|
let text;
|
||||||
const visibility = mxEvent.messageVisibility();
|
const visibility = mxEvent.messageVisibility();
|
||||||
switch (visibility.visible) {
|
switch (visibility.visible) {
|
||||||
@@ -42,6 +37,6 @@ const HiddenBody = React.forwardRef<any, IProps | IBodyProps>(({ mxEvent }, ref)
|
|||||||
{text}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
export default HiddenBody;
|
export default HiddenBody;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type LegacyRef } from "react";
|
|
||||||
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
@@ -44,7 +43,7 @@ export interface IBodyProps {
|
|||||||
// helper function to access relations for this event
|
// helper function to access relations for this event
|
||||||
getRelationsForEvent?: GetRelationsForEvent;
|
getRelationsForEvent?: GetRelationsForEvent;
|
||||||
|
|
||||||
ref?: React.RefObject<any> | LegacyRef<any>;
|
ref?: React.RefObject<any>;
|
||||||
|
|
||||||
// Set to `true` to disable interactions (e.g. video controls) and to remove controls from the tab order.
|
// Set to `true` to disable interactions (e.g. video controls) and to remove controls from the tab order.
|
||||||
// This may be useful when displaying a preview of the event.
|
// This may be useful when displaying a preview of the event.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, type ForwardRefExoticComponent, useCallback, useContext, useEffect, useState } from "react";
|
import React, { type JSX, useCallback, useContext, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
type Beacon,
|
type Beacon,
|
||||||
BeaconEvent,
|
BeaconEvent,
|
||||||
@@ -122,7 +122,7 @@ const useHandleBeaconRedaction = (
|
|||||||
}, [event, onBeforeBeaconInfoRedaction]);
|
}, [event, onBeforeBeaconInfoRedaction]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MBeaconBody = React.forwardRef<HTMLDivElement, IBodyProps>(({ mxEvent, getRelationsForEvent }, ref) => {
|
const MBeaconBody = ({ mxEvent, getRelationsForEvent, ref }: IBodyProps): JSX.Element => {
|
||||||
const { beacon, isLive, latestLocationState, waitingToStart } = useBeaconState(mxEvent);
|
const { beacon, isLive, latestLocationState, waitingToStart } = useBeaconState(mxEvent);
|
||||||
const mapId = useUniqueId(mxEvent.getId()!);
|
const mapId = useUniqueId(mxEvent.getId()!);
|
||||||
|
|
||||||
@@ -225,6 +225,6 @@ const MBeaconBody = React.forwardRef<HTMLDivElement, IBodyProps>(({ mxEvent, get
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}) as ForwardRefExoticComponent<IBodyProps>;
|
};
|
||||||
|
|
||||||
export default MBeaconBody;
|
export default MBeaconBody;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState, useContext, type ForwardRefExoticComponent } from "react";
|
import React, { useEffect, useState, useContext, type JSX } from "react";
|
||||||
import { MatrixEvent, M_TEXT } from "matrix-js-sdk/src/matrix";
|
import { MatrixEvent, M_TEXT } from "matrix-js-sdk/src/matrix";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ const usePollStartEvent = (event: MatrixEvent): { pollStartEvent?: MatrixEvent;
|
|||||||
return { pollStartEvent, isLoadingPollStartEvent };
|
return { pollStartEvent, isLoadingPollStartEvent };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MPollEndBody = React.forwardRef<any, IBodyProps>(({ mxEvent, ...props }, ref) => {
|
export const MPollEndBody = ({ mxEvent, ref, ...props }: IBodyProps): JSX.Element => {
|
||||||
const cli = useMatrixClientContext();
|
const cli = useMatrixClientContext();
|
||||||
const { pollStartEvent, isLoadingPollStartEvent } = usePollStartEvent(mxEvent);
|
const { pollStartEvent, isLoadingPollStartEvent } = usePollStartEvent(mxEvent);
|
||||||
|
|
||||||
@@ -105,4 +105,4 @@ export const MPollEndBody = React.forwardRef<any, IBodyProps>(({ mxEvent, ...pro
|
|||||||
<MPollBody mxEvent={pollStartEvent} {...props} />
|
<MPollBody mxEvent={pollStartEvent} {...props} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}) as ForwardRefExoticComponent<IBodyProps>;
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type ForwardRefExoticComponent, useContext } from "react";
|
import React, { useContext, type JSX } from "react";
|
||||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
@@ -15,7 +15,7 @@ import { formatFullDate } from "../../../DateUtils";
|
|||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { type IBodyProps } from "./IBodyProps";
|
import { type IBodyProps } from "./IBodyProps";
|
||||||
|
|
||||||
const RedactedBody = React.forwardRef<any, IBodyProps>(({ mxEvent }, ref) => {
|
const RedactedBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => {
|
||||||
const cli: MatrixClient = useContext(MatrixClientContext);
|
const cli: MatrixClient = useContext(MatrixClientContext);
|
||||||
let text = _t("timeline|self_redaction");
|
let text = _t("timeline|self_redaction");
|
||||||
const unsigned = mxEvent.getUnsigned();
|
const unsigned = mxEvent.getUnsigned();
|
||||||
@@ -37,6 +37,6 @@ const RedactedBody = React.forwardRef<any, IBodyProps>(({ mxEvent }, ref) => {
|
|||||||
{text}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}) as ForwardRefExoticComponent<IBodyProps>;
|
};
|
||||||
|
|
||||||
export default RedactedBody;
|
export default RedactedBody;
|
||||||
|
|||||||
@@ -7,16 +7,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { forwardRef, type ForwardRefExoticComponent } from "react";
|
import React, { type JSX } from "react";
|
||||||
|
|
||||||
import { type IBodyProps } from "./IBodyProps";
|
import { type IBodyProps } from "./IBodyProps";
|
||||||
|
|
||||||
export default forwardRef<HTMLDivElement, IBodyProps>(({ mxEvent, children }, ref) => {
|
export default ({ mxEvent, ref }: IBodyProps): JSX.Element => {
|
||||||
const text = mxEvent.getContent().body;
|
const text = mxEvent.getContent().body;
|
||||||
return (
|
return (
|
||||||
<div className="mx_UnknownBody" ref={ref}>
|
<div className="mx_UnknownBody" ref={ref}>
|
||||||
{text}
|
{text}
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}) as ForwardRefExoticComponent<IBodyProps>;
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { forwardRef, type ReactNode, type KeyboardEvent, type Ref, type MouseEvent } from "react";
|
import React, { type ReactNode, type KeyboardEvent, type Ref, type MouseEvent } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { IconButton, Text } from "@vector-im/compound-web";
|
import { IconButton, Text } from "@vector-im/compound-web";
|
||||||
import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close";
|
import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close";
|
||||||
@@ -44,106 +44,102 @@ function closeRightPanel(ev: MouseEvent<HTMLButtonElement>): void {
|
|||||||
RightPanelStore.instance.popCard();
|
RightPanelStore.instance.popCard();
|
||||||
}
|
}
|
||||||
|
|
||||||
const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
|
const BaseCard: React.FC<IProps> = ({
|
||||||
(
|
closeLabel,
|
||||||
{
|
onClose,
|
||||||
closeLabel,
|
onBack,
|
||||||
onClose,
|
className,
|
||||||
onBack,
|
id,
|
||||||
className,
|
ariaLabelledBy,
|
||||||
id,
|
role,
|
||||||
ariaLabelledBy,
|
hideHeaderButtons,
|
||||||
role,
|
header,
|
||||||
hideHeaderButtons,
|
footer,
|
||||||
header,
|
withoutScrollContainer,
|
||||||
footer,
|
children,
|
||||||
withoutScrollContainer,
|
onKeyDown,
|
||||||
children,
|
closeButtonRef,
|
||||||
onKeyDown,
|
ref,
|
||||||
closeButtonRef,
|
}: IProps) => {
|
||||||
},
|
let backButton;
|
||||||
ref,
|
const cardHistory = RightPanelStore.instance.roomPhaseHistory;
|
||||||
) => {
|
if (cardHistory.length > 1 && !hideHeaderButtons) {
|
||||||
let backButton;
|
const prevCard = cardHistory[cardHistory.length - 2];
|
||||||
const cardHistory = RightPanelStore.instance.roomPhaseHistory;
|
const onBackClick = (ev: MouseEvent<HTMLButtonElement>): void => {
|
||||||
if (cardHistory.length > 1 && !hideHeaderButtons) {
|
onBack?.(ev);
|
||||||
const prevCard = cardHistory[cardHistory.length - 2];
|
RightPanelStore.instance.popCard();
|
||||||
const onBackClick = (ev: MouseEvent<HTMLButtonElement>): void => {
|
};
|
||||||
onBack?.(ev);
|
const label = backLabelForPhase(prevCard.phase) ?? _t("action|back");
|
||||||
RightPanelStore.instance.popCard();
|
backButton = (
|
||||||
};
|
<IconButton
|
||||||
const label = backLabelForPhase(prevCard.phase) ?? _t("action|back");
|
size="28px"
|
||||||
backButton = (
|
data-testid="base-card-back-button"
|
||||||
<IconButton
|
onClick={onBackClick}
|
||||||
size="28px"
|
tooltip={label}
|
||||||
data-testid="base-card-back-button"
|
subtleBackground
|
||||||
onClick={onBackClick}
|
>
|
||||||
tooltip={label}
|
<ChevronLeftIcon />
|
||||||
subtleBackground
|
</IconButton>
|
||||||
>
|
|
||||||
<ChevronLeftIcon />
|
|
||||||
</IconButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let closeButton;
|
|
||||||
if (!hideHeaderButtons) {
|
|
||||||
closeButton = (
|
|
||||||
<IconButton
|
|
||||||
size="28px"
|
|
||||||
data-testid="base-card-close-button"
|
|
||||||
onClick={onClose ?? closeRightPanel}
|
|
||||||
ref={closeButtonRef}
|
|
||||||
tooltip={closeLabel ?? _t("action|close")}
|
|
||||||
subtleBackground
|
|
||||||
>
|
|
||||||
<CloseIcon />
|
|
||||||
</IconButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!withoutScrollContainer) {
|
|
||||||
children = <AutoHideScrollbar>{children}</AutoHideScrollbar>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldRenderHeader = header || !hideHeaderButtons;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CardContext.Provider value={{ isCard: true }}>
|
|
||||||
<div
|
|
||||||
id={id}
|
|
||||||
aria-labelledby={ariaLabelledBy}
|
|
||||||
role={role}
|
|
||||||
className={classNames("mx_BaseCard", className)}
|
|
||||||
ref={ref}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
>
|
|
||||||
{shouldRenderHeader && (
|
|
||||||
<div className="mx_BaseCard_header">
|
|
||||||
{backButton}
|
|
||||||
{typeof header === "string" ? (
|
|
||||||
<div className="mx_BaseCard_header_title">
|
|
||||||
<Text
|
|
||||||
size="md"
|
|
||||||
weight="medium"
|
|
||||||
className="mx_BaseCard_header_title_heading"
|
|
||||||
role="heading"
|
|
||||||
>
|
|
||||||
{header}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
(header ?? <div className="mx_BaseCard_header_spacer" />)
|
|
||||||
)}
|
|
||||||
{closeButton}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
{footer && <div className="mx_BaseCard_footer">{footer}</div>}
|
|
||||||
</div>
|
|
||||||
</CardContext.Provider>
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
|
||||||
|
let closeButton;
|
||||||
|
if (!hideHeaderButtons) {
|
||||||
|
closeButton = (
|
||||||
|
<IconButton
|
||||||
|
size="28px"
|
||||||
|
data-testid="base-card-close-button"
|
||||||
|
onClick={onClose ?? closeRightPanel}
|
||||||
|
ref={closeButtonRef}
|
||||||
|
tooltip={closeLabel ?? _t("action|close")}
|
||||||
|
subtleBackground
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!withoutScrollContainer) {
|
||||||
|
children = <AutoHideScrollbar>{children}</AutoHideScrollbar>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldRenderHeader = header || !hideHeaderButtons;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardContext.Provider value={{ isCard: true }}>
|
||||||
|
<div
|
||||||
|
id={id}
|
||||||
|
aria-labelledby={ariaLabelledBy}
|
||||||
|
role={role}
|
||||||
|
className={classNames("mx_BaseCard", className)}
|
||||||
|
ref={ref}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
>
|
||||||
|
{shouldRenderHeader && (
|
||||||
|
<div className="mx_BaseCard_header">
|
||||||
|
{backButton}
|
||||||
|
{typeof header === "string" ? (
|
||||||
|
<div className="mx_BaseCard_header_title">
|
||||||
|
<Text
|
||||||
|
size="md"
|
||||||
|
weight="medium"
|
||||||
|
className="mx_BaseCard_header_title_heading"
|
||||||
|
role="heading"
|
||||||
|
>
|
||||||
|
{header}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
(header ?? <div className="mx_BaseCard_header_spacer" />)
|
||||||
|
)}
|
||||||
|
{closeButton}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
{footer && <div className="mx_BaseCard_footer">{footer}</div>}
|
||||||
|
</div>
|
||||||
|
</CardContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default BaseCard;
|
export default BaseCard;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef, forwardRef, type JSX, type MouseEvent, type ReactNode } from "react";
|
import React, { createRef, type JSX, type Ref, type MouseEvent, type ReactNode } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {
|
import {
|
||||||
EventStatus,
|
EventStatus,
|
||||||
@@ -228,6 +228,8 @@ export interface EventTileProps {
|
|||||||
// The following properties are used by EventTilePreview to disable tab indexes within the event tile
|
// The following properties are used by EventTilePreview to disable tab indexes within the event tile
|
||||||
hideTimestamp?: boolean;
|
hideTimestamp?: boolean;
|
||||||
inhibitInteraction?: boolean;
|
inhibitInteraction?: boolean;
|
||||||
|
|
||||||
|
ref?: Ref<UnwrappedEventTile>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
@@ -1482,15 +1484,13 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wrap all event tiles with the tile error boundary so that any throws even during construction are captured
|
// Wrap all event tiles with the tile error boundary so that any throws even during construction are captured
|
||||||
const SafeEventTile = forwardRef<UnwrappedEventTile, EventTileProps>((props, ref) => {
|
const SafeEventTile = (props: EventTileProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<>
|
<TileErrorBoundary mxEvent={props.mxEvent} layout={props.layout ?? Layout.Group}>
|
||||||
<TileErrorBoundary mxEvent={props.mxEvent} layout={props.layout ?? Layout.Group}>
|
<UnwrappedEventTile {...props} />
|
||||||
<UnwrappedEventTile ref={ref} {...props} />
|
</TileErrorBoundary>
|
||||||
</TileErrorBoundary>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
export default SafeEventTile;
|
export default SafeEventTile;
|
||||||
|
|
||||||
function E2ePadlockUnencrypted(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
|
function E2ePadlockUnencrypted(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Form } from "@vector-im/compound-web";
|
import { Form } from "@vector-im/compound-web";
|
||||||
import React from "react";
|
import React, { type JSX } from "react";
|
||||||
import { List, type ListRowProps } from "react-virtualized/dist/commonjs/List";
|
import { List, type ListRowProps } from "react-virtualized/dist/commonjs/List";
|
||||||
import { AutoSizer } from "react-virtualized";
|
import { AutoSizer } from "react-virtualized";
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
|
|||||||
|
|
||||||
const totalRows = vm.members.length;
|
const totalRows = vm.members.length;
|
||||||
|
|
||||||
const getRowComponent = (item: MemberWithSeparator): React.JSX.Element => {
|
const getRowComponent = (item: MemberWithSeparator): JSX.Element => {
|
||||||
if (item === SEPARATOR) {
|
if (item === SEPARATOR) {
|
||||||
return <hr className="mx_MemberListView_separator" />;
|
return <hr className="mx_MemberListView_separator" />;
|
||||||
} else if (item.member) {
|
} else if (item.member) {
|
||||||
@@ -64,7 +64,7 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowRenderer = ({ key, index, style }: ListRowProps): React.JSX.Element => {
|
const rowRenderer = ({ key, index, style }: ListRowProps): JSX.Element => {
|
||||||
if (index === totalRows) {
|
if (index === totalRows) {
|
||||||
// We've rendered all the members,
|
// We've rendered all the members,
|
||||||
// now we render an empty div to add some space to the end of the list.
|
// now we render an empty div to add some space to the end of the list.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { type JSX } from "react";
|
||||||
import { Tooltip } from "@vector-im/compound-web";
|
import { Tooltip } from "@vector-im/compound-web";
|
||||||
import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified";
|
import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified";
|
||||||
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid";
|
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid";
|
||||||
@@ -14,7 +14,7 @@ import { _t } from "../../../../../../languageHandler";
|
|||||||
import { E2EStatus } from "../../../../../../utils/ShieldUtils";
|
import { E2EStatus } from "../../../../../../utils/ShieldUtils";
|
||||||
import { crossSigningUserTitles } from "../../../E2EIcon";
|
import { crossSigningUserTitles } from "../../../E2EIcon";
|
||||||
|
|
||||||
function getIconFromStatus(status: E2EStatus): React.JSX.Element | undefined {
|
function getIconFromStatus(status: E2EStatus): JSX.Element | undefined {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case E2EStatus.Normal:
|
case E2EStatus.Normal:
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { type JSX } from "react";
|
||||||
import OnlineOrUnavailableIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-solid-8x8";
|
import OnlineOrUnavailableIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-solid-8x8";
|
||||||
import OfflineIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-outline-8x8";
|
import OfflineIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-outline-8x8";
|
||||||
import DNDIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-strikethrough-8x8";
|
import DNDIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-strikethrough-8x8";
|
||||||
@@ -19,7 +19,7 @@ interface Props {
|
|||||||
|
|
||||||
export const BUSY_PRESENCE_NAME = new UnstableValue("busy", "org.matrix.msc3026.busy");
|
export const BUSY_PRESENCE_NAME = new UnstableValue("busy", "org.matrix.msc3026.busy");
|
||||||
|
|
||||||
function getIconForPresenceState(state: string): React.JSX.Element {
|
function getIconForPresenceState(state: string): JSX.Element {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case "online":
|
case "online":
|
||||||
return <OnlineOrUnavailableIcon height="8px" width="8px" className="mx_PresenceIconView_online" />;
|
return <OnlineOrUnavailableIcon height="8px" width="8px" className="mx_PresenceIconView_online" />;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { forwardRef } from "react";
|
import React, { type Ref, type JSX, type ReactNode } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { formatCount } from "../../../../utils/FormattingUtils";
|
import { formatCount } from "../../../../utils/FormattingUtils";
|
||||||
@@ -26,6 +26,8 @@ interface Props {
|
|||||||
* for the difference between the two.
|
* for the difference between the two.
|
||||||
*/
|
*/
|
||||||
forceDot?: boolean;
|
forceDot?: boolean;
|
||||||
|
children?: ReactNode;
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClickableProps extends Props {
|
interface ClickableProps extends Props {
|
||||||
@@ -45,61 +47,66 @@ interface ClickableProps extends Props {
|
|||||||
* notifications in the room list, it may have a green badge with the number of unread notifications,
|
* notifications in the room list, it may have a green badge with the number of unread notifications,
|
||||||
* but somewhere else it may just have a green dot as a more compact representation of the same information.
|
* but somewhere else it may just have a green dot as a more compact representation of the same information.
|
||||||
*/
|
*/
|
||||||
export const StatelessNotificationBadge = forwardRef<HTMLDivElement, XOR<Props, ClickableProps>>(
|
export const StatelessNotificationBadge = ({
|
||||||
({ symbol, count, level, knocked, forceDot = false, ...props }, ref) => {
|
symbol,
|
||||||
const hideBold = useSettingValue("feature_hidebold");
|
count,
|
||||||
|
level,
|
||||||
|
knocked,
|
||||||
|
forceDot = false,
|
||||||
|
...props
|
||||||
|
}: XOR<Props, ClickableProps>): JSX.Element => {
|
||||||
|
const hideBold = useSettingValue("feature_hidebold");
|
||||||
|
|
||||||
// Don't show a badge if we don't need to
|
// Don't show a badge if we don't need to
|
||||||
if ((level === NotificationLevel.None || (hideBold && level == NotificationLevel.Activity)) && !knocked) {
|
if ((level === NotificationLevel.None || (hideBold && level == NotificationLevel.Activity)) && !knocked) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasUnreadCount = level >= NotificationLevel.Notification && (!!count || !!symbol);
|
const hasUnreadCount = level >= NotificationLevel.Notification && (!!count || !!symbol);
|
||||||
|
|
||||||
const isEmptyBadge = symbol === null && count === 0;
|
const isEmptyBadge = symbol === null && count === 0;
|
||||||
|
|
||||||
if (symbol === null && count > 0) {
|
if (symbol === null && count > 0) {
|
||||||
symbol = formatCount(count);
|
symbol = formatCount(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We show a dot if either:
|
// We show a dot if either:
|
||||||
// * The props force us to, or
|
// * The props force us to, or
|
||||||
// * It's just an activity-level notification or (in theory) lower and the room isn't knocked
|
// * It's just an activity-level notification or (in theory) lower and the room isn't knocked
|
||||||
const badgeType =
|
const badgeType =
|
||||||
forceDot || (level <= NotificationLevel.Activity && !knocked)
|
forceDot || (level <= NotificationLevel.Activity && !knocked)
|
||||||
? "dot"
|
? "dot"
|
||||||
: !symbol || symbol.length < 3
|
: !symbol || symbol.length < 3
|
||||||
? "badge_2char"
|
? "badge_2char"
|
||||||
: "badge_3char";
|
: "badge_3char";
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
"mx_NotificationBadge": true,
|
"mx_NotificationBadge": true,
|
||||||
"mx_NotificationBadge_visible": isEmptyBadge || knocked ? true : hasUnreadCount,
|
"mx_NotificationBadge_visible": isEmptyBadge || knocked ? true : hasUnreadCount,
|
||||||
"mx_NotificationBadge_level_notification": level == NotificationLevel.Notification,
|
"mx_NotificationBadge_level_notification": level == NotificationLevel.Notification,
|
||||||
"mx_NotificationBadge_level_highlight": level >= NotificationLevel.Highlight,
|
"mx_NotificationBadge_level_highlight": level >= NotificationLevel.Highlight,
|
||||||
"mx_NotificationBadge_knocked": knocked,
|
"mx_NotificationBadge_knocked": knocked,
|
||||||
|
|
||||||
// Exactly one of mx_NotificationBadge_dot, mx_NotificationBadge_2char, mx_NotificationBadge_3char
|
// Exactly one of mx_NotificationBadge_dot, mx_NotificationBadge_2char, mx_NotificationBadge_3char
|
||||||
"mx_NotificationBadge_dot": badgeType === "dot",
|
"mx_NotificationBadge_dot": badgeType === "dot",
|
||||||
"mx_NotificationBadge_2char": badgeType === "badge_2char",
|
"mx_NotificationBadge_2char": badgeType === "badge_2char",
|
||||||
"mx_NotificationBadge_3char": badgeType === "badge_3char",
|
"mx_NotificationBadge_3char": badgeType === "badge_3char",
|
||||||
// Badges with text should always use light colors
|
// Badges with text should always use light colors
|
||||||
"cpd-theme-light": badgeType !== "dot",
|
"cpd-theme-light": badgeType !== "dot",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (props.onClick) {
|
|
||||||
return (
|
|
||||||
<AccessibleButton {...props} className={classes} onClick={props.onClick} ref={ref}>
|
|
||||||
<span className="mx_NotificationBadge_count">{symbol}</span>
|
|
||||||
{props.children}
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (props.onClick) {
|
||||||
return (
|
return (
|
||||||
<div className={classes} ref={ref}>
|
<AccessibleButton {...props} className={classes} onClick={props.onClick} ref={props.ref}>
|
||||||
<span className="mx_NotificationBadge_count">{symbol}</span>
|
<span className="mx_NotificationBadge_count">{symbol}</span>
|
||||||
</div>
|
{props.children}
|
||||||
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
|
||||||
|
return (
|
||||||
|
<div className={classes} ref={props.ref}>
|
||||||
|
<span className="mx_NotificationBadge_count">{symbol}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type ComponentProps, forwardRef, type JSX, useState } from "react";
|
import React, { type ComponentProps, type JSX, type Ref, useState } from "react";
|
||||||
import { IconButton, Menu, MenuItem, Separator, ToggleMenuItem, Tooltip } from "@vector-im/compound-web";
|
import { IconButton, Menu, MenuItem, Separator, ToggleMenuItem, Tooltip } from "@vector-im/compound-web";
|
||||||
import MarkAsReadIcon from "@vector-im/compound-design-tokens/assets/web/icons/mark-as-read";
|
import MarkAsReadIcon from "@vector-im/compound-design-tokens/assets/web/icons/mark-as-read";
|
||||||
import MarkAsUnreadIcon from "@vector-im/compound-design-tokens/assets/web/icons/mark-as-unread";
|
import MarkAsUnreadIcon from "@vector-im/compound-design-tokens/assets/web/icons/mark-as-unread";
|
||||||
@@ -147,22 +147,22 @@ function MoreOptionsMenu({ vm, setMenuOpen }: MoreOptionsMenuProps): JSX.Element
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MoreOptionsButtonProps extends ComponentProps<typeof IconButton> {}
|
interface MoreOptionsButtonProps extends ComponentProps<typeof IconButton> {
|
||||||
|
ref?: Ref<HTMLButtonElement>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A button to trigger the more options menu.
|
* A button to trigger the more options menu.
|
||||||
*/
|
*/
|
||||||
export const MoreOptionsButton = forwardRef<HTMLButtonElement, MoreOptionsButtonProps>(
|
export const MoreOptionsButton = function MoreOptionsButton(props: MoreOptionsButtonProps): JSX.Element {
|
||||||
function MoreOptionsButton(props, ref) {
|
return (
|
||||||
return (
|
<Tooltip label={_t("room_list|room|more_options")}>
|
||||||
<Tooltip label={_t("room_list|room|more_options")}>
|
<IconButton aria-label={_t("room_list|room|more_options")} {...props}>
|
||||||
<IconButton aria-label={_t("room_list|room|more_options")} {...props} ref={ref}>
|
<OverflowIcon />
|
||||||
<OverflowIcon />
|
</IconButton>
|
||||||
</IconButton>
|
</Tooltip>
|
||||||
</Tooltip>
|
);
|
||||||
);
|
};
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
interface NotificationMenuProps {
|
interface NotificationMenuProps {
|
||||||
/**
|
/**
|
||||||
@@ -238,15 +238,17 @@ interface NotificationButtonProps extends ComponentProps<typeof IconButton> {
|
|||||||
* Whether the room is muted.
|
* Whether the room is muted.
|
||||||
*/
|
*/
|
||||||
isRoomMuted: boolean;
|
isRoomMuted: boolean;
|
||||||
|
ref?: Ref<HTMLButtonElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A button to trigger the notification menu.
|
* A button to trigger the notification menu.
|
||||||
*/
|
*/
|
||||||
export const NotificationButton = forwardRef<HTMLButtonElement, NotificationButtonProps>(function MoreOptionsButton(
|
export const NotificationButton = function MoreOptionsButton({
|
||||||
{ isRoomMuted, ...props },
|
isRoomMuted,
|
||||||
ref,
|
ref,
|
||||||
) {
|
...props
|
||||||
|
}: NotificationButtonProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Tooltip label={_t("room_list|notification_options")}>
|
<Tooltip label={_t("room_list|notification_options")}>
|
||||||
<IconButton aria-label={_t("room_list|notification_options")} {...props} ref={ref}>
|
<IconButton aria-label={_t("room_list|notification_options")} {...props} ref={ref}>
|
||||||
@@ -254,4 +256,4 @@ export const NotificationButton = forwardRef<HTMLButtonElement, NotificationButt
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, type ForwardedRef, forwardRef, type RefObject, useMemo } from "react";
|
import React, { type JSX, type RefObject, useMemo, type ReactNode } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import type EditorStateTransfer from "../../../../utils/EditorStateTransfer";
|
import type EditorStateTransfer from "../../../../utils/EditorStateTransfer";
|
||||||
@@ -21,15 +21,13 @@ import { type ComposerFunctions } from "./types";
|
|||||||
interface ContentProps {
|
interface ContentProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
composerFunctions: ComposerFunctions;
|
composerFunctions: ComposerFunctions;
|
||||||
|
ref?: RefObject<HTMLElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Content = forwardRef<HTMLElement, ContentProps>(function Content(
|
const Content = function Content({ disabled = false, composerFunctions, ref }: ContentProps): ReactNode {
|
||||||
{ disabled = false, composerFunctions }: ContentProps,
|
useWysiwygEditActionHandler(disabled, ref, composerFunctions);
|
||||||
forwardRef: ForwardedRef<HTMLElement>,
|
|
||||||
) {
|
|
||||||
useWysiwygEditActionHandler(disabled, forwardRef as RefObject<HTMLElement>, composerFunctions);
|
|
||||||
return null;
|
return null;
|
||||||
});
|
};
|
||||||
|
|
||||||
interface EditWysiwygComposerProps {
|
interface EditWysiwygComposerProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, type ForwardedRef, forwardRef, type RefObject, useMemo } from "react";
|
import React, { type JSX, type RefObject, useMemo, type ReactNode } from "react";
|
||||||
import { type IEventRelation } from "matrix-js-sdk/src/matrix";
|
import { type IEventRelation } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { useWysiwygSendActionHandler } from "./hooks/useWysiwygSendActionHandler";
|
import { useWysiwygSendActionHandler } from "./hooks/useWysiwygSendActionHandler";
|
||||||
@@ -22,15 +22,13 @@ import { ComposerContext, getDefaultContextValue } from "./ComposerContext";
|
|||||||
interface ContentProps {
|
interface ContentProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
composerFunctions: ComposerFunctions;
|
composerFunctions: ComposerFunctions;
|
||||||
|
ref?: RefObject<HTMLElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Content = forwardRef<HTMLElement, ContentProps>(function Content(
|
const Content = function Content({ disabled = false, composerFunctions, ref }: ContentProps): ReactNode {
|
||||||
{ disabled = false, composerFunctions }: ContentProps,
|
useWysiwygSendActionHandler(disabled, ref, composerFunctions);
|
||||||
forwardRef: ForwardedRef<HTMLElement>,
|
|
||||||
) {
|
|
||||||
useWysiwygSendActionHandler(disabled, forwardRef as RefObject<HTMLElement>, composerFunctions);
|
|
||||||
return null;
|
return null;
|
||||||
});
|
};
|
||||||
|
|
||||||
export interface SendWysiwygComposerProps {
|
export interface SendWysiwygComposerProps {
|
||||||
initialContent?: string;
|
initialContent?: string;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React, { type CSSProperties, forwardRef, memo, type RefObject, type ReactNode } from "react";
|
import React, { type CSSProperties, memo, type RefObject, type ReactNode } from "react";
|
||||||
|
|
||||||
import { useIsExpanded } from "../hooks/useIsExpanded";
|
import { useIsExpanded } from "../hooks/useIsExpanded";
|
||||||
import { useSelection } from "../hooks/useSelection";
|
import { useSelection } from "../hooks/useSelection";
|
||||||
@@ -19,44 +19,36 @@ interface EditorProps {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
leftComponent?: ReactNode;
|
leftComponent?: ReactNode;
|
||||||
rightComponent?: ReactNode;
|
rightComponent?: ReactNode;
|
||||||
|
ref?: RefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Editor = memo(
|
export const Editor = memo(function Editor({ disabled, placeholder, leftComponent, rightComponent, ref }: EditorProps) {
|
||||||
forwardRef<HTMLDivElement | null, EditorProps>(function Editor(
|
const isExpanded = useIsExpanded(ref, HEIGHT_BREAKING_POINT);
|
||||||
{ disabled, placeholder, leftComponent, rightComponent }: EditorProps,
|
const { onFocus, onBlur, onInput } = useSelection();
|
||||||
ref,
|
|
||||||
) {
|
|
||||||
const isExpanded = useIsExpanded(ref as RefObject<HTMLDivElement | null>, HEIGHT_BREAKING_POINT);
|
|
||||||
const { onFocus, onBlur, onInput } = useSelection();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div data-testid="WysiwygComposerEditor" className="mx_WysiwygComposer_Editor" data-is-expanded={isExpanded}>
|
||||||
data-testid="WysiwygComposerEditor"
|
{leftComponent}
|
||||||
className="mx_WysiwygComposer_Editor"
|
<div className="mx_WysiwygComposer_Editor_container">
|
||||||
data-is-expanded={isExpanded}
|
<div
|
||||||
>
|
className={classNames("mx_WysiwygComposer_Editor_content", {
|
||||||
{leftComponent}
|
mx_WysiwygComposer_Editor_content_placeholder: Boolean(placeholder),
|
||||||
<div className="mx_WysiwygComposer_Editor_container">
|
})}
|
||||||
<div
|
style={{ "--placeholder": `"${placeholder}"` } as CSSProperties}
|
||||||
className={classNames("mx_WysiwygComposer_Editor_content", {
|
ref={ref}
|
||||||
mx_WysiwygComposer_Editor_content_placeholder: Boolean(placeholder),
|
contentEditable={!disabled}
|
||||||
})}
|
role="textbox"
|
||||||
style={{ "--placeholder": `"${placeholder}"` } as CSSProperties}
|
aria-multiline="true"
|
||||||
ref={ref}
|
aria-autocomplete="list"
|
||||||
contentEditable={!disabled}
|
aria-haspopup="listbox"
|
||||||
role="textbox"
|
dir="auto"
|
||||||
aria-multiline="true"
|
aria-disabled={disabled}
|
||||||
aria-autocomplete="list"
|
onFocus={onFocus}
|
||||||
aria-haspopup="listbox"
|
onBlur={onBlur}
|
||||||
dir="auto"
|
onInput={onInput}
|
||||||
aria-disabled={disabled}
|
/>
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
onInput={onInput}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{rightComponent}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
{rightComponent}
|
||||||
}),
|
</div>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, type ForwardedRef, forwardRef, type FunctionComponent } from "react";
|
import React, { type JSX, type Ref, type FunctionComponent } from "react";
|
||||||
import { type FormattingFunctions, type MappedSuggestion } from "@vector-im/matrix-wysiwyg";
|
import { type FormattingFunctions, type MappedSuggestion } from "@vector-im/matrix-wysiwyg";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
@@ -39,6 +39,8 @@ interface WysiwygAutocompleteProps {
|
|||||||
* Handler purely for the at-room mentions special case
|
* Handler purely for the at-room mentions special case
|
||||||
*/
|
*/
|
||||||
handleAtRoomMention: FormattingFunctions["mentionAtRoom"];
|
handleAtRoomMention: FormattingFunctions["mentionAtRoom"];
|
||||||
|
|
||||||
|
ref?: Ref<Autocomplete>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,69 +50,70 @@ interface WysiwygAutocompleteProps {
|
|||||||
*
|
*
|
||||||
* @param props.ref - the ref will be attached to the rendered `<Autocomplete />` component
|
* @param props.ref - the ref will be attached to the rendered `<Autocomplete />` component
|
||||||
*/
|
*/
|
||||||
const WysiwygAutocomplete = forwardRef(
|
const WysiwygAutocomplete = ({
|
||||||
(
|
suggestion,
|
||||||
{ suggestion, handleMention, handleCommand, handleAtRoomMention }: WysiwygAutocompleteProps,
|
handleMention,
|
||||||
ref: ForwardedRef<Autocomplete>,
|
handleCommand,
|
||||||
): JSX.Element | null => {
|
handleAtRoomMention,
|
||||||
const { room } = useScopedRoomContext("room");
|
ref,
|
||||||
const client = useMatrixClientContext();
|
}: WysiwygAutocompleteProps): JSX.Element | null => {
|
||||||
|
const { room } = useScopedRoomContext("room");
|
||||||
|
const client = useMatrixClientContext();
|
||||||
|
|
||||||
function handleConfirm(completion: ICompletion): void {
|
function handleConfirm(completion: ICompletion): void {
|
||||||
if (client === undefined || room === undefined) {
|
if (client === undefined || room === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
switch (completion.type) {
|
|
||||||
case "command": {
|
|
||||||
// TODO determine if utils in SlashCommands.tsx are required.
|
|
||||||
// Trim the completion as some include trailing spaces, but we always insert a
|
|
||||||
// trailing space in the rust model anyway
|
|
||||||
handleCommand(completion.completion.trim());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case "at-room": {
|
|
||||||
handleAtRoomMention(getMentionAttributes(completion, client, room));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case "room":
|
|
||||||
case "user": {
|
|
||||||
if (typeof completion.href === "string") {
|
|
||||||
handleMention(
|
|
||||||
completion.href,
|
|
||||||
getMentionDisplayText(completion, client),
|
|
||||||
getMentionAttributes(completion, client, room),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// TODO - handle "community" type
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!room) return null;
|
switch (completion.type) {
|
||||||
|
case "command": {
|
||||||
|
// TODO determine if utils in SlashCommands.tsx are required.
|
||||||
|
// Trim the completion as some include trailing spaces, but we always insert a
|
||||||
|
// trailing space in the rust model anyway
|
||||||
|
handleCommand(completion.completion.trim());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case "at-room": {
|
||||||
|
handleAtRoomMention(getMentionAttributes(completion, client, room));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case "room":
|
||||||
|
case "user": {
|
||||||
|
if (typeof completion.href === "string") {
|
||||||
|
handleMention(
|
||||||
|
completion.href,
|
||||||
|
getMentionDisplayText(completion, client),
|
||||||
|
getMentionAttributes(completion, client, room),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO - handle "community" type
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const autoCompleteQuery = buildQuery(suggestion);
|
if (!room) return null;
|
||||||
// debug for https://github.com/vector-im/element-web/issues/26037
|
|
||||||
logger.log(`## 26037 ## Rendering Autocomplete for WysiwygAutocomplete with query: "${autoCompleteQuery}"`);
|
|
||||||
|
|
||||||
// TODO - determine if we show all of the /command suggestions, there are some options in the
|
const autoCompleteQuery = buildQuery(suggestion);
|
||||||
// list which don't seem to make sense in this context, specifically /html and /plain
|
// debug for https://github.com/vector-im/element-web/issues/26037
|
||||||
return (
|
logger.log(`## 26037 ## Rendering Autocomplete for WysiwygAutocomplete with query: "${autoCompleteQuery}"`);
|
||||||
<div className="mx_WysiwygComposer_AutoCompleteWrapper" data-testid="autocomplete-wrapper">
|
|
||||||
<Autocomplete
|
// TODO - determine if we show all of the /command suggestions, there are some options in the
|
||||||
ref={ref}
|
// list which don't seem to make sense in this context, specifically /html and /plain
|
||||||
query={autoCompleteQuery}
|
return (
|
||||||
onConfirm={handleConfirm}
|
<div className="mx_WysiwygComposer_AutoCompleteWrapper" data-testid="autocomplete-wrapper">
|
||||||
selection={{ start: 0, end: 0 }}
|
<Autocomplete
|
||||||
room={room}
|
ref={ref}
|
||||||
/>
|
query={autoCompleteQuery}
|
||||||
</div>
|
onConfirm={handleConfirm}
|
||||||
);
|
selection={{ start: 0, end: 0 }}
|
||||||
},
|
room={room}
|
||||||
);
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
(WysiwygAutocomplete as FunctionComponent).displayName = "WysiwygAutocomplete";
|
(WysiwygAutocomplete as FunctionComponent).displayName = "WysiwygAutocomplete";
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { type RefObject, useEffect, useState } from "react";
|
import { type RefObject, useEffect, useState } from "react";
|
||||||
|
|
||||||
export function useIsExpanded(ref: RefObject<HTMLElement | null>, breakingPoint: number): boolean {
|
export function useIsExpanded(ref: RefObject<HTMLElement | null> | undefined, breakingPoint: number): boolean {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref?.current) {
|
||||||
const editor = ref.current;
|
const editor = ref.current;
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.
|
|||||||
|
|
||||||
export function useWysiwygEditActionHandler(
|
export function useWysiwygEditActionHandler(
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
composerElement: RefObject<HTMLElement>,
|
composerElement: RefObject<HTMLElement | null> | undefined,
|
||||||
composerFunctions: ComposerFunctions,
|
composerFunctions: ComposerFunctions,
|
||||||
): void {
|
): void {
|
||||||
const roomContext = useScopedRoomContext("timelineRenderingType");
|
const roomContext = useScopedRoomContext("timelineRenderingType");
|
||||||
@@ -33,7 +33,7 @@ export function useWysiwygEditActionHandler(
|
|||||||
(payload: ActionPayload) => {
|
(payload: ActionPayload) => {
|
||||||
// don't let the user into the composer if it is disabled - all of these branches lead
|
// don't let the user into the composer if it is disabled - all of these branches lead
|
||||||
// to the cursor being in the composer
|
// to the cursor being in the composer
|
||||||
if (disabled || !composerElement.current) return;
|
if (disabled || !composerElement?.current) return;
|
||||||
|
|
||||||
const context = payload.context ?? TimelineRenderingType.Room;
|
const context = payload.context ?? TimelineRenderingType.Room;
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.
|
|||||||
|
|
||||||
export function useWysiwygSendActionHandler(
|
export function useWysiwygSendActionHandler(
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
composerElement: RefObject<HTMLElement>,
|
composerElement: RefObject<HTMLElement | null> | undefined,
|
||||||
composerFunctions: ComposerFunctions,
|
composerFunctions: ComposerFunctions,
|
||||||
): void {
|
): void {
|
||||||
const roomContext = useScopedRoomContext("timelineRenderingType");
|
const roomContext = useScopedRoomContext("timelineRenderingType");
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import AccessibleButton, { type ButtonProps } from "../../elements/AccessibleBut
|
|||||||
|
|
||||||
type Props<T extends keyof HTMLElementTagNameMap> = Omit<
|
type Props<T extends keyof HTMLElementTagNameMap> = Omit<
|
||||||
ButtonProps<T>,
|
ButtonProps<T>,
|
||||||
"aria-label" | "title" | "kind" | "className" | "element"
|
"aria-label" | "title" | "kind" | "className" | "element" | "ref"
|
||||||
> & {
|
> & {
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type ForwardedRef, forwardRef } from "react";
|
import React, { type JSX, type Ref } from "react";
|
||||||
import { type IPusher, PUSHER_DEVICE_ID, type LocalNotificationSettings } from "matrix-js-sdk/src/matrix";
|
import { type IPusher, PUSHER_DEVICE_ID, type LocalNotificationSettings } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
@@ -47,6 +47,7 @@ interface Props {
|
|||||||
* Changes sign out button to be a manage button
|
* Changes sign out button to be a manage button
|
||||||
*/
|
*/
|
||||||
delegatedAuthAccountUrl?: string;
|
delegatedAuthAccountUrl?: string;
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDeviceSelected = (
|
const isDeviceSelected = (
|
||||||
@@ -237,152 +238,148 @@ const DeviceListItem: React.FC<{
|
|||||||
* Filtered list of devices
|
* Filtered list of devices
|
||||||
* Sorted by latest activity descending
|
* Sorted by latest activity descending
|
||||||
*/
|
*/
|
||||||
export const FilteredDeviceList = forwardRef(
|
export const FilteredDeviceList = ({
|
||||||
(
|
devices,
|
||||||
{
|
pushers,
|
||||||
devices,
|
localNotificationSettings,
|
||||||
pushers,
|
filter,
|
||||||
localNotificationSettings,
|
expandedDeviceIds,
|
||||||
filter,
|
signingOutDeviceIds,
|
||||||
expandedDeviceIds,
|
selectedDeviceIds,
|
||||||
signingOutDeviceIds,
|
onFilterChange,
|
||||||
selectedDeviceIds,
|
onDeviceExpandToggle,
|
||||||
onFilterChange,
|
saveDeviceName,
|
||||||
onDeviceExpandToggle,
|
onSignOutDevices,
|
||||||
saveDeviceName,
|
onRequestDeviceVerification,
|
||||||
onSignOutDevices,
|
setPushNotifications,
|
||||||
onRequestDeviceVerification,
|
setSelectedDeviceIds,
|
||||||
setPushNotifications,
|
supportsMSC3881,
|
||||||
setSelectedDeviceIds,
|
delegatedAuthAccountUrl,
|
||||||
supportsMSC3881,
|
ref,
|
||||||
delegatedAuthAccountUrl,
|
}: Props): JSX.Element => {
|
||||||
}: Props,
|
const sortedDevices = getFilteredSortedDevices(devices, filter);
|
||||||
ref: ForwardedRef<HTMLDivElement>,
|
|
||||||
) => {
|
|
||||||
const sortedDevices = getFilteredSortedDevices(devices, filter);
|
|
||||||
|
|
||||||
function getPusherForDevice(device: ExtendedDevice): IPusher | undefined {
|
function getPusherForDevice(device: ExtendedDevice): IPusher | undefined {
|
||||||
return pushers.find((pusher) => pusher[PUSHER_DEVICE_ID.name] === device.device_id);
|
return pushers.find((pusher) => pusher[PUSHER_DEVICE_ID.name] === device.device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSelection = (deviceId: ExtendedDevice["device_id"]): void => {
|
||||||
|
if (isDeviceSelected(deviceId, selectedDeviceIds)) {
|
||||||
|
// remove from selection
|
||||||
|
setSelectedDeviceIds(selectedDeviceIds.filter((id) => id !== deviceId));
|
||||||
|
} else {
|
||||||
|
setSelectedDeviceIds([...selectedDeviceIds, deviceId]);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const toggleSelection = (deviceId: ExtendedDevice["device_id"]): void => {
|
const options: FilterDropdownOption<DeviceFilterKey>[] = [
|
||||||
if (isDeviceSelected(deviceId, selectedDeviceIds)) {
|
{ id: ALL_FILTER_ID, label: _t("settings|sessions|filter_all") },
|
||||||
// remove from selection
|
{
|
||||||
setSelectedDeviceIds(selectedDeviceIds.filter((id) => id !== deviceId));
|
id: DeviceSecurityVariation.Verified,
|
||||||
} else {
|
label: _t("common|verified"),
|
||||||
setSelectedDeviceIds([...selectedDeviceIds, deviceId]);
|
description: _t("settings|sessions|filter_verified_description"),
|
||||||
}
|
},
|
||||||
};
|
{
|
||||||
|
id: DeviceSecurityVariation.Unverified,
|
||||||
|
label: _t("common|unverified"),
|
||||||
|
description: _t("settings|sessions|filter_unverified_description"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DeviceSecurityVariation.Inactive,
|
||||||
|
label: _t("settings|sessions|filter_inactive"),
|
||||||
|
description: _t("settings|sessions|filter_inactive_description", {
|
||||||
|
inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const options: FilterDropdownOption<DeviceFilterKey>[] = [
|
const onFilterOptionChange = (filterId: DeviceFilterKey): void => {
|
||||||
{ id: ALL_FILTER_ID, label: _t("settings|sessions|filter_all") },
|
onFilterChange(filterId === ALL_FILTER_ID ? undefined : (filterId as FilterVariation));
|
||||||
{
|
};
|
||||||
id: DeviceSecurityVariation.Verified,
|
|
||||||
label: _t("common|verified"),
|
|
||||||
description: _t("settings|sessions|filter_verified_description"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: DeviceSecurityVariation.Unverified,
|
|
||||||
label: _t("common|unverified"),
|
|
||||||
description: _t("settings|sessions|filter_unverified_description"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: DeviceSecurityVariation.Inactive,
|
|
||||||
label: _t("settings|sessions|filter_inactive"),
|
|
||||||
description: _t("settings|sessions|filter_inactive_description", {
|
|
||||||
inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const onFilterOptionChange = (filterId: DeviceFilterKey): void => {
|
const isAllSelected = selectedDeviceIds.length >= sortedDevices.length;
|
||||||
onFilterChange(filterId === ALL_FILTER_ID ? undefined : (filterId as FilterVariation));
|
const toggleSelectAll = (): void => {
|
||||||
};
|
if (isAllSelected) {
|
||||||
|
setSelectedDeviceIds([]);
|
||||||
|
} else {
|
||||||
|
setSelectedDeviceIds(sortedDevices.map((device) => device.device_id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const isAllSelected = selectedDeviceIds.length >= sortedDevices.length;
|
const isSigningOut = !!signingOutDeviceIds.length;
|
||||||
const toggleSelectAll = (): void => {
|
|
||||||
if (isAllSelected) {
|
|
||||||
setSelectedDeviceIds([]);
|
|
||||||
} else {
|
|
||||||
setSelectedDeviceIds(sortedDevices.map((device) => device.device_id));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSigningOut = !!signingOutDeviceIds.length;
|
return (
|
||||||
|
<div className="mx_FilteredDeviceList" ref={ref}>
|
||||||
return (
|
<FilteredDeviceListHeader
|
||||||
<div className="mx_FilteredDeviceList" ref={ref}>
|
selectedDeviceCount={selectedDeviceIds.length}
|
||||||
<FilteredDeviceListHeader
|
isAllSelected={isAllSelected}
|
||||||
selectedDeviceCount={selectedDeviceIds.length}
|
toggleSelectAll={toggleSelectAll}
|
||||||
isAllSelected={isAllSelected}
|
isSelectDisabled={!!delegatedAuthAccountUrl}
|
||||||
toggleSelectAll={toggleSelectAll}
|
>
|
||||||
isSelectDisabled={!!delegatedAuthAccountUrl}
|
{selectedDeviceIds.length ? (
|
||||||
>
|
<>
|
||||||
{selectedDeviceIds.length ? (
|
<AccessibleButton
|
||||||
<>
|
data-testid="sign-out-selection-cta"
|
||||||
<AccessibleButton
|
kind="danger_inline"
|
||||||
data-testid="sign-out-selection-cta"
|
disabled={isSigningOut}
|
||||||
kind="danger_inline"
|
onClick={() => onSignOutDevices(selectedDeviceIds)}
|
||||||
disabled={isSigningOut}
|
className="mx_FilteredDeviceList_headerButton"
|
||||||
onClick={() => onSignOutDevices(selectedDeviceIds)}
|
>
|
||||||
className="mx_FilteredDeviceList_headerButton"
|
{isSigningOut && <Spinner w={16} h={16} />}
|
||||||
>
|
{_t("action|sign_out")}
|
||||||
{isSigningOut && <Spinner w={16} h={16} />}
|
</AccessibleButton>
|
||||||
{_t("action|sign_out")}
|
<AccessibleButton
|
||||||
</AccessibleButton>
|
data-testid="cancel-selection-cta"
|
||||||
<AccessibleButton
|
kind="content_inline"
|
||||||
data-testid="cancel-selection-cta"
|
disabled={isSigningOut}
|
||||||
kind="content_inline"
|
onClick={() => setSelectedDeviceIds([])}
|
||||||
disabled={isSigningOut}
|
className="mx_FilteredDeviceList_headerButton"
|
||||||
onClick={() => setSelectedDeviceIds([])}
|
>
|
||||||
className="mx_FilteredDeviceList_headerButton"
|
{_t("action|cancel")}
|
||||||
>
|
</AccessibleButton>
|
||||||
{_t("action|cancel")}
|
</>
|
||||||
</AccessibleButton>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<FilterDropdown<DeviceFilterKey>
|
|
||||||
id="device-list-filter"
|
|
||||||
label={_t("settings|sessions|filter_label")}
|
|
||||||
value={filter || ALL_FILTER_ID}
|
|
||||||
onOptionChange={onFilterOptionChange}
|
|
||||||
options={options}
|
|
||||||
selectedLabel={_t("action|show")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</FilteredDeviceListHeader>
|
|
||||||
{!!sortedDevices.length ? (
|
|
||||||
<FilterSecurityCard filter={filter} />
|
|
||||||
) : (
|
) : (
|
||||||
<NoResults filter={filter} clearFilter={() => onFilterChange(undefined)} />
|
<FilterDropdown<DeviceFilterKey>
|
||||||
|
id="device-list-filter"
|
||||||
|
label={_t("settings|sessions|filter_label")}
|
||||||
|
value={filter || ALL_FILTER_ID}
|
||||||
|
onOptionChange={onFilterOptionChange}
|
||||||
|
options={options}
|
||||||
|
selectedLabel={_t("action|show")}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<ol className="mx_FilteredDeviceList_list">
|
</FilteredDeviceListHeader>
|
||||||
{sortedDevices.map((device) => (
|
{!!sortedDevices.length ? (
|
||||||
<DeviceListItem
|
<FilterSecurityCard filter={filter} />
|
||||||
key={device.device_id}
|
) : (
|
||||||
device={device}
|
<NoResults filter={filter} clearFilter={() => onFilterChange(undefined)} />
|
||||||
pusher={getPusherForDevice(device)}
|
)}
|
||||||
localNotificationSettings={localNotificationSettings.get(device.device_id)}
|
<ol className="mx_FilteredDeviceList_list">
|
||||||
isExpanded={expandedDeviceIds.includes(device.device_id)}
|
{sortedDevices.map((device) => (
|
||||||
isSigningOut={signingOutDeviceIds.includes(device.device_id)}
|
<DeviceListItem
|
||||||
isSelected={isDeviceSelected(device.device_id, selectedDeviceIds)}
|
key={device.device_id}
|
||||||
isSelectDisabled={!!delegatedAuthAccountUrl}
|
device={device}
|
||||||
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
|
pusher={getPusherForDevice(device)}
|
||||||
onSignOutDevice={() => onSignOutDevices([device.device_id])}
|
localNotificationSettings={localNotificationSettings.get(device.device_id)}
|
||||||
saveDeviceName={(deviceName: string) => saveDeviceName(device.device_id, deviceName)}
|
isExpanded={expandedDeviceIds.includes(device.device_id)}
|
||||||
onRequestDeviceVerification={
|
isSigningOut={signingOutDeviceIds.includes(device.device_id)}
|
||||||
onRequestDeviceVerification
|
isSelected={isDeviceSelected(device.device_id, selectedDeviceIds)}
|
||||||
? () => onRequestDeviceVerification(device.device_id)
|
isSelectDisabled={!!delegatedAuthAccountUrl}
|
||||||
: undefined
|
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
|
||||||
}
|
onSignOutDevice={() => onSignOutDevices([device.device_id])}
|
||||||
setPushNotifications={setPushNotifications}
|
saveDeviceName={(deviceName: string) => saveDeviceName(device.device_id, deviceName)}
|
||||||
toggleSelected={() => toggleSelection(device.device_id)}
|
onRequestDeviceVerification={
|
||||||
supportsMSC3881={supportsMSC3881}
|
onRequestDeviceVerification
|
||||||
delegatedAuthAccountUrl={delegatedAuthAccountUrl}
|
? () => onRequestDeviceVerification(device.device_id)
|
||||||
/>
|
: undefined
|
||||||
))}
|
}
|
||||||
</ol>
|
setPushNotifications={setPushNotifications}
|
||||||
</div>
|
toggleSelected={() => toggleSelection(device.device_id)}
|
||||||
);
|
supportsMSC3881={supportsMSC3881}
|
||||||
},
|
delegatedAuthAccountUrl={delegatedAuthAccountUrl}
|
||||||
);
|
/>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
|||||||
|
|
||||||
type ButtonProps<T extends keyof HTMLElementTagNameMap> = Omit<
|
type ButtonProps<T extends keyof HTMLElementTagNameMap> = Omit<
|
||||||
AccessibleButtonProps<T>,
|
AccessibleButtonProps<T>,
|
||||||
"title" | "onClick" | "size" | "element"
|
"title" | "onClick" | "size" | "element" | "ref"
|
||||||
> & {
|
> & {
|
||||||
space?: Room;
|
space?: Room;
|
||||||
spaceKey?: SpaceKey;
|
spaceKey?: SpaceKey;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type ComponentProps, forwardRef } from "react";
|
import React, { type ComponentProps, type Ref, type JSX } from "react";
|
||||||
import ThreadsSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/threads-solid";
|
import ThreadsSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/threads-solid";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { IconButton, Text, Tooltip } from "@vector-im/compound-web";
|
import { IconButton, Text, Tooltip } from "@vector-im/compound-web";
|
||||||
@@ -28,44 +28,46 @@ interface ThreadsActivityCentreButtonProps extends ComponentProps<typeof IconBut
|
|||||||
* The notification level of the threads.
|
* The notification level of the threads.
|
||||||
*/
|
*/
|
||||||
notificationLevel: NotificationLevel;
|
notificationLevel: NotificationLevel;
|
||||||
|
ref?: Ref<HTMLButtonElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A button to open the thread activity centre.
|
* A button to open the thread activity centre.
|
||||||
*/
|
*/
|
||||||
export const ThreadsActivityCentreButton = forwardRef<HTMLButtonElement, ThreadsActivityCentreButtonProps>(
|
export const ThreadsActivityCentreButton = function ThreadsActivityCentreButton({
|
||||||
function ThreadsActivityCentreButton(
|
displayLabel,
|
||||||
{ displayLabel, notificationLevel, disableTooltip, ...props },
|
notificationLevel,
|
||||||
ref,
|
disableTooltip,
|
||||||
): React.JSX.Element {
|
ref,
|
||||||
// Disable tooltip when the label is displayed
|
...props
|
||||||
const openTooltip = disableTooltip || displayLabel ? false : undefined;
|
}: ThreadsActivityCentreButtonProps): JSX.Element {
|
||||||
|
// Disable tooltip when the label is displayed
|
||||||
|
const openTooltip = disableTooltip || displayLabel ? false : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={_t("common|threads")} placement="right" open={openTooltip}>
|
<Tooltip label={_t("common|threads")} placement="right" open={openTooltip}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={_t("common|threads")}
|
aria-label={_t("common|threads")}
|
||||||
className={classNames("mx_ThreadsActivityCentreButton", { expanded: displayLabel })}
|
className={classNames("mx_ThreadsActivityCentreButton", { expanded: displayLabel })}
|
||||||
indicator={notificationLevelToIndicator(notificationLevel)}
|
indicator={notificationLevelToIndicator(notificationLevel)}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<ThreadsSolidIcon className="mx_ThreadsActivityCentreButton_Icon" />
|
<ThreadsSolidIcon className="mx_ThreadsActivityCentreButton_Icon" />
|
||||||
{/* This is dirty, but we need to add the label to the indicator icon */}
|
{/* This is dirty, but we need to add the label to the indicator icon */}
|
||||||
{displayLabel && (
|
{displayLabel && (
|
||||||
<Text
|
<Text
|
||||||
className="mx_ThreadsActivityCentreButton_Text"
|
className="mx_ThreadsActivityCentreButton_Text"
|
||||||
as="span"
|
as="span"
|
||||||
size="md"
|
size="md"
|
||||||
title={_t("common|threads")}
|
title={_t("common|threads")}
|
||||||
>
|
>
|
||||||
{_t("common|threads")}
|
{_t("common|threads")}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef, useState, forwardRef } from "react";
|
import React, { createRef, useState, type Ref, type FC } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
|
||||||
@@ -41,31 +41,40 @@ type ButtonProps = Omit<AccessibleButtonProps<"div">, "title" | "element"> & {
|
|||||||
offLabel?: string;
|
offLabel?: string;
|
||||||
forceHide?: boolean;
|
forceHide?: boolean;
|
||||||
onHover?: (hovering: boolean) => void;
|
onHover?: (hovering: boolean) => void;
|
||||||
|
ref?: Ref<HTMLElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LegacyCallViewToggleButton = forwardRef<HTMLElement, ButtonProps>(
|
const LegacyCallViewToggleButton: FC<ButtonProps> = ({
|
||||||
({ children, state: isOn, className, onLabel, offLabel, forceHide, onHover, ...props }, ref) => {
|
children,
|
||||||
const classes = classNames("mx_LegacyCallViewButtons_button", className, {
|
state: isOn,
|
||||||
mx_LegacyCallViewButtons_button_on: isOn,
|
className,
|
||||||
mx_LegacyCallViewButtons_button_off: !isOn,
|
onLabel,
|
||||||
});
|
offLabel,
|
||||||
|
forceHide,
|
||||||
|
onHover,
|
||||||
|
ref,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const classes = classNames("mx_LegacyCallViewButtons_button", className, {
|
||||||
|
mx_LegacyCallViewButtons_button_on: isOn,
|
||||||
|
mx_LegacyCallViewButtons_button_off: !isOn,
|
||||||
|
});
|
||||||
|
|
||||||
const title = forceHide ? undefined : isOn ? onLabel : offLabel;
|
const title = forceHide ? undefined : isOn ? onLabel : offLabel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={classes}
|
className={classes}
|
||||||
title={title}
|
title={title}
|
||||||
placement="top"
|
placement="top"
|
||||||
onTooltipOpenChange={onHover}
|
onTooltipOpenChange={onHover}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
|
||||||
interface IDropdownButtonProps extends ButtonProps {
|
interface IDropdownButtonProps extends ButtonProps {
|
||||||
deviceKinds: MediaDeviceKindEnum[];
|
deviceKinds: MediaDeviceKindEnum[];
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type ComponentClass, createContext, forwardRef, useContext } from "react";
|
import React, { type ComponentClass, createContext, useContext } from "react";
|
||||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
// This context is available to components under LoggedInView,
|
// This context is available to components under LoggedInView,
|
||||||
@@ -24,22 +24,16 @@ export function useMatrixClientContext(): MatrixClient {
|
|||||||
return useContext(MatrixClientContext);
|
return useContext(MatrixClientContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
const matrixHOC = <ComposedComponentProps extends object>(
|
const matrixHOC =
|
||||||
ComposedComponent: ComponentClass<ComposedComponentProps>,
|
<ComposedComponentProps extends object>(
|
||||||
): ((
|
ComposedComponent: ComponentClass<ComposedComponentProps>,
|
||||||
props: Omit<ComposedComponentProps, "mxClient"> & React.RefAttributes<InstanceType<typeof ComposedComponent>>,
|
): ((
|
||||||
) => React.ReactElement | null) => {
|
props: Omit<ComposedComponentProps, "mxClient"> & React.RefAttributes<InstanceType<typeof ComposedComponent>>,
|
||||||
type ComposedComponentInstance = InstanceType<typeof ComposedComponent>;
|
) => React.ReactElement | null) =>
|
||||||
|
(props) => {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
|
|
||||||
const TypedComponent = ComposedComponent;
|
|
||||||
|
|
||||||
return forwardRef<ComposedComponentInstance, Omit<ComposedComponentProps, "mxClient">>((props, ref) => {
|
|
||||||
const client = useContext(MatrixClientContext);
|
const client = useContext(MatrixClientContext);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return <TypedComponent ref={ref} {...props} mxClient={client} />;
|
return <ComposedComponent {...props} mxClient={client} />;
|
||||||
});
|
};
|
||||||
};
|
|
||||||
export const withMatrixClientHOC = matrixHOC;
|
export const withMatrixClientHOC = matrixHOC;
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export interface EventTileTypeProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FactoryProps = Omit<EventTileTypeProps, "ref">;
|
type FactoryProps = Omit<EventTileTypeProps, "ref">;
|
||||||
type Factory<X = FactoryProps> = (ref: Optional<React.RefObject<any>>, props: X) => JSX.Element;
|
type Factory<X = FactoryProps> = (ref: React.RefObject<any> | undefined, props: X) => JSX.Element;
|
||||||
|
|
||||||
export const MessageEventFactory: Factory = (ref, props) => <MessageEvent ref={ref} {...props} />;
|
export const MessageEventFactory: Factory = (ref, props) => <MessageEvent ref={ref} {...props} />;
|
||||||
const LegacyCallEventFactory: Factory<FactoryProps & { callEventGrouper: LegacyCallEventGrouper }> = (ref, props) => (
|
const LegacyCallEventFactory: Factory<FactoryProps & { callEventGrouper: LegacyCallEventGrouper }> = (ref, props) => (
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import { act, fireEvent, screen, waitFor } from "jest-matrix-react";
|
|||||||
import { RoomMember, User, RoomEvent } from "matrix-js-sdk/src/matrix";
|
import { RoomMember, User, RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
|
import { type JSX } from "react";
|
||||||
|
|
||||||
import type React from "react";
|
|
||||||
import { shouldShowComponent } from "../../../../../../src/customisations/helpers/UIComponents";
|
import { shouldShowComponent } from "../../../../../../src/customisations/helpers/UIComponents";
|
||||||
import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher";
|
import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher";
|
||||||
import { type Rendered, renderMemberList } from "./common";
|
import { type Rendered, renderMemberList } from "./common";
|
||||||
@@ -21,7 +21,7 @@ jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => ({
|
|||||||
shouldShowComponent: jest.fn(),
|
shouldShowComponent: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
type Children = (args: { height: number; width: number }) => React.JSX.Element;
|
type Children = (args: { height: number; width: number }) => JSX.Element;
|
||||||
jest.mock("react-virtualized", () => {
|
jest.mock("react-virtualized", () => {
|
||||||
const ReactVirtualized = jest.requireActual("react-virtualized");
|
const ReactVirtualized = jest.requireActual("react-virtualized");
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import { act } from "react";
|
import { act } from "react";
|
||||||
import { waitFor } from "jest-matrix-react";
|
import { waitFor } from "jest-matrix-react";
|
||||||
import { type Room, type RoomMember, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import { type Room, type RoomMember, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { type JSX } from "react";
|
||||||
|
|
||||||
import type React from "react";
|
|
||||||
import { filterConsole } from "../../../../../test-utils";
|
import { filterConsole } from "../../../../../test-utils";
|
||||||
import { type Rendered, renderMemberList } from "./common";
|
import { type Rendered, renderMemberList } from "./common";
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => ({
|
|||||||
shouldShowComponent: jest.fn(),
|
shouldShowComponent: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
type Children = (args: { height: number; width: number }) => React.JSX.Element;
|
type Children = (args: { height: number; width: number }) => JSX.Element;
|
||||||
jest.mock("react-virtualized", () => {
|
jest.mock("react-virtualized", () => {
|
||||||
const ReactVirtualized = jest.requireActual("react-virtualized");
|
const ReactVirtualized = jest.requireActual("react-virtualized");
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user