Merge branch 'develop' into johannes/latest-room-in-space
This commit is contained in:
commit
f3fd027db4
40 changed files with 948 additions and 868 deletions
|
@ -1,6 +1,8 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2015, 2016, 2018, 2019, 2020, 2023 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,46 +17,38 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { CSSProperties, useCallback, useContext, useEffect, useState } from "react";
|
||||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||
import classNames from "classnames";
|
||||
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { SyncState } from "matrix-js-sdk/src/sync";
|
||||
|
||||
import * as AvatarLogic from "../../../Avatar";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { toPx } from "../../../utils/units";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
|
||||
interface IProps {
|
||||
/** The name (first initial used as default) */
|
||||
name: string;
|
||||
/** ID for generating hash colours */
|
||||
idName?: string;
|
||||
/** onHover title text */
|
||||
title?: string;
|
||||
/** highest priority of them all, shortcut to set in urls[0] */
|
||||
url?: string;
|
||||
/** [highest_priority, ... , lowest_priority] */
|
||||
urls?: string[];
|
||||
name: string; // The name (first initial used as default)
|
||||
idName?: string; // ID for generating hash colours
|
||||
title?: string; // onHover title text
|
||||
url?: string; // highest priority of them all, shortcut to set in urls[0]
|
||||
urls?: string[]; // [highest_priority, ... , lowest_priority]
|
||||
width?: number;
|
||||
height?: number;
|
||||
/** @deprecated not actually used */
|
||||
// XXX: resizeMethod not actually used.
|
||||
resizeMethod?: ResizeMethod;
|
||||
/** true to add default url */
|
||||
defaultToInitialLetter?: boolean;
|
||||
onClick?: React.ComponentPropsWithoutRef<typeof AccessibleTooltipButton>["onClick"];
|
||||
defaultToInitialLetter?: boolean; // true to add default url
|
||||
onClick?: React.MouseEventHandler;
|
||||
inputRef?: React.RefObject<HTMLImageElement & HTMLSpanElement>;
|
||||
className?: string;
|
||||
tabIndex?: number;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const calculateUrls = (url: string | undefined, urls: string[] | undefined, lowBandwidth: boolean): string[] => {
|
||||
const calculateUrls = (url: string, urls: string[], lowBandwidth: boolean): string[] => {
|
||||
// work out the full set of urls to try to load. This is formed like so:
|
||||
// imageUrls: [ props.url, ...props.urls ]
|
||||
|
||||
|
@ -72,26 +66,11 @@ const calculateUrls = (url: string | undefined, urls: string[] | undefined, lowB
|
|||
return Array.from(new Set(_urls));
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for cycling through a changing set of images.
|
||||
*
|
||||
* The set of images is updated whenever `url` or `urls` change, the user's
|
||||
* `lowBandwidth` preference changes, or the client reconnects.
|
||||
*
|
||||
* Returns `[imageUrl, onError]`. When `onError` is called, the next image in
|
||||
* the set will be displayed.
|
||||
*/
|
||||
const useImageUrl = ({
|
||||
url,
|
||||
urls,
|
||||
}: {
|
||||
url: string | undefined;
|
||||
urls: string[] | undefined;
|
||||
}): [string | undefined, () => void] => {
|
||||
const useImageUrl = ({ url, urls }): [string, () => void] => {
|
||||
// Since this is a hot code path and the settings store can be slow, we
|
||||
// use the cached lowBandwidth value from the room context if it exists
|
||||
const roomContext = useContext(RoomContext);
|
||||
const lowBandwidth = roomContext.lowBandwidth;
|
||||
const lowBandwidth = roomContext ? roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth");
|
||||
|
||||
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls, lowBandwidth));
|
||||
const [urlsIndex, setIndex] = useState<number>(0);
|
||||
|
@ -106,10 +85,10 @@ const useImageUrl = ({
|
|||
}, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const onClientSync = useCallback((syncState: SyncState, prevState: SyncState | null) => {
|
||||
const onClientSync = useCallback((syncState, prevState) => {
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
const reconnected = syncState !== SyncState.Error && prevState !== syncState;
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected) {
|
||||
setIndex(0);
|
||||
}
|
||||
|
@ -129,25 +108,46 @@ const BaseAvatar: React.FC<IProps> = (props) => {
|
|||
urls,
|
||||
width = 40,
|
||||
height = 40,
|
||||
resizeMethod = "crop", // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
defaultToInitialLetter = true,
|
||||
onClick,
|
||||
inputRef,
|
||||
className,
|
||||
style: parentStyle,
|
||||
resizeMethod: _unused, // to keep it from being in `otherProps`
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const style = {
|
||||
...parentStyle,
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
};
|
||||
|
||||
const [imageUrl, onError] = useImageUrl({ url, urls });
|
||||
|
||||
if (!imageUrl && defaultToInitialLetter && name) {
|
||||
const avatar = <TextAvatar name={name} idName={idName} width={width} height={height} title={title} />;
|
||||
const initialLetter = AvatarLogic.getInitialLetter(name);
|
||||
const textNode = (
|
||||
<span
|
||||
className="mx_BaseAvatar_initial"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
fontSize: toPx(width * 0.65),
|
||||
width: toPx(width),
|
||||
lineHeight: toPx(height),
|
||||
}}
|
||||
>
|
||||
{initialLetter}
|
||||
</span>
|
||||
);
|
||||
const imgNode = (
|
||||
<img
|
||||
className="mx_BaseAvatar_image"
|
||||
src={AvatarLogic.defaultAvatarUrlForString(idName || name)}
|
||||
alt=""
|
||||
title={title}
|
||||
onError={onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
aria-hidden="true"
|
||||
data-testid="avatar-img"
|
||||
/>
|
||||
);
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
|
@ -159,9 +159,9 @@ const BaseAvatar: React.FC<IProps> = (props) => {
|
|||
className={classNames("mx_BaseAvatar", className)}
|
||||
onClick={onClick}
|
||||
inputRef={inputRef}
|
||||
style={style}
|
||||
>
|
||||
{avatar}
|
||||
{textNode}
|
||||
{imgNode}
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
|
@ -170,10 +170,10 @@ const BaseAvatar: React.FC<IProps> = (props) => {
|
|||
className={classNames("mx_BaseAvatar", className)}
|
||||
ref={inputRef}
|
||||
{...otherProps}
|
||||
style={style}
|
||||
role="presentation"
|
||||
>
|
||||
{avatar}
|
||||
{textNode}
|
||||
{imgNode}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -187,7 +187,10 @@ const BaseAvatar: React.FC<IProps> = (props) => {
|
|||
src={imageUrl}
|
||||
onClick={onClick}
|
||||
onError={onError}
|
||||
style={style}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
title={title}
|
||||
alt={_t("Avatar")}
|
||||
inputRef={inputRef}
|
||||
|
@ -201,7 +204,10 @@ const BaseAvatar: React.FC<IProps> = (props) => {
|
|||
className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)}
|
||||
src={imageUrl}
|
||||
onError={onError}
|
||||
style={style}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
title={title}
|
||||
alt=""
|
||||
ref={inputRef}
|
||||
|
@ -214,31 +220,3 @@ const BaseAvatar: React.FC<IProps> = (props) => {
|
|||
|
||||
export default BaseAvatar;
|
||||
export type BaseAvatarType = React.FC<IProps>;
|
||||
|
||||
const TextAvatar: React.FC<{
|
||||
name: string;
|
||||
idName?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
title?: string;
|
||||
}> = ({ name, idName, width, height, title }) => {
|
||||
const initialLetter = AvatarLogic.getInitialLetter(name);
|
||||
|
||||
return (
|
||||
<span
|
||||
className="mx_BaseAvatar_image mx_BaseAvatar_initial"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
backgroundColor: AvatarLogic.getColorForString(idName || name),
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
fontSize: toPx(width * 0.65),
|
||||
lineHeight: toPx(height),
|
||||
}}
|
||||
title={title}
|
||||
data-testid="avatar-img"
|
||||
>
|
||||
{initialLetter}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016, 2019 - 2023 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -25,7 +26,6 @@ import { mediaFromMxc } from "../../../customisations/Media";
|
|||
import { CardContext } from "../right_panel/context";
|
||||
import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
|
||||
import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile";
|
||||
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
|
||||
|
||||
interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
|
||||
member: RoomMember | null;
|
||||
|
@ -33,13 +33,14 @@ interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" |
|
|||
width: number;
|
||||
height: number;
|
||||
resizeMethod?: ResizeMethod;
|
||||
/** Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser` */
|
||||
// The onClick to give the avatar
|
||||
onClick?: React.MouseEventHandler;
|
||||
// Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser`
|
||||
viewUserOnClick?: boolean;
|
||||
pushUserOnClick?: boolean;
|
||||
title?: string;
|
||||
style?: React.CSSProperties;
|
||||
/** true to deny `useOnlyCurrentProfiles` usage. Default false. */
|
||||
forceHistorical?: boolean;
|
||||
style?: any;
|
||||
forceHistorical?: boolean; // true to deny `useOnlyCurrentProfiles` usage. Default false.
|
||||
hideTitle?: boolean;
|
||||
}
|
||||
|
||||
|
@ -76,8 +77,8 @@ export default function MemberAvatar({
|
|||
|
||||
if (!title) {
|
||||
title =
|
||||
UserIdentifierCustomisations.getDisplayUserIdentifier!(member.userId, {
|
||||
roomId: member.roomId,
|
||||
UserIdentifierCustomisations.getDisplayUserIdentifier(member?.userId ?? "", {
|
||||
roomId: member?.roomId ?? "",
|
||||
}) ?? fallbackUserId;
|
||||
}
|
||||
}
|
||||
|
@ -87,6 +88,7 @@ export default function MemberAvatar({
|
|||
{...props}
|
||||
width={width}
|
||||
height={height}
|
||||
resizeMethod={resizeMethod}
|
||||
name={name ?? ""}
|
||||
title={hideTitle ? undefined : title}
|
||||
idName={member?.userId ?? fallbackUserId}
|
||||
|
@ -94,9 +96,9 @@ export default function MemberAvatar({
|
|||
onClick={
|
||||
viewUserOnClick
|
||||
? () => {
|
||||
dis.dispatch<ViewUserPayload>({
|
||||
dis.dispatch({
|
||||
action: Action.ViewUser,
|
||||
member: propsMember || undefined,
|
||||
member: propsMember,
|
||||
push: card.isCard,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -109,8 +109,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private onRoomAvatarClick = (): void => {
|
||||
const avatarMxc = this.props.room?.getMxcAvatarUrl();
|
||||
const avatarUrl = avatarMxc ? mediaFromMxc(avatarMxc).srcHttp : null;
|
||||
const avatarUrl = Avatar.avatarUrlForRoom(this.props.room, null, null, null);
|
||||
const params = {
|
||||
src: avatarUrl,
|
||||
name: this.props.room.name,
|
||||
|
|
33
src/components/views/dialogs/polls/PollHistoryDialog.tsx
Normal file
33
src/components/views/dialogs/polls/PollHistoryDialog.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import BaseDialog from "../BaseDialog";
|
||||
import { IDialogProps } from "../IDialogProps";
|
||||
|
||||
type PollHistoryDialogProps = Pick<IDialogProps, "onFinished"> & {
|
||||
roomId: string;
|
||||
};
|
||||
|
||||
export const PollHistoryDialog: React.FC<PollHistoryDialogProps> = ({ onFinished }) => {
|
||||
return (
|
||||
<BaseDialog title={_t("Polls history")} onFinished={onFinished}>
|
||||
{/* @TODO(kerrya) to be implemented in PSG-906 */}
|
||||
</BaseDialog>
|
||||
);
|
||||
};
|
|
@ -28,6 +28,7 @@ import EventTileBubble from "./EventTileBubble";
|
|||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import { useRoomState } from "../../../hooks/useRoomState";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
interface IProps {
|
||||
/** The m.room.create MatrixEvent that this tile represents */
|
||||
|
@ -40,6 +41,8 @@ interface IProps {
|
|||
* room.
|
||||
*/
|
||||
export const RoomCreate: React.FC<IProps> = ({ mxEvent, timestamp }) => {
|
||||
const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors");
|
||||
|
||||
// Note: we ask the room for its predecessor here, instead of directly using
|
||||
// the information inside mxEvent. This allows us the flexibility later to
|
||||
// use a different predecessor (e.g. through MSC3946) and still display it
|
||||
|
@ -47,7 +50,10 @@ export const RoomCreate: React.FC<IProps> = ({ mxEvent, timestamp }) => {
|
|||
const roomContext = useContext(RoomContext);
|
||||
const predecessor = useRoomState(
|
||||
roomContext.room,
|
||||
useCallback((state) => state.findPredecessor(), []),
|
||||
useCallback(
|
||||
(state) => state.findPredecessor(msc3946ProcessDynamicPredecessor),
|
||||
[msc3946ProcessDynamicPredecessor],
|
||||
),
|
||||
);
|
||||
|
||||
const onLinkClicked = useCallback(
|
||||
|
|
|
@ -23,7 +23,7 @@ import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
|
|||
import BaseCard, { Group } from "./BaseCard";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import AccessibleButton, { ButtonEvent, IAccessibleButtonProps } from "../elements/AccessibleButton";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import Modal from "../../../Modal";
|
||||
|
@ -51,6 +51,7 @@ import ExportDialog from "../dialogs/ExportDialog";
|
|||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { PollHistoryDialog } from "../dialogs/polls/PollHistoryDialog";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -61,14 +62,15 @@ interface IAppsSectionProps {
|
|||
room: Room;
|
||||
}
|
||||
|
||||
interface IButtonProps {
|
||||
interface IButtonProps extends IAccessibleButtonProps {
|
||||
className: string;
|
||||
onClick(ev: ButtonEvent): void;
|
||||
}
|
||||
|
||||
const Button: React.FC<IButtonProps> = ({ children, className, onClick }) => {
|
||||
const Button: React.FC<IButtonProps> = ({ children, className, onClick, ...props }) => {
|
||||
return (
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
className={classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
|
@ -281,6 +283,12 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
|||
});
|
||||
};
|
||||
|
||||
const onRoomPollHistoryClick = (): void => {
|
||||
Modal.createDialog(PollHistoryDialog, {
|
||||
roomId: room.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
const isRoomEncrypted = useIsEncrypted(cli, room);
|
||||
const roomContext = useContext(RoomContext);
|
||||
const e2eStatus = roomContext.e2eStatus;
|
||||
|
@ -315,6 +323,8 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
|||
const pinningEnabled = useFeatureEnabled("feature_pinning");
|
||||
const pinCount = usePinnedEvents(pinningEnabled && room)?.length;
|
||||
|
||||
const isPollHistoryEnabled = useFeatureEnabled("feature_poll_history");
|
||||
|
||||
return (
|
||||
<BaseCard header={header} className="mx_RoomSummaryCard" onClose={onClose}>
|
||||
<Group title={_t("About")} className="mx_RoomSummaryCard_aboutGroup">
|
||||
|
@ -327,6 +337,11 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
|||
{_t("Files")}
|
||||
</Button>
|
||||
)}
|
||||
{!isVideoRoom && isPollHistoryEnabled && (
|
||||
<Button className="mx_RoomSummaryCard_icon_poll" onClick={onRoomPollHistoryClick}>
|
||||
{_t("Polls history")}
|
||||
</Button>
|
||||
)}
|
||||
{pinningEnabled && !isVideoRoom && (
|
||||
<Button className="mx_RoomSummaryCard_icon_pins" onClick={onRoomPinsClick}>
|
||||
{_t("Pinned")}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue