Merge branch 'develop' into sort-imports

Signed-off-by: Aaron Raimist <aaron@raim.ist>
This commit is contained in:
Aaron Raimist 2021-12-09 08:34:20 +00:00
commit 7b94e13a84
642 changed files with 30052 additions and 8035 deletions

View file

@ -31,6 +31,7 @@ interface IProps {
className?: string;
withoutScrollContainer?: boolean;
previousPhase?: RightPanelPhases;
previousPhaseLabel?: string;
closeLabel?: string;
onClose?(): void;
refireParams?;
@ -56,6 +57,7 @@ const BaseCard: React.FC<IProps> = ({
footer,
withoutScrollContainer,
previousPhase,
previousPhaseLabel,
children,
refireParams,
}) => {
@ -68,7 +70,8 @@ const BaseCard: React.FC<IProps> = ({
refireParams: refireParams,
});
};
backButton = <AccessibleButton className="mx_BaseCard_back" onClick={onBackClick} title={_t("Back")} />;
const label = previousPhaseLabel ?? _t("Back");
backButton = <AccessibleButton className="mx_BaseCard_back" onClick={onBackClick} title={label} />;
}
let closeButton;

View file

@ -67,7 +67,11 @@ const EncryptionInfo: React.FC<IProps> = ({
content = <PendingActionSpinner text={text} />;
} else {
content = (
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={onStartVerification}>
<AccessibleButton
kind="primary"
className="mx_UserInfo_wideButton mx_UserInfo_startVerification"
onClick={onStartVerification}
>
{ _t("Start Verification") }
</AccessibleButton>
);

View file

@ -93,9 +93,9 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
{ _t("One of the following may be compromised:") }
<ul>
<li>{ _t("Your homeserver") }</li>
<li>{ _t("The homeserver the user youre verifying is connected to") }</li>
<li>{ _t("Yours, or the other users internet connection") }</li>
<li>{ _t("Yours, or the other users session") }</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,

View file

@ -121,24 +121,26 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => {
if (!pinnedEvents) {
content = <Spinner />;
} else if (pinnedEvents.length > 0) {
let onUnpinClicked;
if (canUnpin) {
onUnpinClicked = async (event: MatrixEvent) => {
const pinnedEvents = room.currentState.getStateEvents(EventType.RoomPinnedEvents, "");
if (pinnedEvents?.getContent()?.pinned) {
const pinned = pinnedEvents.getContent().pinned;
const index = pinned.indexOf(event.getId());
if (index !== -1) {
pinned.splice(index, 1);
await cli.sendStateEvent(room.roomId, EventType.RoomPinnedEvents, { pinned }, "");
}
const onUnpinClicked = async (event: MatrixEvent) => {
const pinnedEvents = room.currentState.getStateEvents(EventType.RoomPinnedEvents, "");
if (pinnedEvents?.getContent()?.pinned) {
const pinned = pinnedEvents.getContent().pinned;
const index = pinned.indexOf(event.getId());
if (index !== -1) {
pinned.splice(index, 1);
await cli.sendStateEvent(room.roomId, EventType.RoomPinnedEvents, { pinned }, "");
}
};
}
}
};
// show them in reverse, with latest pinned at the top
content = pinnedEvents.filter(Boolean).reverse().map(ev => (
<PinnedEventTile key={ev.getId()} room={room} event={ev} onUnpinClicked={() => onUnpinClicked(ev)} />
<PinnedEventTile
key={ev.getId()}
room={room}
event={ev}
onUnpinClicked={canUnpin ? () => onUnpinClicked(ev) : undefined}
/>
));
} else {
content = <div className="mx_PinnedMessagesCard_empty">

View file

@ -33,6 +33,9 @@ import { useSettingValue } from "../../../hooks/useSettings";
import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard';
import { dispatchShowThreadsPanelEvent } from "../../../dispatcher/dispatch-actions/threads";
import SettingsStore from "../../../settings/SettingsStore";
import dis from "../../../dispatcher/dispatcher";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
const ROOM_INFO_PHASES = [
RightPanelPhases.RoomSummary,
@ -44,7 +47,24 @@ const ROOM_INFO_PHASES = [
RightPanelPhases.Room3pidMemberInfo,
];
const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => {
interface IUnreadIndicatorProps {
className: string;
}
const UnreadIndicator = ({ className }: IUnreadIndicatorProps) => {
return <React.Fragment>
<div className="mx_RightPanel_headerButton_unreadIndicator_bg" />
<div className={className} />
</React.Fragment>;
};
interface IHeaderButtonProps {
room: Room;
isHighlighted: boolean;
onClick: () => void;
}
const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }: IHeaderButtonProps) => {
const pinningEnabled = useSettingValue("feature_pinning");
const pinnedEvents = usePinnedEvents(pinningEnabled && room);
const readPinnedEvents = useReadPinnedEvents(pinningEnabled && room);
@ -52,7 +72,7 @@ const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => {
let unreadIndicator;
if (pinnedEvents.some(id => !readPinnedEvents.has(id))) {
unreadIndicator = <div className="mx_RightPanel_pinnedMessagesButton_unreadIndicator" />;
unreadIndicator = <UnreadIndicator className="mx_RightPanel_headerButton_unreadIndicator" />;
}
return <HeaderButton
@ -66,12 +86,44 @@ const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => {
</HeaderButton>;
};
const TimelineCardHeaderButton = ({ room, isHighlighted, onClick }: IHeaderButtonProps) => {
if (!SettingsStore.getValue("feature_maximised_widgets")) return null;
let unreadIndicator;
switch (RoomNotificationStateStore.instance.getRoomState(room).color) {
case NotificationColor.Grey:
unreadIndicator =
<UnreadIndicator className="mx_RightPanel_headerButton_unreadIndicator mx_Indicator_gray" />;
break;
case NotificationColor.Red:
unreadIndicator =
<UnreadIndicator className="mx_RightPanel_headerButton_unreadIndicator" />;
break;
default:
break;
}
return <HeaderButton
name="timelineCardButton"
title={_t("Chat")}
isHighlighted={isHighlighted}
onClick={onClick}
analytics={["Right Panel", "Timeline Panel Button", "click"]}
>
{ unreadIndicator }
</HeaderButton>;
};
interface IProps {
room?: Room;
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
}
@replaceableComponent("views.right_panel.RoomHeaderButtons")
export default class RoomHeaderButtons extends HeaderButtons<IProps> {
private static readonly THREAD_PHASES = [
RightPanelPhases.ThreadPanel,
RightPanelPhases.ThreadView,
];
constructor(props: IProps) {
super(props, HeaderKind.Room);
}
@ -116,35 +168,70 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
// This toggles for us, if needed
this.setPhase(RightPanelPhases.PinnedMessages);
};
private onTimelineCardClicked = () => {
this.setPhase(RightPanelPhases.Timeline);
};
private onThreadsPanelClicked = () => {
if (RoomHeaderButtons.THREAD_PHASES.includes(this.state.phase)) {
dis.dispatch({
action: Action.ToggleRightPanel,
type: "room",
});
} else {
dispatchShowThreadsPanelEvent();
}
};
public renderButtons() {
return <>
const rightPanelPhaseButtons: Map<RightPanelPhases, any> = new Map();
rightPanelPhaseButtons.set(RightPanelPhases.PinnedMessages,
<PinnedMessagesHeaderButton
room={this.props.room}
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
onClick={this.onPinnedMessagesClicked}
/>
{ SettingsStore.getValue("feature_thread") && <HeaderButton
name="threadsButton"
title={_t("Threads")}
onClick={dispatchShowThreadsPanelEvent}
isHighlighted={this.isPhase(RightPanelPhases.ThreadPanel)}
analytics={['Right Panel', 'Threads List Button', 'click']}
/> }
onClick={this.onPinnedMessagesClicked} />,
);
rightPanelPhaseButtons.set(RightPanelPhases.Timeline,
<TimelineCardHeaderButton
room={this.props.room}
isHighlighted={this.isPhase(RightPanelPhases.Timeline)}
onClick={this.onTimelineCardClicked} />,
);
rightPanelPhaseButtons.set(RightPanelPhases.ThreadPanel,
SettingsStore.getValue("feature_thread")
? <HeaderButton
name="threadsButton"
title={_t("Threads")}
onClick={this.onThreadsPanelClicked}
isHighlighted={this.isPhase(RoomHeaderButtons.THREAD_PHASES)}
analytics={['Right Panel', 'Threads List Button', 'click']} />
: null,
);
rightPanelPhaseButtons.set(RightPanelPhases.NotificationPanel,
<HeaderButton
name="notifsButton"
title={_t('Notifications')}
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
onClick={this.onNotificationsClicked}
analytics={['Right Panel', 'Notification List Button', 'click']}
/>
analytics={['Right Panel', 'Notification List Button', 'click']} />,
);
rightPanelPhaseButtons.set(RightPanelPhases.RoomSummary,
<HeaderButton
name="roomSummaryButton"
title={_t('Room Info')}
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
onClick={this.onRoomSummaryClicked}
analytics={['Right Panel', 'Room Summary Button', 'click']}
/>
analytics={['Right Panel', 'Room Summary Button', 'click']} />,
);
return <>
{
Array.from(rightPanelPhaseButtons.keys()).map((phase) =>
( this.props.excludedRightPanelPhaseButtons.includes(phase)
? null
: rightPanelPhaseButtons.get(phase)))
}
</>;
}
}

View file

@ -48,7 +48,6 @@ import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widget
import RoomName from "../elements/RoomName";
import UIStore from "../../../stores/UIStore";
import ExportDialog from "../dialogs/ExportDialog";
import { dispatchShowThreadsPanelEvent } from "../../../dispatcher/dispatch-actions/threads";
interface IProps {
room: Room;
@ -139,14 +138,28 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
mx_RoomSummaryCard_Button_pinned: isPinned,
});
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); };
const maximiseTitle = isMaximised ? _t("Close") : _t("Maximise widget");
let openTitle = "";
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");
}
return <div className={classes} ref={handle}>
<AccessibleTooltipButton
className="mx_RoomSummaryCard_icon_app"
onClick={onOpenWidgetClick}
// only show a tooltip if the widget is pinned
title={isPinned ? _t("Unpin a widget to view it in this panel") : ""}
forceHide={!isPinned}
disabled={isPinned}
title={openTitle}
forceHide={!(isPinned || isMaximised)}
disabled={isPinned || isMaximised}
yOffset={-48}
>
<WidgetAvatar app={app} />
@ -155,7 +168,10 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
</AccessibleTooltipButton>
<ContextMenuTooltipButton
className="mx_RoomSummaryCard_app_options"
className={classNames({
"mx_RoomSummaryCard_app_options": true,
"mx_RoomSummaryCard_maximised_widget": SettingsStore.getValue("feature_maximised_widgets"),
})}
isExpanded={menuDisplayed}
onClick={openMenu}
title={_t("Options")}
@ -169,6 +185,13 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
disabled={cannotPin}
yOffset={-24}
/>
{ SettingsStore.getValue("feature_maximised_widgets") &&
<AccessibleTooltipButton
className={isMaximised ? "mx_RoomSummaryCard_app_minimise" : "mx_RoomSummaryCard_app_maximise"}
onClick={toggleMaximised}
title={maximiseTitle}
yOffset={-24}
/> }
{ contextMenu }
</div>;
@ -208,17 +231,19 @@ const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
</Group>;
};
const onRoomMembersClick = () => {
export const onRoomMembersClick = (allowClose = true) => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomMemberList,
allowClose,
});
};
const onRoomFilesClick = () => {
export const onRoomFilesClick = (allowClose = true) => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.FilePanel,
allowClose,
});
};
@ -276,19 +301,17 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
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("%(count)s people", { count: memberCount }) }
{ _t("People") }
<span className="mx_BaseCard_Button_sublabel">
{ memberCount }
</span>
</Button>
<Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
{ _t("Show files") }
{ _t("Files") }
</Button>
<Button className="mx_RoomSummaryCard_icon_export" onClick={onRoomExportClick}>
{ _t("Export chat") }
</Button>
{ SettingsStore.getValue("feature_thread") && (
<Button className="mx_RoomSummaryCard_icon_threads" onClick={dispatchShowThreadsPanelEvent}>
{ _t("Show threads") }
</Button>
) }
<Button className="mx_RoomSummaryCard_icon_share" onClick={onShareRoomClick}>
{ _t("Share room") }
</Button>

View file

@ -0,0 +1,207 @@
/*
Copyright 2021 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 { EventSubscription } from "fbemitter";
import { EventTimelineSet, IEventRelation, MatrixEvent, Room } from 'matrix-js-sdk/src';
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 { replaceableComponent } from '../../../utils/replaceableComponent';
import { ActionPayload } from '../../../dispatcher/payloads';
import { Action } from '../../../dispatcher/actions';
import RoomViewStore from '../../../stores/RoomViewStore';
import ContentMessages from '../../../ContentMessages';
import UploadBar from '../../structures/UploadBar';
import SettingsStore from '../../../settings/SettingsStore';
interface IProps {
room: Room;
onClose: () => void;
resizeNotifier: ResizeNotifier;
permalinkCreator?: RoomPermalinkCreator;
e2eStatus?: E2EStatus;
timelineSet?: EventTimelineSet;
timelineRenderingType?: TimelineRenderingType;
showComposer?: boolean;
composerRelation?: IEventRelation;
}
interface IState {
thread?: Thread;
editState?: EditorStateTransfer;
replyToEvent?: MatrixEvent;
initialEventId?: string;
initialEventHighlighted?: boolean;
// settings:
showReadReceipts?: boolean;
}
@replaceableComponent("structures.TimelineCard")
export default class TimelineCard extends React.Component<IProps, IState> {
static contextType = RoomContext;
private dispatcherRef: string;
private timelinePanelRef: React.RefObject<TimelinePanel> = React.createRef();
private roomStoreToken: EventSubscription;
private settingWatchers: string[];
constructor(props: IProps) {
super(props);
this.state = {
showReadReceipts: false,
};
this.settingWatchers = [];
}
public componentDidMount(): void {
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
this.dispatcherRef = dis.register(this.onAction);
}
public componentWillUnmount(): void {
// Remove RoomStore listener
if (this.roomStoreToken) {
this.roomStoreToken.remove();
}
dis.unregister(this.dispatcherRef);
for (const watcher of this.settingWatchers) {
SettingsStore.unwatchSetting(watcher);
}
}
private onRoomViewStoreUpdate = async (initial?: boolean): Promise<void> => {
const roomId = this.props.room.roomId;
const newState: Pick<IState, any> = {
// roomLoading: RoomViewStore.isRoomLoading(),
// roomLoadError: RoomViewStore.getRoomLoadError(),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
initialEventId: RoomViewStore.getInitialEventId(),
initialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
replyToEvent: RoomViewStore.getQuotingEvent(),
};
this.settingWatchers = this.settingWatchers.concat([
SettingsStore.watchSetting("showReadReceipts", roomId, (...[,,, value]) =>
this.setState({ showReadReceipts: value as boolean }),
),
]);
this.setState(newState);
};
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.timelinePanelRef.current?.scrollToEventIfNeeded(payload.event.getId());
}
});
break;
default:
break;
}
};
private onScroll = (): void => {
if (this.state.initialEventId && this.state.initialEventHighlighted) {
dis.dispatch({
action: Action.ViewRoom,
room_id: this.props.room.roomId,
event_id: this.state.initialEventId,
highlighted: false,
replyingToEvent: this.state.replyToEvent,
});
}
};
private renderTimelineCardHeader = (): JSX.Element => {
return <div className="mx_TimelineCard__header">
<span>{ _t("Chat") }</span>
</div>;
};
public render(): JSX.Element {
const highlightedEventId = this.state.initialEventHighlighted
? this.state.initialEventId
: null;
return (
<RoomContext.Provider value={{
...this.context,
timelineRenderingType: this.props.timelineRenderingType ?? this.context.timelineRenderingType,
liveTimeline: this.props.timelineSet.getLiveTimeline(),
}}>
<BaseCard
className="mx_ThreadPanel mx_TimelineCard"
onClose={this.props.onClose}
withoutScrollContainer={true}
header={this.renderTimelineCardHeader()}
>
<TimelinePanel
ref={this.timelinePanelRef}
showReadReceipts={/*this.state.showReadReceipts*/ false} // TODO: RR's cause issues with limited horizontal space
manageReadReceipts={true}
manageReadMarkers={false} // No RM support in the TimelineCard
sendReadReceiptOnLoad={true}
timelineSet={this.props.timelineSet}
showUrlPreview={true}
layout={Layout.Group}
hideThreadedMessages={false}
hidden={false}
showReactions={true}
className="mx_RoomView_messagePanel mx_GroupLayout"
permalinkCreator={this.props.permalinkCreator}
membersLoaded={true}
editState={this.state.editState}
eventId={this.state.initialEventId}
resizeNotifier={this.props.resizeNotifier}
highlightedEventId={highlightedEventId}
onUserScroll={this.onScroll}
/>
{ ContentMessages.sharedInstance().getCurrentUploads(this.props.composerRelation).length > 0 && (
<UploadBar room={this.props.room} relation={this.props.composerRelation} />
) }
<MessageComposer
room={this.props.room}
relation={this.props.composerRelation}
resizeNotifier={this.props.resizeNotifier}
replyToEvent={this.state.replyToEvent}
permalinkCreator={this.props.permalinkCreator}
e2eStatus={this.props.e2eStatus}
compact={true}
/>
</BaseCard>
</RoomContext.Provider>
);
}
}

View file

@ -70,7 +70,7 @@ import RoomName from "../elements/RoomName";
import { mediaFromMxc } from "../../../customisations/Media";
import UIStore from "../../../stores/UIStore";
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import ConfirmSpaceUserActionDialog from "../dialogs/ConfirmSpaceUserActionDialog";
import { bulkSpaceBehaviour } from "../../../utils/space";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
@ -125,7 +125,7 @@ async function openDMForUser(matrixClient: MatrixClient, userId: string) {
if (lastActiveRoom) {
dis.dispatch({
action: 'view_room',
action: Action.ViewRoom,
room_id: lastActiveRoom.roomId,
});
return;
@ -367,7 +367,7 @@ const UserOptionsSection: React.FC<{
const onReadReceiptButton = function() {
const room = cli.getRoom(member.roomId);
dis.dispatch({
action: 'view_room',
action: Action.ViewRoom,
highlighted: true,
event_id: room.getEventReadUpTo(member.userId),
room_id: member.roomId,
@ -1121,6 +1121,10 @@ const PowerLevelEditor: React.FC<{
const cli = useContext(MatrixClientContext);
const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
useEffect(() => {
setSelectedPowerLevel(user.powerLevel);
}, [user]);
const onPowerChange = useCallback(async (powerLevel: number) => {
setSelectedPowerLevel(powerLevel);

View file

@ -222,7 +222,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
if (this.props.isRoomEncrypted) {
text = _t("Verify all users in a room to ensure it's secure.");
} else {
text = _t("In encrypted rooms, verify all users to ensure its secure.");
text = _t("In encrypted rooms, verify all users to ensure it's secure.");
}
}