Apply prettier formatting
This commit is contained in:
parent
1cac306093
commit
526645c791
1576 changed files with 65385 additions and 62478 deletions
|
@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { forwardRef, ReactNode, KeyboardEvent, Ref } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React, { forwardRef, ReactNode, KeyboardEvent, Ref } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import RightPanelStore from '../../../stores/right-panel/RightPanelStore';
|
||||
import { backLabelForPhase } from '../../../stores/right-panel/RightPanelStorePhases';
|
||||
import { CardContext } from './context';
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import { backLabelForPhase } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import { CardContext } from "./context";
|
||||
|
||||
interface IProps {
|
||||
header?: ReactNode;
|
||||
|
@ -43,64 +43,58 @@ interface IGroupProps {
|
|||
}
|
||||
|
||||
export const Group: React.FC<IGroupProps> = ({ className, title, children }) => {
|
||||
return <div className={classNames("mx_BaseCard_Group", className)}>
|
||||
<h1>{ title }</h1>
|
||||
{ children }
|
||||
</div>;
|
||||
return (
|
||||
<div className={classNames("mx_BaseCard_Group", className)}>
|
||||
<h1>{title}</h1>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(({
|
||||
closeLabel,
|
||||
onClose,
|
||||
onBack,
|
||||
className,
|
||||
header,
|
||||
footer,
|
||||
withoutScrollContainer,
|
||||
children,
|
||||
onKeyDown,
|
||||
}, ref) => {
|
||||
let backButton;
|
||||
const cardHistory = RightPanelStore.instance.roomPhaseHistory;
|
||||
if (cardHistory.length > 1) {
|
||||
const prevCard = cardHistory[cardHistory.length - 2];
|
||||
const onBackClick = (ev: ButtonEvent) => {
|
||||
onBack?.(ev);
|
||||
RightPanelStore.instance.popCard();
|
||||
};
|
||||
const label = backLabelForPhase(prevCard.phase) ?? _t("Back");
|
||||
backButton = <AccessibleButton className="mx_BaseCard_back" onClick={onBackClick} title={label} />;
|
||||
}
|
||||
const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
|
||||
({ closeLabel, onClose, onBack, className, header, footer, withoutScrollContainer, children, onKeyDown }, ref) => {
|
||||
let backButton;
|
||||
const cardHistory = RightPanelStore.instance.roomPhaseHistory;
|
||||
if (cardHistory.length > 1) {
|
||||
const prevCard = cardHistory[cardHistory.length - 2];
|
||||
const onBackClick = (ev: ButtonEvent) => {
|
||||
onBack?.(ev);
|
||||
RightPanelStore.instance.popCard();
|
||||
};
|
||||
const label = backLabelForPhase(prevCard.phase) ?? _t("Back");
|
||||
backButton = <AccessibleButton className="mx_BaseCard_back" onClick={onBackClick} title={label} />;
|
||||
}
|
||||
|
||||
let closeButton;
|
||||
if (onClose) {
|
||||
closeButton = <AccessibleButton
|
||||
data-test-id='base-card-close-button'
|
||||
className="mx_BaseCard_close"
|
||||
onClick={onClose}
|
||||
title={closeLabel || _t("Close")}
|
||||
/>;
|
||||
}
|
||||
let closeButton;
|
||||
if (onClose) {
|
||||
closeButton = (
|
||||
<AccessibleButton
|
||||
data-test-id="base-card-close-button"
|
||||
className="mx_BaseCard_close"
|
||||
onClick={onClose}
|
||||
title={closeLabel || _t("Close")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!withoutScrollContainer) {
|
||||
children = <AutoHideScrollbar>
|
||||
{ children }
|
||||
</AutoHideScrollbar>;
|
||||
}
|
||||
if (!withoutScrollContainer) {
|
||||
children = <AutoHideScrollbar>{children}</AutoHideScrollbar>;
|
||||
}
|
||||
|
||||
return (
|
||||
<CardContext.Provider value={{ isCard: true }}>
|
||||
<div className={classNames("mx_BaseCard", className)} ref={ref} onKeyDown={onKeyDown}>
|
||||
<div className="mx_BaseCard_header">
|
||||
{ backButton }
|
||||
{ closeButton }
|
||||
{ header }
|
||||
return (
|
||||
<CardContext.Provider value={{ isCard: true }}>
|
||||
<div className={classNames("mx_BaseCard", className)} ref={ref} onKeyDown={onKeyDown}>
|
||||
<div className="mx_BaseCard_header">
|
||||
{backButton}
|
||||
{closeButton}
|
||||
{header}
|
||||
</div>
|
||||
{children}
|
||||
{footer && <div className="mx_BaseCard_footer">{footer}</div>}
|
||||
</div>
|
||||
{ children }
|
||||
{ footer && <div className="mx_BaseCard_footer">{ footer }</div> }
|
||||
</div>
|
||||
</CardContext.Provider>
|
||||
);
|
||||
});
|
||||
</CardContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default BaseCard;
|
||||
|
|
|
@ -23,10 +23,12 @@ import AccessibleButton from "../elements/AccessibleButton";
|
|||
import Spinner from "../elements/Spinner";
|
||||
|
||||
export const PendingActionSpinner = ({ text }) => {
|
||||
return <div className="mx_EncryptionInfo_spinner">
|
||||
<Spinner />
|
||||
{ text }
|
||||
</div>;
|
||||
return (
|
||||
<div className="mx_EncryptionInfo_spinner">
|
||||
<Spinner />
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
|
@ -50,11 +52,7 @@ const EncryptionInfo: React.FC<IProps> = ({
|
|||
}: IProps) => {
|
||||
let content: JSX.Element;
|
||||
if (waitingForOtherParty && isSelfVerification) {
|
||||
content = (
|
||||
<div>
|
||||
{ _t("To proceed, please accept the verification request on your other device.") }
|
||||
</div>
|
||||
);
|
||||
content = <div>{_t("To proceed, please accept the verification request on your other device.")}</div>;
|
||||
} else if (waitingForOtherParty || waitingForNetwork) {
|
||||
let text: string;
|
||||
if (waitingForOtherParty) {
|
||||
|
@ -72,7 +70,7 @@ const EncryptionInfo: React.FC<IProps> = ({
|
|||
className="mx_UserInfo_wideButton mx_UserInfo_startVerification"
|
||||
onClick={onStartVerification}
|
||||
>
|
||||
{ _t("Start Verification") }
|
||||
{_t("Start Verification")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -81,17 +79,25 @@ const EncryptionInfo: React.FC<IProps> = ({
|
|||
if (isRoomEncrypted) {
|
||||
description = (
|
||||
<div>
|
||||
<p>{ _t("Messages in this room are end-to-end encrypted.") }</p>
|
||||
<p>{ _t("Your messages are secured and only you and the recipient have " +
|
||||
"the unique keys to unlock them.") }</p>
|
||||
<p>{_t("Messages in this room are end-to-end encrypted.")}</p>
|
||||
<p>
|
||||
{_t(
|
||||
"Your messages are secured and only you and the recipient have " +
|
||||
"the unique keys to unlock them.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
description = (
|
||||
<div>
|
||||
<p>{ _t("Messages in this room are not end-to-end encrypted.") }</p>
|
||||
<p>{ _t("In encrypted rooms, your messages are secured and only you and the recipient have " +
|
||||
"the unique keys to unlock them.") }</p>
|
||||
<p>{_t("Messages in this room are not end-to-end encrypted.")}</p>
|
||||
<p>
|
||||
{_t(
|
||||
"In encrypted rooms, your messages are secured and only you and the recipient have " +
|
||||
"the unique keys to unlock them.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -100,20 +106,26 @@ const EncryptionInfo: React.FC<IProps> = ({
|
|||
return content;
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<div data-test-id='encryption-info-description' className="mx_UserInfo_container">
|
||||
<h3>{ _t("Encryption") }</h3>
|
||||
{ description }
|
||||
</div>
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Verify User") }</h3>
|
||||
<div>
|
||||
<p>{ _t("For extra security, verify this user by checking a one-time code on both of your devices.") }</p>
|
||||
<p>{ _t("To be secure, do this in person or use a trusted way to communicate.") }</p>
|
||||
{ content }
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div data-test-id="encryption-info-description" className="mx_UserInfo_container">
|
||||
<h3>{_t("Encryption")}</h3>
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{_t("Verify User")}</h3>
|
||||
<div>
|
||||
<p>
|
||||
{_t(
|
||||
"For extra security, verify this user by checking a one-time code on both of your devices.",
|
||||
)}
|
||||
</p>
|
||||
<p>{_t("To be secure, do this in person or use a trusted way to communicate.")}</p>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default EncryptionInfo;
|
||||
|
|
|
@ -31,7 +31,7 @@ import { ensureDMExists } from "../../../createRoom";
|
|||
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import Modal from "../../../Modal";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
|
||||
|
@ -48,14 +48,7 @@ interface IProps {
|
|||
}
|
||||
|
||||
const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
|
||||
const {
|
||||
verificationRequest,
|
||||
verificationRequestPromise,
|
||||
member,
|
||||
onClose,
|
||||
layout,
|
||||
isRoomEncrypted,
|
||||
} = props;
|
||||
const { verificationRequest, verificationRequestPromise, member, onClose, layout, isRoomEncrypted } = props;
|
||||
const [request, setRequest] = useState(verificationRequest);
|
||||
// state to show a spinner immediately after clicking "start verification",
|
||||
// before we have a request
|
||||
|
@ -87,15 +80,17 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
|
|||
Modal.createDialog(ErrorDialog, {
|
||||
headerImage: require("../../../../res/img/e2e/warning-deprecated.svg").default,
|
||||
title: _t("Your messages are not secure"),
|
||||
description: <div>
|
||||
{ _t("One of the following may be compromised:") }
|
||||
<ul>
|
||||
<li>{ _t("Your homeserver") }</li>
|
||||
<li>{ _t("The homeserver the user you're verifying is connected to") }</li>
|
||||
<li>{ _t("Yours, or the other users' internet connection") }</li>
|
||||
<li>{ _t("Yours, or the other users' session") }</li>
|
||||
</ul>
|
||||
</div>,
|
||||
description: (
|
||||
<div>
|
||||
{_t("One of the following may be compromised:")}
|
||||
<ul>
|
||||
<li>{_t("Your homeserver")}</li>
|
||||
<li>{_t("The homeserver the user you're verifying is connected to")}</li>
|
||||
<li>{_t("Yours, or the other users' internet connection")}</li>
|
||||
<li>{_t("Yours, or the other users' session")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
onFinished: onClose,
|
||||
});
|
||||
return; // don't update phase here as we will be transitioning away from this view shortly
|
||||
|
@ -141,9 +136,9 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
|
|||
const requested =
|
||||
(!request && isRequesting) ||
|
||||
(request && (phase === PHASE_REQUESTED || phase === PHASE_UNSENT || phase === undefined));
|
||||
const isSelfVerification = request ?
|
||||
request.isSelfVerification :
|
||||
member.userId === MatrixClientPeg.get().getUserId();
|
||||
const isSelfVerification = request
|
||||
? request.isSelfVerification
|
||||
: member.userId === MatrixClientPeg.get().getUserId();
|
||||
|
||||
if (!request || requested) {
|
||||
const initiatedByMe = (!request && isRequesting) || (request && request.initiatedByMe);
|
||||
|
@ -155,7 +150,8 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
|
|||
isSelfVerification={isSelfVerification}
|
||||
waitingForOtherParty={requested && initiatedByMe}
|
||||
waitingForNetwork={requested && !initiatedByMe}
|
||||
inDialog={layout === "dialog"} />
|
||||
inDialog={layout === "dialog"}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
|
|
|
@ -18,8 +18,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
|
@ -50,14 +50,16 @@ export default class HeaderButton extends React.Component<IProps> {
|
|||
[`mx_RightPanel_${name}`]: true,
|
||||
});
|
||||
|
||||
return <AccessibleTooltipButton
|
||||
{...props}
|
||||
aria-selected={isHighlighted}
|
||||
role="tab"
|
||||
title={title}
|
||||
alignment={Alignment.Bottom}
|
||||
className={classes}
|
||||
onClick={onClick}
|
||||
/>;
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
{...props}
|
||||
aria-selected={isHighlighted}
|
||||
role="tab"
|
||||
title={title}
|
||||
alignment={Alignment.Bottom}
|
||||
className={classes}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,17 +18,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
|
||||
import { IRightPanelCardState } from '../../../stores/right-panel/RightPanelStoreIPanelState';
|
||||
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
|
||||
import { NotificationColor } from '../../../stores/notifications/NotificationColor';
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import { IRightPanelCardState } from "../../../stores/right-panel/RightPanelStoreIPanelState";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||
|
||||
export enum HeaderKind {
|
||||
Room = "room",
|
||||
Room = "room",
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -97,8 +97,10 @@ export default abstract class HeaderButtons<P = {}> extends React.Component<IPro
|
|||
public abstract renderButtons(): JSX.Element;
|
||||
|
||||
public render() {
|
||||
return <div className="mx_HeaderButtons" role="tablist">
|
||||
{ this.renderButtons() }
|
||||
</div>;
|
||||
return (
|
||||
<div className="mx_HeaderButtons" role="tablist">
|
||||
{this.renderButtons()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,13 +17,13 @@ limitations under the License.
|
|||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventType, RelationType } from 'matrix-js-sdk/src/@types/event';
|
||||
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
|
||||
import { Icon as ContextMenuIcon } from '../../../../res/img/element-icons/context-menu.svg';
|
||||
import { Icon as ContextMenuIcon } from "../../../../res/img/element-icons/context-menu.svg";
|
||||
import { Icon as EmojiIcon } from "../../../../res/img/element-icons/room/message-bar/emoji.svg";
|
||||
import { Icon as ReplyIcon } from '../../../../res/img/element-icons/room/message-bar/reply.svg';
|
||||
import { Icon as ReplyIcon } from "../../../../res/img/element-icons/room/message-bar/reply.svg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import BaseCard from "./BaseCard";
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
@ -35,7 +35,7 @@ import PinnedEventTile from "../rooms/PinnedEventTile";
|
|||
import { useRoomState } from "../../../hooks/useRoomState";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import { ReadPinsEventId } from "./types";
|
||||
import Heading from '../typography/Heading';
|
||||
import Heading from "../typography/Heading";
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
|
||||
interface IProps {
|
||||
|
@ -47,11 +47,16 @@ interface IProps {
|
|||
export const usePinnedEvents = (room: Room): string[] => {
|
||||
const [pinnedEvents, setPinnedEvents] = useState<string[]>([]);
|
||||
|
||||
const update = useCallback((ev?: MatrixEvent) => {
|
||||
if (!room) return;
|
||||
if (ev && ev.getType() !== EventType.RoomPinnedEvents) return;
|
||||
setPinnedEvents(room.currentState.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent()?.pinned || []);
|
||||
}, [room]);
|
||||
const update = useCallback(
|
||||
(ev?: MatrixEvent) => {
|
||||
if (!room) return;
|
||||
if (ev && ev.getType() !== EventType.RoomPinnedEvents) return;
|
||||
setPinnedEvents(
|
||||
room.currentState.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent()?.pinned || [],
|
||||
);
|
||||
},
|
||||
[room],
|
||||
);
|
||||
|
||||
useTypedEventEmitter(room?.currentState, RoomStateEvent.Events, update);
|
||||
useEffect(() => {
|
||||
|
@ -66,12 +71,15 @@ export const usePinnedEvents = (room: Room): string[] => {
|
|||
export const useReadPinnedEvents = (room: Room): Set<string> => {
|
||||
const [readPinnedEvents, setReadPinnedEvents] = useState<Set<string>>(new Set());
|
||||
|
||||
const update = useCallback((ev?: MatrixEvent) => {
|
||||
if (!room) return;
|
||||
if (ev && ev.getType() !== ReadPinsEventId) return;
|
||||
const readPins = room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids;
|
||||
setReadPinnedEvents(new Set(readPins || []));
|
||||
}, [room]);
|
||||
const update = useCallback(
|
||||
(ev?: MatrixEvent) => {
|
||||
if (!room) return;
|
||||
if (ev && ev.getType() !== ReadPinsEventId) return;
|
||||
const readPins = room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids;
|
||||
setReadPinnedEvents(new Set(readPins || []));
|
||||
},
|
||||
[room],
|
||||
);
|
||||
|
||||
useTypedEventEmitter(room, RoomEvent.AccountData, update);
|
||||
useEffect(() => {
|
||||
|
@ -86,12 +94,12 @@ export const useReadPinnedEvents = (room: Room): Set<string> => {
|
|||
const PinnedMessagesCard = ({ room, onClose, permalinkCreator }: IProps) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const roomContext = useContext(RoomContext);
|
||||
const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli));
|
||||
const canUnpin = useRoomState(room, (state) => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli));
|
||||
const pinnedEventIds = usePinnedEvents(room);
|
||||
const readPinnedEvents = useReadPinnedEvents(room);
|
||||
|
||||
useEffect(() => {
|
||||
const newlyRead = pinnedEventIds.filter(id => !readPinnedEvents.has(id));
|
||||
const newlyRead = pinnedEventIds.filter((id) => !readPinnedEvents.has(id));
|
||||
if (newlyRead.length > 0) {
|
||||
// clear out any read pinned events which no longer are pinned
|
||||
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
||||
|
@ -100,40 +108,52 @@ const PinnedMessagesCard = ({ room, onClose, permalinkCreator }: IProps) => {
|
|||
}
|
||||
}, [cli, room.roomId, pinnedEventIds, readPinnedEvents]);
|
||||
|
||||
const pinnedEvents = useAsyncMemo(() => {
|
||||
const promises = pinnedEventIds.map(async eventId => {
|
||||
const timelineSet = room.getUnfilteredTimelineSet();
|
||||
const localEvent = timelineSet?.getTimelineForEvent(eventId)?.getEvents().find(e => e.getId() === eventId);
|
||||
if (localEvent) return PinningUtils.isPinnable(localEvent) ? localEvent : null;
|
||||
const pinnedEvents = useAsyncMemo(
|
||||
() => {
|
||||
const promises = pinnedEventIds.map(async (eventId) => {
|
||||
const timelineSet = room.getUnfilteredTimelineSet();
|
||||
const localEvent = timelineSet
|
||||
?.getTimelineForEvent(eventId)
|
||||
?.getEvents()
|
||||
.find((e) => e.getId() === eventId);
|
||||
if (localEvent) return PinningUtils.isPinnable(localEvent) ? localEvent : null;
|
||||
|
||||
try {
|
||||
// Fetch the event and latest edit in parallel
|
||||
const [evJson, { events: [edit] }] = await Promise.all([
|
||||
cli.fetchRoomEvent(room.roomId, eventId),
|
||||
cli.relations(room.roomId, eventId, RelationType.Replace, null, { limit: 1 }),
|
||||
]);
|
||||
const event = new MatrixEvent(evJson);
|
||||
if (event.isEncrypted()) {
|
||||
await cli.decryptEventIfNeeded(event); // TODO await?
|
||||
try {
|
||||
// Fetch the event and latest edit in parallel
|
||||
const [
|
||||
evJson,
|
||||
{
|
||||
events: [edit],
|
||||
},
|
||||
] = await Promise.all([
|
||||
cli.fetchRoomEvent(room.roomId, eventId),
|
||||
cli.relations(room.roomId, eventId, RelationType.Replace, null, { limit: 1 }),
|
||||
]);
|
||||
const event = new MatrixEvent(evJson);
|
||||
if (event.isEncrypted()) {
|
||||
await cli.decryptEventIfNeeded(event); // TODO await?
|
||||
}
|
||||
|
||||
if (event && PinningUtils.isPinnable(event)) {
|
||||
// Inject sender information
|
||||
event.sender = room.getMember(event.getSender());
|
||||
// Also inject any edits we've found
|
||||
if (edit) event.makeReplaced(edit);
|
||||
|
||||
return event;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error looking up pinned event " + eventId + " in room " + room.roomId);
|
||||
logger.error(err);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (event && PinningUtils.isPinnable(event)) {
|
||||
// Inject sender information
|
||||
event.sender = room.getMember(event.getSender());
|
||||
// Also inject any edits we've found
|
||||
if (edit) event.makeReplaced(edit);
|
||||
|
||||
return event;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error looking up pinned event " + eventId + " in room " + room.roomId);
|
||||
logger.error(err);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}, [cli, room, pinnedEventIds], null);
|
||||
return Promise.all(promises);
|
||||
},
|
||||
[cli, room, pinnedEventIds],
|
||||
null,
|
||||
);
|
||||
|
||||
let content;
|
||||
if (!pinnedEvents) {
|
||||
|
@ -152,53 +172,72 @@ const PinnedMessagesCard = ({ room, onClose, permalinkCreator }: IProps) => {
|
|||
};
|
||||
|
||||
// show them in reverse, with latest pinned at the top
|
||||
content = pinnedEvents.filter(Boolean).reverse().map(ev => (
|
||||
<PinnedEventTile
|
||||
key={ev.getId()}
|
||||
event={ev}
|
||||
onUnpinClicked={canUnpin ? () => onUnpinClicked(ev) : undefined}
|
||||
permalinkCreator={permalinkCreator}
|
||||
/>
|
||||
));
|
||||
content = pinnedEvents
|
||||
.filter(Boolean)
|
||||
.reverse()
|
||||
.map((ev) => (
|
||||
<PinnedEventTile
|
||||
key={ev.getId()}
|
||||
event={ev}
|
||||
onUnpinClicked={canUnpin ? () => onUnpinClicked(ev) : undefined}
|
||||
permalinkCreator={permalinkCreator}
|
||||
/>
|
||||
));
|
||||
} else {
|
||||
content = <div className="mx_PinnedMessagesCard_empty_wrapper">
|
||||
<div className="mx_PinnedMessagesCard_empty">
|
||||
{ /* XXX: We reuse the classes for simplicity, but deliberately not the components for non-interactivity. */ }
|
||||
<div className="mx_MessageActionBar mx_PinnedMessagesCard_MessageActionBar">
|
||||
<div className="mx_MessageActionBar_iconButton">
|
||||
<EmojiIcon />
|
||||
content = (
|
||||
<div className="mx_PinnedMessagesCard_empty_wrapper">
|
||||
<div className="mx_PinnedMessagesCard_empty">
|
||||
{/* XXX: We reuse the classes for simplicity, but deliberately not the components for non-interactivity. */}
|
||||
<div className="mx_MessageActionBar mx_PinnedMessagesCard_MessageActionBar">
|
||||
<div className="mx_MessageActionBar_iconButton">
|
||||
<EmojiIcon />
|
||||
</div>
|
||||
<div className="mx_MessageActionBar_iconButton">
|
||||
<ReplyIcon />
|
||||
</div>
|
||||
<div className="mx_MessageActionBar_iconButton mx_MessageActionBar_optionsButton">
|
||||
<ContextMenuIcon />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_MessageActionBar_iconButton">
|
||||
<ReplyIcon />
|
||||
</div>
|
||||
<div className="mx_MessageActionBar_iconButton mx_MessageActionBar_optionsButton">
|
||||
<ContextMenuIcon />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Heading size="h4" className="mx_PinnedMessagesCard_empty_header">{ _t("Nothing pinned, yet") }</Heading>
|
||||
{ _t("If you have permissions, open the menu on any message and select " +
|
||||
"<b>Pin</b> to stick them here.", {}, {
|
||||
b: sub => <b>{ sub }</b>,
|
||||
}) }
|
||||
<Heading size="h4" className="mx_PinnedMessagesCard_empty_header">
|
||||
{_t("Nothing pinned, yet")}
|
||||
</Heading>
|
||||
{_t(
|
||||
"If you have permissions, open the menu on any message and select " +
|
||||
"<b>Pin</b> to stick them here.",
|
||||
{},
|
||||
{
|
||||
b: (sub) => <b>{sub}</b>,
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
);
|
||||
}
|
||||
|
||||
return <BaseCard
|
||||
header={<div className="mx_BaseCard_header_title">
|
||||
<Heading size="h4" className="mx_BaseCard_header_title_heading">{ _t("Pinned messages") }</Heading>
|
||||
</div>}
|
||||
className="mx_PinnedMessagesCard"
|
||||
onClose={onClose}
|
||||
>
|
||||
<RoomContext.Provider value={{
|
||||
...roomContext,
|
||||
timelineRenderingType: TimelineRenderingType.Pinned,
|
||||
}}>
|
||||
{ content }
|
||||
</RoomContext.Provider>
|
||||
</BaseCard>;
|
||||
return (
|
||||
<BaseCard
|
||||
header={
|
||||
<div className="mx_BaseCard_header_title">
|
||||
<Heading size="h4" className="mx_BaseCard_header_title_heading">
|
||||
{_t("Pinned messages")}
|
||||
</Heading>
|
||||
</div>
|
||||
}
|
||||
className="mx_PinnedMessagesCard"
|
||||
onClose={onClose}
|
||||
>
|
||||
<RoomContext.Provider
|
||||
value={{
|
||||
...roomContext,
|
||||
timelineRenderingType: TimelineRenderingType.Pinned,
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</RoomContext.Provider>
|
||||
</BaseCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default PinnedMessagesCard;
|
||||
|
|
|
@ -23,14 +23,14 @@ import classNames from "classnames";
|
|||
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import HeaderButton from './HeaderButton';
|
||||
import HeaderButtons, { HeaderKind } from './HeaderButtons';
|
||||
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import HeaderButton from "./HeaderButton";
|
||||
import HeaderButtons, { HeaderKind } from "./HeaderButtons";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard';
|
||||
import { useReadPinnedEvents, usePinnedEvents } from "./PinnedMessagesCard";
|
||||
import { showThreadPanel } from "../../../dispatcher/dispatch-actions/threads";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {
|
||||
|
@ -65,16 +65,18 @@ const UnreadIndicator = ({ color }: IUnreadIndicatorProps) => {
|
|||
}
|
||||
|
||||
const classes = classNames({
|
||||
"mx_Indicator": true,
|
||||
"mx_RightPanel_headerButton_unreadIndicator": true,
|
||||
"mx_Indicator_bold": color === NotificationColor.Bold,
|
||||
"mx_Indicator_gray": color === NotificationColor.Grey,
|
||||
"mx_Indicator_red": color === NotificationColor.Red,
|
||||
mx_Indicator: true,
|
||||
mx_RightPanel_headerButton_unreadIndicator: true,
|
||||
mx_Indicator_bold: color === NotificationColor.Bold,
|
||||
mx_Indicator_gray: color === NotificationColor.Grey,
|
||||
mx_Indicator_red: color === NotificationColor.Red,
|
||||
});
|
||||
return <>
|
||||
<div className="mx_RightPanel_headerButton_unreadIndicator_bg" />
|
||||
<div className={classes} />
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
<div className="mx_RightPanel_headerButton_unreadIndicator_bg" />
|
||||
<div className={classes} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IHeaderButtonProps {
|
||||
|
@ -89,19 +91,21 @@ const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }: IHeaderBut
|
|||
if (!pinnedEvents?.length) return null;
|
||||
|
||||
let unreadIndicator;
|
||||
if (pinnedEvents.some(id => !readPinnedEvents.has(id))) {
|
||||
if (pinnedEvents.some((id) => !readPinnedEvents.has(id))) {
|
||||
unreadIndicator = <UnreadIndicator />;
|
||||
}
|
||||
|
||||
return <HeaderButton
|
||||
name="pinnedMessagesButton"
|
||||
title={_t("Pinned messages")}
|
||||
isHighlighted={isHighlighted}
|
||||
isUnread={!!unreadIndicator}
|
||||
onClick={onClick}
|
||||
>
|
||||
{ unreadIndicator }
|
||||
</HeaderButton>;
|
||||
return (
|
||||
<HeaderButton
|
||||
name="pinnedMessagesButton"
|
||||
title={_t("Pinned messages")}
|
||||
isHighlighted={isHighlighted}
|
||||
isUnread={!!unreadIndicator}
|
||||
onClick={onClick}
|
||||
>
|
||||
{unreadIndicator}
|
||||
</HeaderButton>
|
||||
);
|
||||
};
|
||||
|
||||
const TimelineCardHeaderButton = ({ room, isHighlighted, onClick }: IHeaderButtonProps) => {
|
||||
|
@ -113,14 +117,11 @@ const TimelineCardHeaderButton = ({ room, isHighlighted, onClick }: IHeaderButto
|
|||
case NotificationColor.Red:
|
||||
unreadIndicator = <UnreadIndicator color={color} />;
|
||||
}
|
||||
return <HeaderButton
|
||||
name="timelineCardButton"
|
||||
title={_t("Chat")}
|
||||
isHighlighted={isHighlighted}
|
||||
onClick={onClick}
|
||||
>
|
||||
{ unreadIndicator }
|
||||
</HeaderButton>;
|
||||
return (
|
||||
<HeaderButton name="timelineCardButton" title={_t("Chat")} isHighlighted={isHighlighted} onClick={onClick}>
|
||||
{unreadIndicator}
|
||||
</HeaderButton>
|
||||
);
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
|
@ -129,10 +130,7 @@ interface IProps {
|
|||
}
|
||||
|
||||
export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||
private static readonly THREAD_PHASES = [
|
||||
RightPanelPhases.ThreadPanel,
|
||||
RightPanelPhases.ThreadView,
|
||||
];
|
||||
private static readonly THREAD_PHASES = [RightPanelPhases.ThreadPanel, RightPanelPhases.ThreadView];
|
||||
private threadNotificationState: ThreadsRoomNotificationState | null;
|
||||
private globalNotificationState: SummarizedNotificationState;
|
||||
|
||||
|
@ -144,9 +142,10 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
constructor(props: IProps) {
|
||||
super(props, HeaderKind.Room);
|
||||
|
||||
this.threadNotificationState = !this.supportsThreadNotifications && this.props.room
|
||||
? RoomNotificationStateStore.instance.getThreadsRoomState(this.props.room)
|
||||
: null;
|
||||
this.threadNotificationState =
|
||||
!this.supportsThreadNotifications && this.props.room
|
||||
? RoomNotificationStateStore.instance.getThreadsRoomState(this.props.room)
|
||||
: null;
|
||||
this.globalNotificationState = RoomNotificationStateStore.instance.globalState;
|
||||
}
|
||||
|
||||
|
@ -209,9 +208,10 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
if (payload.action === Action.ViewUser) {
|
||||
if (payload.member) {
|
||||
if (payload.push) {
|
||||
RightPanelStore.instance.pushCard(
|
||||
{ phase: RightPanelPhases.RoomMemberInfo, state: { member: payload.member } },
|
||||
);
|
||||
RightPanelStore.instance.pushCard({
|
||||
phase: RightPanelPhases.RoomMemberInfo,
|
||||
state: { member: payload.member },
|
||||
});
|
||||
} else {
|
||||
RightPanelStore.instance.setCards([
|
||||
{ phase: RightPanelPhases.RoomSummary },
|
||||
|
@ -276,24 +276,29 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
const rightPanelPhaseButtons: Map<RightPanelPhases, any> = new Map();
|
||||
|
||||
if (SettingsStore.getValue("feature_pinning")) {
|
||||
rightPanelPhaseButtons.set(RightPanelPhases.PinnedMessages,
|
||||
rightPanelPhaseButtons.set(
|
||||
RightPanelPhases.PinnedMessages,
|
||||
<PinnedMessagesHeaderButton
|
||||
key="pinnedMessagesButton"
|
||||
room={this.props.room}
|
||||
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
|
||||
onClick={this.onPinnedMessagesClicked} />,
|
||||
onClick={this.onPinnedMessagesClicked}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
rightPanelPhaseButtons.set(RightPanelPhases.Timeline,
|
||||
rightPanelPhaseButtons.set(
|
||||
RightPanelPhases.Timeline,
|
||||
<TimelineCardHeaderButton
|
||||
key="timelineButton"
|
||||
room={this.props.room}
|
||||
isHighlighted={this.isPhase(RightPanelPhases.Timeline)}
|
||||
onClick={this.onTimelineCardClicked} />,
|
||||
onClick={this.onTimelineCardClicked}
|
||||
/>,
|
||||
);
|
||||
rightPanelPhaseButtons.set(RightPanelPhases.ThreadPanel,
|
||||
SettingsStore.getValue("feature_thread")
|
||||
? <HeaderButton
|
||||
rightPanelPhaseButtons.set(
|
||||
RightPanelPhases.ThreadPanel,
|
||||
SettingsStore.getValue("feature_thread") ? (
|
||||
<HeaderButton
|
||||
key={RightPanelPhases.ThreadPanel}
|
||||
name="threadsButton"
|
||||
data-testid="threadsButton"
|
||||
|
@ -304,39 +309,42 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
>
|
||||
<UnreadIndicator color={this.state.threadNotificationColor} />
|
||||
</HeaderButton>
|
||||
: null,
|
||||
) : null,
|
||||
);
|
||||
rightPanelPhaseButtons.set(RightPanelPhases.NotificationPanel,
|
||||
rightPanelPhaseButtons.set(
|
||||
RightPanelPhases.NotificationPanel,
|
||||
<HeaderButton
|
||||
key="notifsButton"
|
||||
name="notifsButton"
|
||||
title={_t('Notifications')}
|
||||
title={_t("Notifications")}
|
||||
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
|
||||
onClick={this.onNotificationsClicked}
|
||||
isUnread={this.globalNotificationState.color === NotificationColor.Red}
|
||||
>
|
||||
{ this.globalNotificationState.color === NotificationColor.Red ?
|
||||
<UnreadIndicator color={this.globalNotificationState.color} /> :
|
||||
null }
|
||||
{this.globalNotificationState.color === NotificationColor.Red ? (
|
||||
<UnreadIndicator color={this.globalNotificationState.color} />
|
||||
) : null}
|
||||
</HeaderButton>,
|
||||
);
|
||||
rightPanelPhaseButtons.set(RightPanelPhases.RoomSummary,
|
||||
rightPanelPhaseButtons.set(
|
||||
RightPanelPhases.RoomSummary,
|
||||
<HeaderButton
|
||||
key="roomSummaryButton"
|
||||
name="roomSummaryButton"
|
||||
title={_t('Room info')}
|
||||
title={_t("Room info")}
|
||||
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
|
||||
onClick={this.onRoomSummaryClicked}
|
||||
/>,
|
||||
);
|
||||
|
||||
return <>
|
||||
{
|
||||
Array.from(rightPanelPhaseButtons.keys()).map((phase) =>
|
||||
(this.props.excludedRightPanelPhaseButtons?.includes(phase)
|
||||
return (
|
||||
<>
|
||||
{Array.from(rightPanelPhaseButtons.keys()).map((phase) =>
|
||||
this.props.excludedRightPanelPhaseButtons?.includes(phase)
|
||||
? null
|
||||
: rightPanelPhaseButtons.get(phase)))
|
||||
}
|
||||
</>;
|
||||
: rightPanelPhaseButtons.get(phase),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,15 +19,15 @@ import classNames from "classnames";
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { useIsEncrypted } from '../../../hooks/useIsEncrypted';
|
||||
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
|
||||
import BaseCard, { Group } from "./BaseCard";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import Modal from "../../../Modal";
|
||||
import ShareDialog from '../dialogs/ShareDialog';
|
||||
import ShareDialog from "../dialogs/ShareDialog";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
||||
|
@ -67,12 +67,14 @@ interface IButtonProps {
|
|||
}
|
||||
|
||||
const Button: React.FC<IButtonProps> = ({ children, className, onClick }) => {
|
||||
return <AccessibleButton
|
||||
className={classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{ children }
|
||||
</AccessibleButton>;
|
||||
return (
|
||||
<AccessibleButton
|
||||
className={classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const useWidgets = (room: Room) => {
|
||||
|
@ -114,20 +116,26 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
|||
|
||||
const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, Container.Top);
|
||||
const togglePin = isPinned
|
||||
? () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right); }
|
||||
: () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top); };
|
||||
? () => {
|
||||
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right);
|
||||
}
|
||||
: () => {
|
||||
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top);
|
||||
};
|
||||
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
||||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
const rect = handle.current.getBoundingClientRect();
|
||||
contextMenu = <WidgetContextMenu
|
||||
chevronFace={ChevronFace.None}
|
||||
right={UIStore.instance.windowWidth - rect.right}
|
||||
bottom={UIStore.instance.windowHeight - rect.top}
|
||||
onFinished={closeMenu}
|
||||
app={app}
|
||||
/>;
|
||||
contextMenu = (
|
||||
<WidgetContextMenu
|
||||
chevronFace={ChevronFace.None}
|
||||
right={UIStore.instance.windowWidth - rect.right}
|
||||
bottom={UIStore.instance.windowHeight - rect.top}
|
||||
onFinished={closeMenu}
|
||||
app={app}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top);
|
||||
|
@ -141,8 +149,12 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
|||
|
||||
const isMaximised = WidgetLayoutStore.instance.isInContainer(room, app, Container.Center);
|
||||
const toggleMaximised = isMaximised
|
||||
? () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right); }
|
||||
: () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Center); };
|
||||
? () => {
|
||||
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right);
|
||||
}
|
||||
: () => {
|
||||
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Center);
|
||||
};
|
||||
|
||||
const maximiseTitle = isMaximised ? _t("Close") : _t("Maximise");
|
||||
|
||||
|
@ -150,7 +162,7 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
|||
if (isPinned) {
|
||||
openTitle = _t("Unpin this widget to view it in this panel");
|
||||
} else if (isMaximised) {
|
||||
openTitle =_t("Close this widget to view it in this panel");
|
||||
openTitle = _t("Close this widget to view it in this panel");
|
||||
}
|
||||
|
||||
const classes = classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", {
|
||||
|
@ -158,47 +170,51 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
|||
mx_RoomSummaryCard_Button_maximised: isMaximised,
|
||||
});
|
||||
|
||||
return <div className={classes} ref={handle}>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomSummaryCard_icon_app"
|
||||
onClick={onOpenWidgetClick}
|
||||
// only show a tooltip if the widget is pinned
|
||||
title={openTitle}
|
||||
forceHide={!(isPinned || isMaximised)}
|
||||
disabled={isPinned || isMaximised}
|
||||
>
|
||||
<WidgetAvatar app={app} />
|
||||
<span>{ name }</span>
|
||||
{ subtitle }
|
||||
</AccessibleTooltipButton>
|
||||
return (
|
||||
<div className={classes} ref={handle}>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomSummaryCard_icon_app"
|
||||
onClick={onOpenWidgetClick}
|
||||
// only show a tooltip if the widget is pinned
|
||||
title={openTitle}
|
||||
forceHide={!(isPinned || isMaximised)}
|
||||
disabled={isPinned || isMaximised}
|
||||
>
|
||||
<WidgetAvatar app={app} />
|
||||
<span>{name}</span>
|
||||
{subtitle}
|
||||
</AccessibleTooltipButton>
|
||||
|
||||
{ canModifyWidget && <ContextMenuTooltipButton
|
||||
className="mx_RoomSummaryCard_app_options"
|
||||
isExpanded={menuDisplayed}
|
||||
onClick={openMenu}
|
||||
title={_t("Options")}
|
||||
/> }
|
||||
{canModifyWidget && (
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_RoomSummaryCard_app_options"
|
||||
isExpanded={menuDisplayed}
|
||||
onClick={openMenu}
|
||||
title={_t("Options")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomSummaryCard_app_pinToggle"
|
||||
onClick={togglePin}
|
||||
title={pinTitle}
|
||||
disabled={cannotPin}
|
||||
/>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomSummaryCard_app_maximiseToggle"
|
||||
onClick={toggleMaximised}
|
||||
title={maximiseTitle}
|
||||
/>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomSummaryCard_app_pinToggle"
|
||||
onClick={togglePin}
|
||||
title={pinTitle}
|
||||
disabled={cannotPin}
|
||||
/>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomSummaryCard_app_maximiseToggle"
|
||||
onClick={toggleMaximised}
|
||||
title={maximiseTitle}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
</div>;
|
||||
{contextMenu}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
|
||||
const apps = useWidgets(room);
|
||||
// Filter out virtual widgets
|
||||
const realApps = useMemo(() => apps.filter(app => app.eventId !== undefined), [apps]);
|
||||
const realApps = useMemo(() => apps.filter((app) => app.eventId !== undefined), [apps]);
|
||||
|
||||
const onManageIntegrations = () => {
|
||||
const managers = IntegrationManagers.sharedInstance();
|
||||
|
@ -214,18 +230,22 @@ const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
|
|||
if (realApps.length > 0 && WidgetLayoutStore.instance.canCopyLayoutToRoom(room)) {
|
||||
copyLayoutBtn = (
|
||||
<AccessibleButton kind="link" onClick={() => WidgetLayoutStore.instance.copyLayoutToRoom(room)}>
|
||||
{ _t("Set my room layout for everyone") }
|
||||
{_t("Set my room layout for everyone")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <Group className="mx_RoomSummaryCard_appsGroup" title={_t("Widgets")}>
|
||||
{ realApps.map(app => <AppRow key={app.id} app={app} room={room} />) }
|
||||
{ copyLayoutBtn }
|
||||
<AccessibleButton kind="link" onClick={onManageIntegrations}>
|
||||
{ realApps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots") }
|
||||
</AccessibleButton>
|
||||
</Group>;
|
||||
return (
|
||||
<Group className="mx_RoomSummaryCard_appsGroup" title={_t("Widgets")}>
|
||||
{realApps.map((app) => (
|
||||
<AppRow key={app.id} app={app} room={room} />
|
||||
))}
|
||||
{copyLayoutBtn}
|
||||
<AccessibleButton kind="link" onClick={onManageIntegrations}>
|
||||
{realApps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots")}
|
||||
</AccessibleButton>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
const onRoomMembersClick = (ev: ButtonEvent) => {
|
||||
|
@ -266,76 +286,71 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
|||
const e2eStatus = roomContext.e2eStatus;
|
||||
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
|
||||
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
|
||||
const isVideoRoom = videoRoomsEnabled && (
|
||||
room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom())
|
||||
);
|
||||
const isVideoRoom =
|
||||
videoRoomsEnabled && (room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom()));
|
||||
|
||||
const alias = room.getCanonicalAlias() || room.getAltAliases()[0] || "";
|
||||
const header = <React.Fragment>
|
||||
<div className="mx_RoomSummaryCard_avatar" role="presentation">
|
||||
<RoomAvatar room={room} height={54} width={54} viewAvatarOnClick />
|
||||
<TextWithTooltip
|
||||
tooltip={isRoomEncrypted ? _t("Encrypted") : _t("Not encrypted")}
|
||||
class={classNames("mx_RoomSummaryCard_e2ee", {
|
||||
mx_RoomSummaryCard_e2ee_normal: isRoomEncrypted,
|
||||
mx_RoomSummaryCard_e2ee_warning: isRoomEncrypted && e2eStatus === E2EStatus.Warning,
|
||||
mx_RoomSummaryCard_e2ee_verified: isRoomEncrypted && e2eStatus === E2EStatus.Verified,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
const header = (
|
||||
<React.Fragment>
|
||||
<div className="mx_RoomSummaryCard_avatar" role="presentation">
|
||||
<RoomAvatar room={room} height={54} width={54} viewAvatarOnClick />
|
||||
<TextWithTooltip
|
||||
tooltip={isRoomEncrypted ? _t("Encrypted") : _t("Not encrypted")}
|
||||
class={classNames("mx_RoomSummaryCard_e2ee", {
|
||||
mx_RoomSummaryCard_e2ee_normal: isRoomEncrypted,
|
||||
mx_RoomSummaryCard_e2ee_warning: isRoomEncrypted && e2eStatus === E2EStatus.Warning,
|
||||
mx_RoomSummaryCard_e2ee_verified: isRoomEncrypted && e2eStatus === E2EStatus.Verified,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RoomName room={room}>
|
||||
{ name => (
|
||||
<h2 title={name}>
|
||||
{ name }
|
||||
</h2>
|
||||
) }
|
||||
</RoomName>
|
||||
<div className="mx_RoomSummaryCard_alias" title={alias}>
|
||||
{ alias }
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
<RoomName room={room}>{(name) => <h2 title={name}>{name}</h2>}</RoomName>
|
||||
<div className="mx_RoomSummaryCard_alias" title={alias}>
|
||||
{alias}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
const memberCount = useRoomMemberCount(room);
|
||||
const pinningEnabled = useFeatureEnabled("feature_pinning");
|
||||
const pinCount = usePinnedEvents(pinningEnabled && room)?.length;
|
||||
|
||||
return <BaseCard header={header} className="mx_RoomSummaryCard" onClose={onClose}>
|
||||
<Group title={_t("About")} className="mx_RoomSummaryCard_aboutGroup">
|
||||
<Button className="mx_RoomSummaryCard_icon_people" onClick={onRoomMembersClick}>
|
||||
{ _t("People") }
|
||||
<span className="mx_BaseCard_Button_sublabel">
|
||||
{ memberCount }
|
||||
</span>
|
||||
</Button>
|
||||
{ !isVideoRoom && <Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
|
||||
{ _t("Files") }
|
||||
</Button> }
|
||||
{ pinningEnabled && !isVideoRoom &&
|
||||
<Button className="mx_RoomSummaryCard_icon_pins" onClick={onRoomPinsClick}>
|
||||
{ _t("Pinned") }
|
||||
{ pinCount > 0 && <span className="mx_BaseCard_Button_sublabel">
|
||||
{ pinCount }
|
||||
</span> }
|
||||
</Button> }
|
||||
{ !isVideoRoom && <Button className="mx_RoomSummaryCard_icon_export" onClick={onRoomExportClick}>
|
||||
{ _t("Export chat") }
|
||||
</Button> }
|
||||
<Button className="mx_RoomSummaryCard_icon_share" onClick={onShareRoomClick}>
|
||||
{ _t("Share room") }
|
||||
</Button>
|
||||
<Button className="mx_RoomSummaryCard_icon_settings" onClick={onRoomSettingsClick}>
|
||||
{ _t("Room settings") }
|
||||
</Button>
|
||||
</Group>
|
||||
return (
|
||||
<BaseCard header={header} className="mx_RoomSummaryCard" onClose={onClose}>
|
||||
<Group title={_t("About")} className="mx_RoomSummaryCard_aboutGroup">
|
||||
<Button className="mx_RoomSummaryCard_icon_people" onClick={onRoomMembersClick}>
|
||||
{_t("People")}
|
||||
<span className="mx_BaseCard_Button_sublabel">{memberCount}</span>
|
||||
</Button>
|
||||
{!isVideoRoom && (
|
||||
<Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
|
||||
{_t("Files")}
|
||||
</Button>
|
||||
)}
|
||||
{pinningEnabled && !isVideoRoom && (
|
||||
<Button className="mx_RoomSummaryCard_icon_pins" onClick={onRoomPinsClick}>
|
||||
{_t("Pinned")}
|
||||
{pinCount > 0 && <span className="mx_BaseCard_Button_sublabel">{pinCount}</span>}
|
||||
</Button>
|
||||
)}
|
||||
{!isVideoRoom && (
|
||||
<Button className="mx_RoomSummaryCard_icon_export" onClick={onRoomExportClick}>
|
||||
{_t("Export chat")}
|
||||
</Button>
|
||||
)}
|
||||
<Button className="mx_RoomSummaryCard_icon_share" onClick={onShareRoomClick}>
|
||||
{_t("Share room")}
|
||||
</Button>
|
||||
<Button className="mx_RoomSummaryCard_icon_settings" onClick={onRoomSettingsClick}>
|
||||
{_t("Room settings")}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{
|
||||
SettingsStore.getValue(UIFeature.Widgets)
|
||||
&& !isVideoRoom
|
||||
&& shouldShowComponent(UIComponent.AddIntegrations)
|
||||
&& <AppsSection room={room} />
|
||||
}
|
||||
</BaseCard>;
|
||||
{SettingsStore.getValue(UIFeature.Widgets) &&
|
||||
!isVideoRoom &&
|
||||
shouldShowComponent(UIComponent.AddIntegrations) && <AppsSection room={room} />}
|
||||
</BaseCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoomSummaryCard;
|
||||
|
|
|
@ -14,34 +14,34 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set';
|
||||
import { NotificationCountType, Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { Thread } from 'matrix-js-sdk/src/models/thread';
|
||||
import React from "react";
|
||||
import { IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set";
|
||||
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||
import { Thread } from "matrix-js-sdk/src/models/thread";
|
||||
|
||||
import BaseCard from "./BaseCard";
|
||||
import ResizeNotifier from '../../../utils/ResizeNotifier';
|
||||
import MessageComposer from '../rooms/MessageComposer';
|
||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||
import { Layout } from '../../../settings/enums/Layout';
|
||||
import TimelinePanel from '../../structures/TimelinePanel';
|
||||
import { E2EStatus } from '../../../utils/ShieldUtils';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
import UploadBar from '../../structures/UploadBar';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import JumpToBottomButton from '../rooms/JumpToBottomButton';
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import MessageComposer from "../rooms/MessageComposer";
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import { Layout } from "../../../settings/enums/Layout";
|
||||
import TimelinePanel from "../../structures/TimelinePanel";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
import EditorStateTransfer from "../../../utils/EditorStateTransfer";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import ContentMessages from "../../../ContentMessages";
|
||||
import UploadBar from "../../structures/UploadBar";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import JumpToBottomButton from "../rooms/JumpToBottomButton";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import Measured from '../elements/Measured';
|
||||
import Heading from '../typography/Heading';
|
||||
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
|
||||
import { SdkContextClass } from '../../../contexts/SDKContext';
|
||||
import Measured from "../elements/Measured";
|
||||
import Heading from "../typography/Heading";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -93,10 +93,10 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||
public componentDidMount(): void {
|
||||
SdkContextClass.instance.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.readReceiptsSettingWatcher = SettingsStore.watchSetting("showReadReceipts", null, (...[,,, value]) =>
|
||||
this.readReceiptsSettingWatcher = SettingsStore.watchSetting("showReadReceipts", null, (...[, , , value]) =>
|
||||
this.setState({ showReadReceipts: value as boolean }),
|
||||
);
|
||||
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[,,, value]) =>
|
||||
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[, , , value]) =>
|
||||
this.setState({ layout: value as Layout }),
|
||||
);
|
||||
}
|
||||
|
@ -127,13 +127,16 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||
private onAction = (payload: ActionPayload): void => {
|
||||
switch (payload.action) {
|
||||
case Action.EditEvent:
|
||||
this.setState({
|
||||
editState: payload.event ? new EditorStateTransfer(payload.event) : null,
|
||||
}, () => {
|
||||
if (payload.event) {
|
||||
this.timelinePanel.current?.scrollToEventIfNeeded(payload.event.getId());
|
||||
}
|
||||
});
|
||||
this.setState(
|
||||
{
|
||||
editState: payload.event ? new EditorStateTransfer(payload.event) : null,
|
||||
},
|
||||
() => {
|
||||
if (payload.event) {
|
||||
this.timelinePanel.current?.scrollToEventIfNeeded(payload.event.getId());
|
||||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -186,22 +189,26 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private renderTimelineCardHeader = (): JSX.Element => {
|
||||
return <div className="mx_BaseCard_header_title">
|
||||
<Heading size="h4" className="mx_BaseCard_header_title_heading">{ _t("Chat") }</Heading>
|
||||
</div>;
|
||||
return (
|
||||
<div className="mx_BaseCard_header_title">
|
||||
<Heading size="h4" className="mx_BaseCard_header_title_heading">
|
||||
{_t("Chat")}
|
||||
</Heading>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const highlightedEventId = this.state.isInitialEventHighlighted
|
||||
? this.state.initialEventId
|
||||
: null;
|
||||
const highlightedEventId = this.state.isInitialEventHighlighted ? this.state.initialEventId : null;
|
||||
|
||||
let jumpToBottom;
|
||||
if (!this.state.atEndOfLiveTimeline) {
|
||||
jumpToBottom = (<JumpToBottomButton
|
||||
highlight={this.props.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0}
|
||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||
/>);
|
||||
jumpToBottom = (
|
||||
<JumpToBottomButton
|
||||
highlight={this.props.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0}
|
||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const isUploading = ContentMessages.sharedInstance().getCurrentUploads(this.props.composerRelation).length > 0;
|
||||
|
@ -210,12 +217,14 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||
const showComposer = myMembership === "join";
|
||||
|
||||
return (
|
||||
<RoomContext.Provider value={{
|
||||
...this.context,
|
||||
timelineRenderingType: this.props.timelineRenderingType ?? this.context.timelineRenderingType,
|
||||
liveTimeline: this.props.timelineSet.getLiveTimeline(),
|
||||
narrow: this.state.narrow,
|
||||
}}>
|
||||
<RoomContext.Provider
|
||||
value={{
|
||||
...this.context,
|
||||
timelineRenderingType: this.props.timelineRenderingType ?? this.context.timelineRenderingType,
|
||||
liveTimeline: this.props.timelineSet.getLiveTimeline(),
|
||||
narrow: this.state.narrow,
|
||||
}}
|
||||
>
|
||||
<BaseCard
|
||||
className={this.props.classNames}
|
||||
onClose={this.props.onClose}
|
||||
|
@ -223,12 +232,9 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||
header={this.renderTimelineCardHeader()}
|
||||
ref={this.card}
|
||||
>
|
||||
<Measured
|
||||
sensor={this.card.current}
|
||||
onMeasurement={this.onMeasurement}
|
||||
/>
|
||||
<Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />
|
||||
<div className="mx_TimelineCard_timeline">
|
||||
{ jumpToBottom }
|
||||
{jumpToBottom}
|
||||
<TimelinePanel
|
||||
ref={this.timelinePanel}
|
||||
showReadReceipts={this.state.showReadReceipts}
|
||||
|
@ -253,11 +259,9 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{ isUploading && (
|
||||
<UploadBar room={this.props.room} relation={this.props.composerRelation} />
|
||||
) }
|
||||
{isUploading && <UploadBar room={this.props.room} relation={this.props.composerRelation} />}
|
||||
|
||||
{ showComposer && (
|
||||
{showComposer && (
|
||||
<MessageComposer
|
||||
room={this.props.room}
|
||||
relation={this.props.composerRelation}
|
||||
|
@ -267,7 +271,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
/>
|
||||
) }
|
||||
)}
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import { verificationMethods } from 'matrix-js-sdk/src/crypto';
|
||||
import { verificationMethods } from "matrix-js-sdk/src/crypto";
|
||||
import { QrCodeEvent, ReciprocateQRCode, SCAN_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||
import {
|
||||
Phase,
|
||||
|
@ -68,52 +68,64 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
const showQR: boolean = request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD);
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
const noCommonMethodError: JSX.Element = !showSAS && !showQR ?
|
||||
<p>{ _t(
|
||||
"The device you are trying to verify doesn't support scanning a " +
|
||||
"QR code or emoji verification, which is what %(brand)s supports. Try " +
|
||||
"with a different client.",
|
||||
{ brand },
|
||||
) }</p> :
|
||||
null;
|
||||
const noCommonMethodError: JSX.Element =
|
||||
!showSAS && !showQR ? (
|
||||
<p>
|
||||
{_t(
|
||||
"The device you are trying to verify doesn't support scanning a " +
|
||||
"QR code or emoji verification, which is what %(brand)s supports. Try " +
|
||||
"with a different client.",
|
||||
{ brand },
|
||||
)}
|
||||
</p>
|
||||
) : null;
|
||||
|
||||
if (this.props.layout === 'dialog') {
|
||||
if (this.props.layout === "dialog") {
|
||||
// HACK: This is a terrible idea.
|
||||
let qrBlockDialog: JSX.Element;
|
||||
let sasBlockDialog: JSX.Element;
|
||||
if (showQR) {
|
||||
qrBlockDialog =
|
||||
<div className='mx_VerificationPanel_QRPhase_startOption'>
|
||||
<p>{ _t("Scan this unique code") }</p>
|
||||
qrBlockDialog = (
|
||||
<div className="mx_VerificationPanel_QRPhase_startOption">
|
||||
<p>{_t("Scan this unique code")}</p>
|
||||
<VerificationQRCode qrCodeData={request.qrCodeData} />
|
||||
</div>;
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (showSAS) {
|
||||
sasBlockDialog = <div className='mx_VerificationPanel_QRPhase_startOption'>
|
||||
<p>{ _t("Compare unique emoji") }</p>
|
||||
<span className='mx_VerificationPanel_QRPhase_helpText'>
|
||||
{ _t("Compare a unique set of emoji if you don't have a camera on either device") }
|
||||
</span>
|
||||
<AccessibleButton disabled={this.state.emojiButtonClicked} onClick={this.startSAS} kind='primary'>
|
||||
{ _t("Start") }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
sasBlockDialog = (
|
||||
<div className="mx_VerificationPanel_QRPhase_startOption">
|
||||
<p>{_t("Compare unique emoji")}</p>
|
||||
<span className="mx_VerificationPanel_QRPhase_helpText">
|
||||
{_t("Compare a unique set of emoji if you don't have a camera on either device")}
|
||||
</span>
|
||||
<AccessibleButton
|
||||
disabled={this.state.emojiButtonClicked}
|
||||
onClick={this.startSAS}
|
||||
kind="primary"
|
||||
>
|
||||
{_t("Start")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const or = qrBlockDialog && sasBlockDialog ?
|
||||
<div className='mx_VerificationPanel_QRPhase_betweenText'>
|
||||
{ _t("%(qrCode)s or %(emojiCompare)s", {
|
||||
emojiCompare: "",
|
||||
qrCode: "",
|
||||
}) }
|
||||
</div> : null;
|
||||
const or =
|
||||
qrBlockDialog && sasBlockDialog ? (
|
||||
<div className="mx_VerificationPanel_QRPhase_betweenText">
|
||||
{_t("%(qrCode)s or %(emojiCompare)s", {
|
||||
emojiCompare: "",
|
||||
qrCode: "",
|
||||
})}
|
||||
</div>
|
||||
) : null;
|
||||
return (
|
||||
<div>
|
||||
{ _t("Verify this device by completing one of the following:") }
|
||||
<div className='mx_VerificationPanel_QRPhase_startOptions'>
|
||||
{ qrBlockDialog }
|
||||
{ or }
|
||||
{ sasBlockDialog }
|
||||
{ noCommonMethodError }
|
||||
{_t("Verify this device by completing one of the following:")}
|
||||
<div className="mx_VerificationPanel_QRPhase_startOptions">
|
||||
{qrBlockDialog}
|
||||
{or}
|
||||
{sasBlockDialog}
|
||||
{noCommonMethodError}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -121,50 +133,58 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
|
||||
let qrBlock: JSX.Element;
|
||||
if (showQR) {
|
||||
qrBlock = <div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Verify by scanning") }</h3>
|
||||
<p>{ _t("Ask %(displayName)s to scan your code:", {
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
}) }</p>
|
||||
qrBlock = (
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{_t("Verify by scanning")}</h3>
|
||||
<p>
|
||||
{_t("Ask %(displayName)s to scan your code:", {
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
})}
|
||||
</p>
|
||||
|
||||
<div className="mx_VerificationPanel_qrCode">
|
||||
<VerificationQRCode qrCodeData={request.qrCodeData} />
|
||||
<div className="mx_VerificationPanel_qrCode">
|
||||
<VerificationQRCode qrCodeData={request.qrCodeData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
);
|
||||
}
|
||||
|
||||
let sasBlock: JSX.Element;
|
||||
if (showSAS) {
|
||||
const disabled = this.state.emojiButtonClicked;
|
||||
const sasLabel = showQR ?
|
||||
_t("If you can't scan the code above, verify by comparing unique emoji.") :
|
||||
_t("Verify by comparing unique emoji.");
|
||||
const sasLabel = showQR
|
||||
? _t("If you can't scan the code above, verify by comparing unique emoji.")
|
||||
: _t("Verify by comparing unique emoji.");
|
||||
|
||||
// Note: mx_VerificationPanel_verifyByEmojiButton is for the end-to-end tests
|
||||
sasBlock = <div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Verify by emoji") }</h3>
|
||||
<p>{ sasLabel }</p>
|
||||
<AccessibleButton
|
||||
disabled={disabled}
|
||||
kind="primary"
|
||||
className="mx_UserInfo_wideButton mx_VerificationPanel_verifyByEmojiButton"
|
||||
onClick={this.startSAS}
|
||||
>
|
||||
{ _t("Verify by emoji") }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
sasBlock = (
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{_t("Verify by emoji")}</h3>
|
||||
<p>{sasLabel}</p>
|
||||
<AccessibleButton
|
||||
disabled={disabled}
|
||||
kind="primary"
|
||||
className="mx_UserInfo_wideButton mx_VerificationPanel_verifyByEmojiButton"
|
||||
onClick={this.startSAS}
|
||||
>
|
||||
{_t("Verify by emoji")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const noCommonMethodBlock = noCommonMethodError ?
|
||||
<div className="mx_UserInfo_container">{ noCommonMethodError }</div> :
|
||||
null;
|
||||
const noCommonMethodBlock = noCommonMethodError ? (
|
||||
<div className="mx_UserInfo_container">{noCommonMethodError}</div>
|
||||
) : null;
|
||||
|
||||
// TODO: add way to open camera to scan a QR code
|
||||
return <React.Fragment>
|
||||
{ qrBlock }
|
||||
{ sasBlock }
|
||||
{ noCommonMethodBlock }
|
||||
</React.Fragment>;
|
||||
return (
|
||||
<React.Fragment>
|
||||
{qrBlock}
|
||||
{sasBlock}
|
||||
{noCommonMethodBlock}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private onReciprocateYesClick = () => {
|
||||
|
@ -184,41 +204,49 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
|
||||
private renderQRReciprocatePhase() {
|
||||
const { member, request } = this.props;
|
||||
const description = request.isSelfVerification ?
|
||||
_t("Almost there! Is your other device showing the same shield?") :
|
||||
_t("Almost there! Is %(displayName)s showing the same shield?", {
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
});
|
||||
const description = request.isSelfVerification
|
||||
? _t("Almost there! Is your other device showing the same shield?")
|
||||
: _t("Almost there! Is %(displayName)s showing the same shield?", {
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
});
|
||||
let body: JSX.Element;
|
||||
if (this.state.reciprocateQREvent) {
|
||||
// Element Web doesn't support scanning yet, so assume here we're the client being scanned.
|
||||
body = <React.Fragment>
|
||||
<p>{ description }</p>
|
||||
<E2EIcon isUser={true} status={E2EState.Verified} size={128} hideTooltip={true} />
|
||||
<div className="mx_VerificationPanel_reciprocateButtons">
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
disabled={this.state.reciprocateButtonClicked}
|
||||
onClick={this.onReciprocateNoClick}
|
||||
>
|
||||
{ _t("No") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
disabled={this.state.reciprocateButtonClicked}
|
||||
onClick={this.onReciprocateYesClick}
|
||||
>
|
||||
{ _t("Yes") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
body = (
|
||||
<React.Fragment>
|
||||
<p>{description}</p>
|
||||
<E2EIcon isUser={true} status={E2EState.Verified} size={128} hideTooltip={true} />
|
||||
<div className="mx_VerificationPanel_reciprocateButtons">
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
disabled={this.state.reciprocateButtonClicked}
|
||||
onClick={this.onReciprocateNoClick}
|
||||
>
|
||||
{_t("No")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
disabled={this.state.reciprocateButtonClicked}
|
||||
onClick={this.onReciprocateYesClick}
|
||||
>
|
||||
{_t("Yes")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
body = <p><Spinner /></p>;
|
||||
body = (
|
||||
<p>
|
||||
<Spinner />
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return <div className="mx_UserInfo_container mx_VerificationPanel_reciprocate_section">
|
||||
<h3>{ _t("Verify by scanning") }</h3>
|
||||
{ body }
|
||||
</div>;
|
||||
return (
|
||||
<div className="mx_UserInfo_container mx_VerificationPanel_reciprocate_section">
|
||||
<h3>{_t("Verify by scanning")}</h3>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderVerifiedPhase() {
|
||||
|
@ -243,7 +271,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
description = _t("You've successfully verified your device!");
|
||||
} else {
|
||||
description = _t("You've successfully verified %(deviceName)s (%(deviceId)s)!", {
|
||||
deviceName: device ? device.getDisplayName() : '',
|
||||
deviceName: device ? device.getDisplayName() : "",
|
||||
deviceId: this.props.request.channel.deviceId,
|
||||
});
|
||||
}
|
||||
|
@ -255,11 +283,11 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
|
||||
return (
|
||||
<div className="mx_UserInfo_container mx_VerificationPanel_verified_section">
|
||||
<p>{ description }</p>
|
||||
<p>{description}</p>
|
||||
<E2EIcon isUser={true} status={E2EState.Verified} size={128} hideTooltip={true} />
|
||||
{ text ? <p>{ text }</p> : null }
|
||||
{text ? <p>{text}</p> : null}
|
||||
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
|
||||
{ _t("Got it") }
|
||||
{_t("Got it")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
@ -293,11 +321,11 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
|
||||
return (
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Verification cancelled") }</h3>
|
||||
<p>{ text }</p>
|
||||
<h3>{_t("Verification cancelled")}</h3>
|
||||
<p>{text}</p>
|
||||
|
||||
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
|
||||
{ _t("Got it") }
|
||||
{_t("Got it")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
@ -316,7 +344,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
case verificationMethods.RECIPROCATE_QR_CODE:
|
||||
return this.renderQRReciprocatePhase();
|
||||
case verificationMethods.SAS: {
|
||||
const emojis = this.state.sasEvent ?
|
||||
const emojis = this.state.sasEvent ? (
|
||||
<VerificationShowSas
|
||||
displayName={displayName}
|
||||
device={this.getDevice()}
|
||||
|
@ -325,10 +353,11 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
onDone={this.onSasMatchesClick}
|
||||
inDialog={this.props.inDialog}
|
||||
isSelf={request.isSelfVerification}
|
||||
/> : <Spinner />;
|
||||
return <div className="mx_UserInfo_container">
|
||||
{ emojis }
|
||||
</div>;
|
||||
/>
|
||||
) : (
|
||||
<Spinner />
|
||||
);
|
||||
return <div className="mx_UserInfo_container">{emojis}</div>;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
|
|
|
@ -28,7 +28,7 @@ import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
|||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import Heading from '../typography/Heading';
|
||||
import Heading from "../typography/Heading";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -40,7 +40,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
|||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const apps = useWidgets(room);
|
||||
const app = apps.find(a => a.id === widgetId);
|
||||
const app = apps.find((a) => a.id === widgetId);
|
||||
const isRight = app && WidgetLayoutStore.instance.isInContainer(room, app, Container.Right);
|
||||
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
|
@ -69,35 +69,36 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
|||
);
|
||||
}
|
||||
|
||||
const header = <div className="mx_BaseCard_header_title">
|
||||
<Heading size="h4" className="mx_BaseCard_header_title_heading">{ WidgetUtils.getWidgetName(app) }</Heading>
|
||||
<ContextMenuButton
|
||||
className="mx_BaseCard_header_title_button--option"
|
||||
inputRef={handle}
|
||||
onClick={openMenu}
|
||||
isExpanded={menuDisplayed}
|
||||
label={_t("Options")}
|
||||
/>
|
||||
{ contextMenu }
|
||||
</div>;
|
||||
const header = (
|
||||
<div className="mx_BaseCard_header_title">
|
||||
<Heading size="h4" className="mx_BaseCard_header_title_heading">
|
||||
{WidgetUtils.getWidgetName(app)}
|
||||
</Heading>
|
||||
<ContextMenuButton
|
||||
className="mx_BaseCard_header_title_button--option"
|
||||
inputRef={handle}
|
||||
onClick={openMenu}
|
||||
isExpanded={menuDisplayed}
|
||||
label={_t("Options")}
|
||||
/>
|
||||
{contextMenu}
|
||||
</div>
|
||||
);
|
||||
|
||||
return <BaseCard
|
||||
header={header}
|
||||
className="mx_WidgetCard"
|
||||
onClose={onClose}
|
||||
withoutScrollContainer
|
||||
>
|
||||
<AppTile
|
||||
app={app}
|
||||
fullWidth
|
||||
showMenubar={false}
|
||||
room={room}
|
||||
userId={cli.getUserId()}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
/>
|
||||
</BaseCard>;
|
||||
return (
|
||||
<BaseCard header={header} className="mx_WidgetCard" onClose={onClose} withoutScrollContainer>
|
||||
<AppTile
|
||||
app={app}
|
||||
fullWidth
|
||||
showMenubar={false}
|
||||
room={room}
|
||||
userId={cli.getUserId()}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
/>
|
||||
</BaseCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default WidgetCard;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue