Refactor all of Devtools and tidy it up (#8097)

pull/21833/head
Michael Telatynski 2022-03-23 20:17:57 +00:00 committed by GitHub
parent 64871c057b
commit 306ddd51e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1516 additions and 1562 deletions

View File

@ -66,8 +66,9 @@ limitations under the License.
margin: 0 0 16px;
}
.mx_AccessibleButton_kind_primary_outline {
.mx_AccessibleButton_hasKind {
display: block;
margin-top: 4px;
}
> div > h4 {

View File

@ -14,36 +14,40 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_DevTools_content {
margin: 10px 0;
.mx_DevtoolsDialog_wrapper {
.mx_Dialog {
height: 100%;
}
.mx_Dialog_fixedWidth {
overflow-y: hidden;
height: 100%;
}
}
.mx_DevTools_ServersInRoomList_button {
/* Set the cursor back to default as `.mx_Dialog button` sets it to pointer */
cursor: default !important;
.mx_DevTools_content {
margin: 10px 0;
overflow-y: auto;
height: calc(100% - 124px); // 58px for buttons + 50px for header + 8px margin around
}
.mx_DevTools_RoomStateExplorer_query {
margin-bottom: 10px;
}
.mx_DevTools_RoomStateExplorer_button {
font-family: monospace;
.mx_DevTools_button {
font-family: monospace !important;
margin-bottom: 8px !important;
}
.mx_DevTools_RoomStateExplorer_button_hasSpaces {
text-decoration: underline;
}
.mx_DevTools_RoomStateExplorer_button.mx_DevTools_RoomStateExplorer_button_emptyString {
.mx_DevTools_button.mx_DevTools_RoomStateExplorer_button_emptyString {
font-style: italic;
}
.mx_DevTools_RoomStateExplorer_button, .mx_DevTools_ServersInRoomList_button {
margin-bottom: 10px;
width: 100%;
}
.mx_DevTools_label_left {
float: left;
}
@ -83,108 +87,6 @@ limitations under the License.
margin-right: 42px;
}
.mx_DevTools_tgl {
display: none;
// add default box-sizing for this scope
&,
&::after,
&::before,
& *,
& *::after,
& *::before,
& + .mx_DevTools_tgl-btn {
box-sizing: border-box;
&::selection {
background: none;
}
}
+ .mx_DevTools_tgl-btn {
outline: 0;
display: block;
width: 7em;
height: 2em;
position: relative;
cursor: pointer;
user-select: none;
&::after,
&::before {
position: relative;
display: block;
content: "";
width: 50%;
height: 100%;
}
&::after {
left: 0;
}
&::before {
display: none;
}
}
&:checked + .mx_DevTools_tgl-btn::after {
left: 50%;
}
}
.mx_DevTools_tgl-flip {
+ .mx_DevTools_tgl-btn {
padding: 2px;
transition: all .2s ease;
perspective: 100px;
&::after,
&::before {
display: inline-block;
transition: all .4s ease;
width: 100%;
text-align: center;
position: absolute;
line-height: 2em;
font-weight: bold;
color: #fff;
top: 0;
left: 0;
backface-visibility: hidden;
border-radius: 4px;
}
&::after {
content: attr(data-tg-on);
background: #02c66f;
transform: rotateY(-180deg);
}
&::before {
background: #ff3a19;
content: attr(data-tg-off);
}
&:active::before {
transform: rotateY(-20deg);
}
}
&:checked + .mx_DevTools_tgl-btn {
&::before {
transform: rotateY(180deg);
}
&::after {
transform: rotateY(0);
left: 0;
background: #7fc6a6;
}
&:active::after {
transform: rotateY(20deg);
}
}
}
.mx_DevTools_VerificationRequest {
border: 1px solid #cccccc;
border-radius: 3px;

View File

@ -292,6 +292,10 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/export.svg');
}
.mx_RoomTile_iconDeveloperTools::before {
mask-image: url('$(res)/img/element-icons/settings/flask.svg');
}
.mx_RoomTile_iconCopyLink::before {
mask-image: url('$(res)/img/element-icons/link.svg');
}

View File

@ -922,7 +922,7 @@ export const Commands = [
command: 'devtools',
description: _td('Opens the Developer Tools dialog'),
runFn: function(roomId) {
Modal.createDialog(DevtoolsDialog, { roomId });
Modal.createDialog(DevtoolsDialog, { roomId }, "mx_DevtoolsDialog_wrapper");
return success();
},
category: CommandCategories.advanced,

View File

@ -22,12 +22,14 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import SyntaxHighlight from "../views/elements/SyntaxHighlight";
import { _t } from "../../languageHandler";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog";
import { canEditContent } from "../../utils/EventUtils";
import { MatrixClientPeg } from '../../MatrixClientPeg';
import { replaceableComponent } from "../../utils/replaceableComponent";
import { IDialogProps } from "../views/dialogs/IDialogProps";
import BaseDialog from "../views/dialogs/BaseDialog";
import { DevtoolsContext } from "../views/dialogs/devtools/BaseTool";
import { StateEventEditor } from "../views/dialogs/devtools/RoomState";
import { stringify, TimelineEventEditor } from "../views/dialogs/devtools/Event";
interface IProps extends IDialogProps {
mxEvent: MatrixEvent; // the MatrixEvent associated with the context menu
@ -47,10 +49,10 @@ export default class ViewSource extends React.Component<IProps, IState> {
};
}
private onBack(): void {
private onBack = (): void => {
// TODO: refresh the "Event ID:" modal header
this.setState({ isEditing: false });
}
};
private onEdit(): void {
this.setState({ isEditing: true });
@ -71,13 +73,13 @@ export default class ViewSource extends React.Component<IProps, IState> {
<summary>
<span className="mx_ViewSource_heading">{ _t("Decrypted event source") }</span>
</summary>
<SyntaxHighlight language="json">{ JSON.stringify(decryptedEventSource, null, 2) }</SyntaxHighlight>
<SyntaxHighlight language="json">{ stringify(decryptedEventSource) }</SyntaxHighlight>
</details>
<details className="mx_ViewSource_details">
<summary>
<span className="mx_ViewSource_heading">{ _t("Original event source") }</span>
</summary>
<SyntaxHighlight language="json">{ JSON.stringify(originalEventSource, null, 2) }</SyntaxHighlight>
<SyntaxHighlight language="json">{ stringify(originalEventSource) }</SyntaxHighlight>
</details>
</>
);
@ -85,84 +87,40 @@ export default class ViewSource extends React.Component<IProps, IState> {
return (
<>
<div className="mx_ViewSource_heading">{ _t("Original event source") }</div>
<SyntaxHighlight language="json">{ JSON.stringify(originalEventSource, null, 2) }</SyntaxHighlight>
<SyntaxHighlight language="json">{ stringify(originalEventSource) }</SyntaxHighlight>
</>
);
}
}
// returns the id of the initial message, not the id of the previous edit
private getBaseEventId(): string {
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
const isEncrypted = mxEvent.isEncrypted();
const baseMxEvent = this.props.mxEvent;
if (isEncrypted) {
// `relates_to` field is inside the encrypted event
return mxEvent.event.content["m.relates_to"]?.event_id ?? baseMxEvent.getId();
} else {
return mxEvent.getContent()["m.relates_to"]?.event_id ?? baseMxEvent.getId();
}
}
// returns the SendCustomEvent component prefilled with the correct details
private editSourceContent(): JSX.Element {
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
const isStateEvent = mxEvent.isState();
const roomId = mxEvent.getRoomId();
const originalContent = mxEvent.getContent();
if (isStateEvent) {
return (
<MatrixClientContext.Consumer>
{ (cli) => (
<SendCustomEvent
room={cli.getRoom(roomId)}
forceStateEvent={true}
onBack={() => this.onBack()}
inputs={{
eventType: mxEvent.getType(),
evContent: JSON.stringify(originalContent, null, "\t"),
stateKey: mxEvent.getStateKey(),
}}
/>
) }
</MatrixClientContext.Consumer>
);
} else {
// prefill an edit-message event
// keep only the `body` and `msgtype` fields of originalContent
const bodyToStartFrom = originalContent["m.new_content"]?.body ?? originalContent.body; // prefill the last edit body, to start editing from there
const newContent = {
"body": ` * ${bodyToStartFrom}`,
"msgtype": originalContent.msgtype,
"m.new_content": {
body: bodyToStartFrom,
msgtype: originalContent.msgtype,
},
"m.relates_to": {
rel_type: "m.replace",
event_id: this.getBaseEventId(),
},
};
return (
<MatrixClientContext.Consumer>
{ (cli) => (
<SendCustomEvent
room={cli.getRoom(roomId)}
forceStateEvent={false}
forceGeneralEvent={true}
onBack={() => this.onBack()}
inputs={{
eventType: mxEvent.getType(),
evContent: JSON.stringify(newContent, null, "\t"),
}}
/>
<DevtoolsContext.Provider value={{ room: cli.getRoom(roomId) }}>
<StateEventEditor onBack={this.onBack} mxEvent={mxEvent} />
</DevtoolsContext.Provider>
) }
</MatrixClientContext.Consumer>
);
}
return (
<MatrixClientContext.Consumer>
{ (cli) => (
<DevtoolsContext.Provider value={{ room: cli.getRoom(roomId) }}>
<TimelineEventEditor onBack={this.onBack} mxEvent={mxEvent} />
</DevtoolsContext.Provider>
) }
</MatrixClientContext.Consumer>
);
}
private canSendStateEvent(mxEvent: MatrixEvent): boolean {
@ -181,8 +139,8 @@ export default class ViewSource extends React.Component<IProps, IState> {
return (
<BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}>
<div>
<div>Room ID: { roomId }</div>
<div>Event ID: { eventId }</div>
<div>{ _t("Room ID: %(roomId)s", { roomId }) }</div>
<div>{ _t("Event ID: %(eventId)s", { eventId }) }</div>
<div className="mx_ViewSource_separator" />
{ isEditing ? this.editSourceContent() : this.viewSourceContent() }
</div>

View File

@ -359,13 +359,16 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
}
}
const viewSourceButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconSource"
label={_t("View source")}
onClick={this.onViewSourceClick}
/>
);
let viewSourceButton: JSX.Element;
if (SettingsStore.getValue("developerMode")) {
viewSourceButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconSource"
label={_t("View source")}
onClick={this.onViewSourceClick}
/>
);
}
if (this.props.eventTileOps) {
if (this.props.eventTileOps.isWidgetHidden()) {

View File

@ -48,6 +48,8 @@ import PosthogTrackers from "../../../PosthogTrackers";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import SettingsStore from "../../../settings/SettingsStore";
import DevtoolsDialog from "../dialogs/DevtoolsDialog";
interface IProps extends IContextMenuProps {
room: Room;
@ -353,6 +355,20 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
iconClassName="mx_RoomTile_iconExport"
/>
{ SettingsStore.getValue("developerMode") && <IconizedContextMenuOption
onClick={(ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
Modal.createDialog(DevtoolsDialog, {
roomId: RoomViewStore.getRoomId(),
}, "mx_DevtoolsDialog_wrapper");
onFinished();
}}
label={_t("Developer tools")}
iconClassName="mx_RoomTile_iconDeveloperTools"
/> }
{ leaveOption }
</IconizedContextMenuOptionList>
</IconizedContextMenu>;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,117 @@
/*
Copyright 2022 Michael Telatynski <7t3chguy@gmail.com>
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, { useContext, useMemo, useState } from "react";
import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event";
import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool";
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
import { EventEditor, EventViewer, eventTypeField, IEditorProps, stringify } from "./Event";
import FilteredList from "./FilteredList";
import { _t } from "../../../../languageHandler";
export const AccountDataEventEditor = ({ mxEvent, onBack }: IEditorProps) => {
const cli = useContext(MatrixClientContext);
const fields = useMemo(() => [
eventTypeField(mxEvent?.getType()),
], [mxEvent]);
const onSend = ([eventType]: string[], content?: IContent) => {
return cli.setAccountData(eventType, content);
};
const defaultContent = mxEvent ? stringify(mxEvent.getContent()) : undefined;
return <EventEditor fieldDefs={fields} defaultContent={defaultContent} onSend={onSend} onBack={onBack} />;
};
export const RoomAccountDataEventEditor = ({ mxEvent, onBack }: IEditorProps) => {
const context = useContext(DevtoolsContext);
const cli = useContext(MatrixClientContext);
const fields = useMemo(() => [
eventTypeField(mxEvent?.getType()),
], [mxEvent]);
const onSend = ([eventType]: string[], content?: IContent) => {
return cli.setRoomAccountData(context.room.roomId, eventType, content);
};
const defaultContent = mxEvent ? stringify(mxEvent.getContent()) : undefined;
return <EventEditor fieldDefs={fields} defaultContent={defaultContent} onSend={onSend} onBack={onBack} />;
};
interface IProps extends IDevtoolsProps {
events: Record<string, MatrixEvent>;
Editor: React.FC<IEditorProps>;
actionLabel: string;
}
const BaseAccountDataExplorer = ({ events, Editor, actionLabel, onBack, setTool }: IProps) => {
const [query, setQuery] = useState("");
const [event, setEvent] = useState<MatrixEvent>(null);
if (event) {
const onBack = () => {
setEvent(null);
};
return <EventViewer mxEvent={event} onBack={onBack} Editor={Editor} />;
}
const onAction = async () => {
setTool(actionLabel, Editor);
};
return <BaseTool onBack={onBack} actionLabel={actionLabel} onAction={onAction}>
<FilteredList query={query} onChange={setQuery}>
{
Object.entries(events).map(([eventType, ev]) => {
const onClick = () => {
setEvent(ev);
};
return <button className="mx_DevTools_button" key={eventType} onClick={onClick}>
{ eventType }
</button>;
})
}
</FilteredList>
</BaseTool>;
};
export const AccountDataExplorer = ({ onBack, setTool }: IDevtoolsProps) => {
const cli = useContext(MatrixClientContext);
return <BaseAccountDataExplorer
events={cli.store.accountData}
Editor={AccountDataEventEditor}
actionLabel={_t("Send custom account data event")}
onBack={onBack}
setTool={setTool}
/>;
};
export const RoomAccountDataExplorer = ({ onBack, setTool }: IDevtoolsProps) => {
const context = useContext(DevtoolsContext);
return <BaseAccountDataExplorer
events={context.room.accountData}
Editor={RoomAccountDataEventEditor}
actionLabel={_t("Send custom room account data event")}
onBack={onBack}
setTool={setTool}
/>;
};

View File

@ -0,0 +1,88 @@
/*
Copyright 2022 Michael Telatynski <7t3chguy@gmail.com>
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, { createContext, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import classNames from "classnames";
import { _t } from "../../../../languageHandler";
import { XOR } from "../../../../@types/common";
import { Tool } from "../DevtoolsDialog";
export interface IDevtoolsProps {
onBack(): void;
setTool(label: string, tool: Tool): void;
}
interface IMinProps extends Pick<IDevtoolsProps, "onBack"> {
className?: string;
}
interface IProps extends IMinProps {
actionLabel: string;
onAction(): Promise<string | void>;
}
const BaseTool: React.FC<XOR<IMinProps, IProps>> = ({ className, actionLabel, onBack, onAction, children }) => {
const [message, setMessage] = useState<string>(null);
const onBackClick = () => {
if (message) {
setMessage(null);
} else {
onBack();
}
};
let actionButton: JSX.Element;
if (message) {
children = message;
} else if (onAction) {
const onActionClick = () => {
onAction().then((msg) => {
if (typeof msg === "string") {
setMessage(msg);
}
});
};
actionButton = (
<button onClick={onActionClick}>
{ actionLabel }
</button>
);
}
return <>
<div className={classNames("mx_DevTools_content", className)}>
{ children }
</div>
<div className="mx_Dialog_buttons">
<button onClick={onBackClick}>
{ _t("Back") }
</button>
{ actionButton }
</div>
</>;
};
export default BaseTool;
interface IContext {
room: Room;
}
export const DevtoolsContext = createContext<IContext>({} as IContext);

View File

@ -0,0 +1,209 @@
/*
Copyright 2022 Michael Telatynski <7t3chguy@gmail.com>
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, { useContext, useMemo, useRef, useState } from "react";
import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t, _td } from "../../../../languageHandler";
import Field from "../../elements/Field";
import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool";
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
import withValidation from "../../elements/Validation";
import SyntaxHighlight from "../../elements/SyntaxHighlight";
export const stringify = (object: object): string => {
return JSON.stringify(object, null, 2);
};
interface IEventEditorProps extends Pick<IDevtoolsProps, "onBack"> {
fieldDefs: IFieldDef[]; // immutable
defaultContent?: string;
onSend(fields: string[], content?: IContent): Promise<unknown>;
}
interface IFieldDef {
id: string;
label: string; // _td
default?: string;
}
export const eventTypeField = (defaultValue?: string): IFieldDef => ({
id: "eventType",
label: _td("Event Type"),
default: defaultValue,
});
export const stateKeyField = (defaultValue?: string): IFieldDef => ({
id: "stateKey",
label: _td("State Key"),
default: defaultValue,
});
const validateEventContent = withValidation<any, Error | undefined>({
deriveData({ value }) {
try {
JSON.parse(value);
} catch (e) {
return e;
}
},
rules: [{
key: "validJson",
test: ({ value }, error) => {
if (!value) return true;
return !error;
},
invalid: (error) => _t("Doesn't look like valid JSON.") + " " + error,
}],
});
export const EventEditor = ({ fieldDefs, defaultContent = "{\n\n}", onSend, onBack }: IEventEditorProps) => {
const [fieldData, setFieldData] = useState<string[]>(fieldDefs.map(def => def.default ?? ""));
const [content, setContent] = useState<string>(defaultContent);
const contentField = useRef<Field>();
const fields = fieldDefs.map((def, i) => (
<Field
key={def.id}
id={def.id}
label={_t(def.label)}
size={42}
autoFocus={defaultContent === undefined && i === 0}
type="text"
autoComplete="on"
value={fieldData[i]}
onChange={ev => setFieldData(data => {
data[i] = ev.target.value;
return [...data];
})}
/>
));
const onAction = async () => {
const valid = await contentField.current.validate({});
if (!valid) {
contentField.current.focus();
contentField.current.validate({ focused: true });
return;
}
try {
const json = JSON.parse(content);
await onSend(fieldData, json);
} catch (e) {
return _t("Failed to send event!") + ` (${e.toString()})`;
}
return _t("Event sent!");
};
return <BaseTool
actionLabel={_t("Send")}
onAction={onAction}
onBack={onBack}
>
<div className="mx_DevTools_eventTypeStateKeyGroup">
{ fields }
</div>
<Field
id="evContent"
label={_t("Event Content")}
type="text"
className="mx_DevTools_textarea"
autoComplete="off"
value={content}
onChange={ev => setContent(ev.target.value)}
element="textarea"
onValidate={validateEventContent}
ref={contentField}
autoFocus={!!defaultContent}
/>
</BaseTool>;
};
export interface IEditorProps extends Pick<IDevtoolsProps, "onBack"> {
mxEvent?: MatrixEvent;
}
interface IViewerProps extends Required<IEditorProps> {
Editor: React.FC<Required<IEditorProps>>;
}
export const EventViewer = ({ mxEvent, onBack, Editor }: IViewerProps) => {
const [editing, setEditing] = useState(false);
if (editing) {
const onBack = () => {
setEditing(false);
};
return <Editor mxEvent={mxEvent} onBack={onBack} />;
}
const onAction = async () => {
setEditing(true);
};
return <BaseTool onBack={onBack} actionLabel={_t("Edit")} onAction={onAction}>
<SyntaxHighlight language="json">
{ stringify(mxEvent.event) }
</SyntaxHighlight>
</BaseTool>;
};
// returns the id of the initial message, not the id of the previous edit
const getBaseEventId = (baseEvent: MatrixEvent): string => {
// show the replacing event, not the original, if it is an edit
const mxEvent = baseEvent.replacingEvent() ?? baseEvent;
return mxEvent.getWireContent()["m.relates_to"]?.event_id ?? baseEvent.getId();
};
export const TimelineEventEditor = ({ mxEvent, onBack }: IEditorProps) => {
const context = useContext(DevtoolsContext);
const cli = useContext(MatrixClientContext);
const fields = useMemo(() => [
eventTypeField(mxEvent?.getType()),
], [mxEvent]);
const onSend = ([eventType]: string[], content?: IContent) => {
return cli.sendEvent(context.room.roomId, eventType, content);
};
let defaultContent: string;
if (mxEvent) {
const originalContent = mxEvent.getContent();
// prefill an edit-message event, keep only the `body` and `msgtype` fields of originalContent
const bodyToStartFrom = originalContent["m.new_content"]?.body ?? originalContent.body; // prefill the last edit body, to start editing from there
const newContent = {
"body": ` * ${bodyToStartFrom}`,
"msgtype": originalContent.msgtype,
"m.new_content": {
body: bodyToStartFrom,
msgtype: originalContent.msgtype,
},
"m.relates_to": {
rel_type: "m.replace",
event_id: getBaseEventId(mxEvent),
},
};
defaultContent = stringify(newContent);
}
return <EventEditor fieldDefs={fields} defaultContent={defaultContent} onSend={onSend} onBack={onBack} />;
};

View File

@ -0,0 +1,90 @@
/*
Copyright 2022 Michael Telatynski <7t3chguy@gmail.com>
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, { useEffect, useState } from "react";
import { _t } from "../../../../languageHandler";
import Field from "../../elements/Field";
import TruncatedList from "../../elements/TruncatedList";
const INITIAL_LOAD_TILES = 20;
const LOAD_TILES_STEP_SIZE = 50;
interface IProps {
children: React.ReactElement[];
query: string;
onChange(value: string): void;
}
const FilteredList = ({ children, query, onChange }: IProps) => {
const [truncateAt, setTruncateAt] = useState<number>(INITIAL_LOAD_TILES);
const [filteredChildren, setFilteredChildren] = useState<React.ReactElement[]>(children);
useEffect(() => {
let filteredChildren = children;
if (query) {
const lcQuery = query.toLowerCase();
filteredChildren = children.filter((child) => child.key.toString().toLowerCase().includes(lcQuery));
}
setFilteredChildren(filteredChildren);
setTruncateAt(INITIAL_LOAD_TILES);
}, [children, query]);
const getChildren = (start: number, end: number): React.ReactElement[] => {
return filteredChildren.slice(start, end);
};
const getChildCount = (): number => {
return filteredChildren.length;
};
const createOverflowElement = (overflowCount: number, totalCount: number) => {
const showMore = () => {
setTruncateAt(num => num + LOAD_TILES_STEP_SIZE);
};
return <button className="mx_DevTools_button" onClick={showMore}>
{ _t("and %(count)s others...", { count: overflowCount }) }
</button>;
};
return <>
<Field
label={_t('Filter results')}
autoFocus={true}
size={64}
type="text"
autoComplete="off"
value={query}
onChange={ev => onChange(ev.target.value)}
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
// force re-render so that autoFocus is applied when this component is re-used
key={children?.[0]?.key ?? ''}
/>
{ filteredChildren.length < 1
? _t("No results found")
: <TruncatedList
getChildren={getChildren}
getChildCount={getChildCount}
truncateAt={truncateAt}
createOverflowElement={createOverflowElement}
/>
}
</>;
};
export default FilteredList;

View File

@ -0,0 +1,132 @@
/*
Copyright 2022 Michael Telatynski <7t3chguy@gmail.com>
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, { useContext, useEffect, useMemo, useState } from "react";
import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event";
import classNames from "classnames";
import { _t } from "../../../../languageHandler";
import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool";
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
import { EventEditor, EventViewer, eventTypeField, stateKeyField, IEditorProps, stringify } from "./Event";
import FilteredList from "./FilteredList";
export const StateEventEditor = ({ mxEvent, onBack }: IEditorProps) => {
const context = useContext(DevtoolsContext);
const cli = useContext(MatrixClientContext);
const fields = useMemo(() => [
eventTypeField(mxEvent?.getType()),
stateKeyField(mxEvent?.getStateKey()),
], [mxEvent]);
const onSend = ([eventType, stateKey]: string[], content?: IContent) => {
return cli.sendStateEvent(context.room.roomId, eventType, content, stateKey);
};
const defaultContent = mxEvent ? stringify(mxEvent.getContent()) : undefined;
return <EventEditor fieldDefs={fields} defaultContent={defaultContent} onSend={onSend} onBack={onBack} />;
};
interface IEventTypeProps extends Pick<IDevtoolsProps, "onBack"> {
eventType: string;
}
const RoomStateExplorerEventType = ({ eventType, onBack }: IEventTypeProps) => {
const context = useContext(DevtoolsContext);
const [query, setQuery] = useState("");
const [event, setEvent] = useState<MatrixEvent>(null);
const events = context.room.currentState.events.get(eventType);
useEffect(() => {
if (events.size === 1 && events.has("")) {
setEvent(events.get(""));
} else {
setEvent(null);
}
}, [events]);
if (event) {
const onBack = () => {
setEvent(null);
};
return <EventViewer mxEvent={event} onBack={onBack} Editor={StateEventEditor} />;
}
return <BaseTool onBack={onBack}>
<FilteredList query={query} onChange={setQuery}>
{
Array.from(events.entries()).map(([stateKey, ev]) => {
const trimmed = stateKey.trim();
const onClick = () => {
setEvent(ev);
};
return <button
className={classNames("mx_DevTools_button", {
mx_DevTools_RoomStateExplorer_button_hasSpaces: trimmed.length !== stateKey.length,
mx_DevTools_RoomStateExplorer_button_emptyString: !trimmed,
})}
key={stateKey}
onClick={onClick}
>
{ trimmed ? stateKey : _t("<%(count)s spaces>", { count: stateKey.length }) }
</button>;
})
}
</FilteredList>
</BaseTool>;
};
export const RoomStateExplorer = ({ onBack, setTool }: IDevtoolsProps) => {
const context = useContext(DevtoolsContext);
const [query, setQuery] = useState("");
const [eventType, setEventType] = useState<string>(null);
const events = context.room.currentState.events;
if (eventType) {
const onBack = () => {
setEventType(null);
};
return <RoomStateExplorerEventType eventType={eventType} onBack={onBack} />;
}
const onAction = async () => {
setTool(_t("Send custom state event"), StateEventEditor);
};
return <BaseTool onBack={onBack} actionLabel={_t("Send custom state event")} onAction={onAction}>
<FilteredList query={query} onChange={setQuery}>
{
Array.from(events.keys()).map((eventType) => {
const onClick = () => {
setEventType(eventType);
};
return <button
className="mx_DevTools_button"
key={eventType}
onClick={onClick}
>
{ eventType }
</button>;
})
}
</FilteredList>
</BaseTool>;
};

View File

@ -0,0 +1,95 @@
/*
Copyright 2022 Michael Telatynski <7t3chguy@gmail.com>
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, { useContext } from "react";
import BaseTool, { IDevtoolsProps } from "./BaseTool";
import { _t } from "../../../../languageHandler";
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
import Spinner from "../../elements/Spinner";
import SyntaxHighlight from "../../elements/SyntaxHighlight";
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
const FAILED_TO_LOAD = Symbol("failed-to-load");
interface IServerWellKnown {
server: {
name: string;
version: string;
};
}
const ServerInfo = ({ onBack }: IDevtoolsProps) => {
const cli = useContext(MatrixClientContext);
const capabilities = useAsyncMemo(() => cli.getCapabilities(true).catch(() => FAILED_TO_LOAD), [cli]);
const clientVersions = useAsyncMemo(() => cli.getVersions().catch(() => FAILED_TO_LOAD), [cli]);
const serverVersions = useAsyncMemo<IServerWellKnown | symbol>(async () => {
let baseUrl = cli.getHomeserverUrl();
try {
const hsName = MatrixClientPeg.getHomeserverName();
// We don't use the js-sdk Autodiscovery module here as it only support client well-known, not server ones.
const response = await fetch(`https://${hsName}/.well-known/matrix/server`);
const json = await response.json();
if (json["m.server"]) {
baseUrl = `https://${json["m.server"]}`;
}
} catch (e) {
console.warn(e);
}
try {
const response = await fetch(`${baseUrl}/_matrix/federation/v1/version`);
return response.json();
} catch (e) {
console.warn(e);
}
return FAILED_TO_LOAD;
}, [cli]);
let body: JSX.Element;
if (!capabilities || !clientVersions || !serverVersions) {
body = <Spinner />;
} else {
body = <>
<h4>{ _t("Capabilities") }</h4>
{ capabilities !== FAILED_TO_LOAD
? <SyntaxHighlight language="json" children={JSON.stringify(capabilities, null, 4)} />
: <div>{ _t("Failed to load.") }</div>
}
<h4>{ _t("Client Versions") }</h4>
{ capabilities !== FAILED_TO_LOAD
? <SyntaxHighlight language="json" children={JSON.stringify(clientVersions, null, 4)} />
: <div>{ _t("Failed to load.") }</div>
}
<h4>{ _t("Server Versions") }</h4>
{ capabilities !== FAILED_TO_LOAD
? <SyntaxHighlight language="json" children={JSON.stringify(serverVersions, null, 4)} />
: <div>{ _t("Failed to load.") }</div>
}
</>;
}
return <BaseTool onBack={onBack}>
{ body }
</BaseTool>;
};
export default ServerInfo;

View File

@ -0,0 +1,56 @@
/*
Copyright 2022 Michael Telatynski <7t3chguy@gmail.com>
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, { useContext, useMemo } from "react";
import { EventType } from "matrix-js-sdk/src/@types/event";
import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool";
import { _t } from "../../../../languageHandler";
const ServersInRoom = ({ onBack }: IDevtoolsProps) => {
const context = useContext(DevtoolsContext);
const servers = useMemo<Record<string, number>>(() => {
const servers: Record<string, number> = {};
context.room.currentState.getStateEvents(EventType.RoomMember).forEach(ev => {
if (ev.getContent().membership !== "join") return; // only count joined users
const server = ev.getSender().split(":")[1];
servers[server] = (servers[server] ?? 0) + 1;
});
return servers;
}, [context.room]);
return <BaseTool onBack={onBack}>
<table>
<thead>
<tr>
<th>{ _t("Server") }</th>
<th>{ _t("Number of users") }</th>
</tr>
</thead>
<tbody>
{ Object.entries(servers).map(([server, numUsers]) => (
<tr key={server}>
<td>{ server }</td>
<td>{ numUsers }</td>
</tr>
)) }
</tbody>
</table>
</BaseTool>;
};
export default ServersInRoom;

View File

@ -0,0 +1,305 @@
/*
Copyright 2022 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2018-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, { useContext, useMemo, useState } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../../languageHandler";
import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool";
import AccessibleButton from "../../elements/AccessibleButton";
import SettingsStore, { LEVEL_ORDER } from "../../../../settings/SettingsStore";
import { SettingLevel } from "../../../../settings/SettingLevel";
import { SETTINGS } from "../../../../settings/Settings";
import Field from "../../elements/Field";
const SettingExplorer = ({ onBack }: IDevtoolsProps) => {
const [setting, setSetting] = useState<string>(null);
const [editing, setEditing] = useState(false);
if (setting && editing) {
const onBack = () => {
setEditing(false);
};
return <EditSetting setting={setting} onBack={onBack} />;
} else if (setting) {
const onBack = () => {
setSetting(null);
};
const onEdit = async () => {
setEditing(true);
};
return <ViewSetting setting={setting} onBack={onBack} onEdit={onEdit} />;
} else {
const onView = (setting: string) => {
setSetting(setting);
};
const onEdit = (setting: string) => {
setSetting(setting);
setEditing(true);
};
return <SettingsList onBack={onBack} onView={onView} onEdit={onEdit} />;
}
};
export default SettingExplorer;
interface ICanEditLevelFieldProps {
setting: string;
level: SettingLevel;
roomId?: string;
}
const CanEditLevelField = ({ setting, roomId, level }: ICanEditLevelFieldProps) => {
const canEdit = SettingsStore.canSetValue(setting, roomId, level);
const className = canEdit ? "mx_DevTools_SettingsExplorer_mutable" : "mx_DevTools_SettingsExplorer_immutable";
return <td className={className}><code>{ canEdit.toString() }</code></td>;
};
function renderExplicitSettingValues(setting: string, roomId: string): string {
const vals = {};
for (const level of LEVEL_ORDER) {
try {
vals[level] = SettingsStore.getValueAt(level, setting, roomId, true, true);
if (vals[level] === undefined) {
vals[level] = null;
}
} catch (e) {
logger.warn(e);
}
}
return JSON.stringify(vals, null, 4);
}
interface IEditSettingProps extends Pick<IDevtoolsProps, "onBack"> {
setting: string;
}
const EditSetting = ({ setting, onBack }: IEditSettingProps) => {
const context = useContext(DevtoolsContext);
const [explicitValue, setExplicitValue] = useState(renderExplicitSettingValues(setting, null));
const [explicitRoomValue, setExplicitRoomValue] =
useState(renderExplicitSettingValues(setting, context.room.roomId));
const onSave = async () => {
try {
const parsedExplicit = JSON.parse(explicitValue);
const parsedExplicitRoom = JSON.parse(explicitRoomValue);
for (const level of Object.keys(parsedExplicit)) {
logger.log(`[Devtools] Setting value of ${setting} at ${level} from user input`);
try {
const val = parsedExplicit[level];
await SettingsStore.setValue(setting, null, level as SettingLevel, val);
} catch (e) {
logger.warn(e);
}
}
const roomId = context.room.roomId;
for (const level of Object.keys(parsedExplicit)) {
logger.log(`[Devtools] Setting value of ${setting} at ${level} in ${roomId} from user input`);
try {
const val = parsedExplicitRoom[level];
await SettingsStore.setValue(setting, roomId, level as SettingLevel, val);
} catch (e) {
logger.warn(e);
}
}
onBack();
} catch (e) {
return _t("Failed to save settings.") + ` (${e.message})`;
}
};
return <BaseTool onBack={onBack} actionLabel={_t("Save setting values")} onAction={onSave}>
<h3>{ _t("Setting:") } <code>{ setting }</code></h3>
<div className="mx_DevTools_SettingsExplorer_warning">
<b>{ _t("Caution:") }</b> { _t("This UI does NOT check the types of the values. Use at your own risk.") }
</div>
<div>
{ _t("Setting definition:") }
<pre><code>{ JSON.stringify(SETTINGS[setting], null, 4) }</code></pre>
</div>
<div>
<table>
<thead>
<tr>
<th>{ _t("Level") }</th>
<th>{ _t("Settable at global") }</th>
<th>{ _t("Settable at room") }</th>
</tr>
</thead>
<tbody>
{ LEVEL_ORDER.map(lvl => (
<tr key={lvl}>
<td><code>{ lvl }</code></td>
<CanEditLevelField setting={setting} level={lvl} />
<CanEditLevelField setting={setting} roomId={context.room.roomId} level={lvl} />
</tr>
)) }
</tbody>
</table>
</div>
<div>
<Field
id="valExpl"
label={_t("Values at explicit levels")}
type="text"
className="mx_DevTools_textarea"
element="textarea"
autoComplete="off"
value={explicitValue}
onChange={e => setExplicitValue(e.target.value)}
/>
</div>
<div>
<Field
id="valExpl"
label={_t("Values at explicit levels in this room")}
type="text"
className="mx_DevTools_textarea"
element="textarea"
autoComplete="off"
value={explicitRoomValue}
onChange={e => setExplicitRoomValue(e.target.value)}
/>
</div>
</BaseTool>;
};
interface IViewSettingProps extends Pick<IDevtoolsProps, "onBack"> {
setting: string;
onEdit(): Promise<void>;
}
const ViewSetting = ({ setting, onEdit, onBack }: IViewSettingProps) => {
const context = useContext(DevtoolsContext);
return <BaseTool onBack={onBack} actionLabel={_t("Edit values")} onAction={onEdit}>
<h3>{ _t("Setting:") } <code>{ setting }</code></h3>
<div>
{ _t("Setting definition:") }
<pre><code>{ JSON.stringify(SETTINGS[setting], null, 4) }</code></pre>
</div>
<div>
{ _t("Value:") }&nbsp;
<code>{ renderSettingValue(SettingsStore.getValue(setting)) }</code>
</div>
<div>
{ _t("Value in this room:") }&nbsp;
<code>{ renderSettingValue(SettingsStore.getValue(setting, context.room.roomId)) }</code>
</div>
<div>
{ _t("Values at explicit levels:") }
<pre><code>{ renderExplicitSettingValues(setting, null) }</code></pre>
</div>
<div>
{ _t("Values at explicit levels in this room:") }
<pre><code>{ renderExplicitSettingValues(setting, context.room.roomId) }</code></pre>
</div>
</BaseTool>;
};
function renderSettingValue(val: any): string {
// Note: we don't .toString() a string because we want JSON.stringify to inject quotes for us
const toStringTypes = ["boolean", "number"];
if (toStringTypes.includes(typeof(val))) {
return val.toString();
} else {
return JSON.stringify(val);
}
}
interface ISettingsListProps extends Pick<IDevtoolsProps, "onBack"> {
onView(setting: string): void;
onEdit(setting: string): void;
}
const SettingsList = ({ onBack, onView, onEdit }: ISettingsListProps) => {
const context = useContext(DevtoolsContext);
const [query, setQuery] = useState("");
const allSettings = useMemo(() => {
let allSettings = Object.keys(SETTINGS);
if (query) {
const lcQuery = query.toLowerCase();
allSettings = allSettings.filter(setting => setting.toLowerCase().includes(lcQuery));
}
return allSettings;
}, [query]);
return <BaseTool onBack={onBack} className="mx_DevTools_SettingsExplorer">
<Field
label={_t("Filter results")}
autoFocus={true}
size={64}
type="text"
autoComplete="off"
value={query}
onChange={ev => setQuery(ev.target.value)}
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
/>
<table>
<thead>
<tr>
<th>{ _t("Setting ID") }</th>
<th>{ _t("Value") }</th>
<th>{ _t("Value in this room") }</th>
</tr>
</thead>
<tbody>
{ allSettings.map(i => (
<tr key={i}>
<td>
<AccessibleButton
kind="link_inline"
className="mx_DevTools_SettingsExplorer_setting"
onClick={() => onView(i)}
>
<code>{ i }</code>
</AccessibleButton>
<AccessibleButton
alt={_t("Edit setting")}
onClick={() => onEdit(i)}
className="mx_DevTools_SettingsExplorer_edit"
>
</AccessibleButton>
</td>
<td>
<code>{ renderSettingValue(SettingsStore.getValue(i)) }</code>
</td>
<td>
<code>
{ renderSettingValue(SettingsStore.getValue(i, context.room.roomId)) }
</code>
</td>
</tr>
)) }
</tbody>
</table>
</BaseTool>;
};

View File

@ -0,0 +1,101 @@
/*
Copyright 2022 Michael Telatynski <7t3chguy@gmail.com>
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, { useContext, useEffect, useState } from "react";
import {
PHASE_CANCELLED,
PHASE_DONE,
PHASE_READY,
PHASE_REQUESTED,
PHASE_STARTED,
PHASE_UNSENT,
VerificationRequest,
VerificationRequestEvent,
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { useTypedEventEmitter, useTypedEventEmitterState } from "../../../../hooks/useEventEmitter";
import { _t, _td } from "../../../../languageHandler";
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool";
const PHASE_MAP = {
[PHASE_UNSENT]: _td("Unsent"),
[PHASE_REQUESTED]: _td("Requested"),
[PHASE_READY]: _td("Ready"),
[PHASE_DONE]: _td("Done"),
[PHASE_STARTED]: _td("Started"),
[PHASE_CANCELLED]: _td("Cancelled"),
};
const VerificationRequestExplorer: React.FC<{
txnId: string;
request: VerificationRequest;
}> = ({ txnId, request }) => {
const [, updateState] = useState();
const [timeout, setRequestTimeout] = useState(request.timeout);
/* Re-render if something changes state */
useTypedEventEmitter(request, VerificationRequestEvent.Change, updateState);
/* Keep re-rendering if there's a timeout */
useEffect(() => {
if (request.timeout == 0) return;
/* Note that request.timeout is a getter, so its value changes */
const id = setInterval(() => {
setRequestTimeout(request.timeout);
}, 500);
return () => { clearInterval(id); };
}, [request]);
return (<div className="mx_DevTools_VerificationRequest">
<dl>
<dt>{ _t("Transaction") }</dt>
<dd>{ txnId }</dd>
<dt>{ _t("Phase") }</dt>
<dd>{ PHASE_MAP[request.phase] || request.phase }</dd> // TODO
<dt>{ _t("Timeout") }</dt>
<dd>{ Math.floor(timeout / 1000) }</dd>
<dt>{ _t("Methods") }</dt>
<dd>{ request.methods && request.methods.join(", ") }</dd>
<dt>{ _t("Requester") }</dt>
<dd>{ request.requestingUserId }</dd>
<dt>{ _t("Observe only") }</dt>
<dd>{ JSON.stringify(request.observeOnly) }</dd>
</dl>
</div>);
};
const VerificationExplorer = ({ onBack }: IDevtoolsProps) => {
const cli = useContext(MatrixClientContext);
const context = useContext(DevtoolsContext);
const requests = useTypedEventEmitterState(cli, CryptoEvent.VerificationRequest, () => {
return cli.crypto.inRoomVerificationRequests["requestsByRoomId"]?.get(context.room.roomId)
?? new Map<string, VerificationRequest>();
});
return <BaseTool onBack={onBack}>
{ Array.from(requests.entries()).reverse().map(([txnId, request]) =>
<VerificationRequestExplorer txnId={txnId} request={request} key={txnId} />,
) }
{ requests.size < 1 && _t("No verification requests found") }
</BaseTool>;
};
export default VerificationExplorer;

View File

@ -0,0 +1,68 @@
/*
Copyright 2022 Michael Telatynski <7t3chguy@gmail.com>
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, { useContext, useState } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { useEventEmitterState } from "../../../../hooks/useEventEmitter";
import { _t } from "../../../../languageHandler";
import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool";
import WidgetStore, { IApp } from "../../../../stores/WidgetStore";
import { UPDATE_EVENT } from "../../../../stores/AsyncStore";
import FilteredList from "./FilteredList";
import { StateEventEditor } from "./RoomState";
const WidgetExplorer = ({ onBack }: IDevtoolsProps) => {
const context = useContext(DevtoolsContext);
const [query, setQuery] = useState("");
const [widget, setWidget] = useState<IApp>(null);
const widgets = useEventEmitterState(WidgetStore.instance, UPDATE_EVENT, () => {
return WidgetStore.instance.getApps(context.room.roomId);
});
if (widget && widgets.includes(widget)) {
const onBack = () => {
setWidget(null);
};
const allState = Array.from(
Array.from(context.room.currentState.events.values()).map((e: Map<string, MatrixEvent>) => {
return e.values();
}),
).reduce((p, c) => { p.push(...c); return p; }, []);
const event = allState.find(ev => ev.getId() === widget.eventId);
if (!event) { // "should never happen"
return <BaseTool onBack={onBack}>
{ _t("There was an error finding this widget.") }
</BaseTool>;
}
return <StateEventEditor mxEvent={event} onBack={onBack} />;
}
return <BaseTool onBack={onBack}>
<FilteredList query={query} onChange={setQuery}>
{ widgets.map(w => (
<button className="mx_DevTools_button" key={w.url + w.eventId} onClick={() => setWidget(w)}>
{ w.url }
</button>
)) }
</FilteredList>
</BaseTool>;
};
export default WidgetExplorer;

View File

@ -30,6 +30,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleButton from "../elements/AccessibleButton";
import ConfirmAndWaitRedactDialog from "../dialogs/ConfirmAndWaitRedactDialog";
import ViewSource from "../../structures/ViewSource";
import SettingsStore from "../../../settings/SettingsStore";
function getReplacedContent(event) {
const originalContent = event.getOriginalContent();
@ -112,7 +113,7 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
private renderActionBar(): JSX.Element {
// hide the button when already redacted
let redactButton;
let redactButton: JSX.Element;
if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent && this.state.canRedact) {
redactButton = (
<AccessibleButton onClick={this.onRedactClick}>
@ -120,11 +121,16 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
</AccessibleButton>
);
}
const viewSourceButton = (
<AccessibleButton onClick={this.onViewSourceClick}>
{ _t("View Source") }
</AccessibleButton>
);
let viewSourceButton: JSX.Element;
if (SettingsStore.getValue("developerMode")) {
viewSourceButton = (
<AccessibleButton onClick={this.onViewSourceClick}>
{ _t("View Source") }
</AccessibleButton>
);
}
// disabled remove button when not allowed
return (
<div className="mx_MessageActionBar">

View File

@ -21,7 +21,6 @@ import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton";
import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog";
import DevtoolsDialog from "../../../dialogs/DevtoolsDialog";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher";
import { Action } from '../../../../../dispatcher/actions';
@ -83,10 +82,6 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, { room });
};
private openDevtools = (e) => {
Modal.createDialog(DevtoolsDialog, { roomId: this.props.roomId });
};
private onOldRoomClicked = (e) => {
e.preventDefault();
e.stopPropagation();
@ -169,12 +164,6 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
{ oldRoomLink }
{ roomUpgradeButton }
</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{ _t("Developer options") }</span>
<AccessibleButton onClick={this.openDevtools} kind='primary'>
{ _t("Open Devtools") }
</AccessibleButton>
</div>
</div>
);
}

View File

@ -110,19 +110,6 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
/>,
);
groups.getOrCreate(LabGroup.Developer, []).push(
<SettingsFlag
key="developerMode"
name="developerMode"
level={SettingLevel.ACCOUNT}
/>,
<SettingsFlag
key="showHiddenEventsInTimeline"
name="showHiddenEventsInTimeline"
level={SettingLevel.DEVICE}
/>,
);
groups.getOrCreate(LabGroup.Analytics, []).push(
<SettingsFlag
key="automaticErrorReporting"

View File

@ -33,6 +33,10 @@ import { Icon as PinUprightIcon } from '../../../../res/img/element-icons/room/p
import { Icon as EllipsisIcon } from '../../../../res/img/element-icons/room/ellipsis.svg';
import { Icon as MembersIcon } from '../../../../res/img/element-icons/room/members.svg';
import { Icon as FavoriteIcon } from '../../../../res/img/element-icons/roomlist/favorite.svg';
import SettingsStore from "../../../settings/SettingsStore";
import Modal from "../../../Modal";
import DevtoolsDialog from "../dialogs/DevtoolsDialog";
import RoomViewStore from "../../../stores/RoomViewStore";
const QuickSettingsButton = ({ isPanelCollapsed = false }) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
@ -63,6 +67,20 @@ const QuickSettingsButton = ({ isPanelCollapsed = false }) => {
{ _t("All settings") }
</AccessibleButton>
{ SettingsStore.getValue("developerMode") && (
<AccessibleButton
onClick={() => {
closeMenu();
Modal.createDialog(DevtoolsDialog, {
roomId: RoomViewStore.getRoomId(),
}, "mx_DevtoolsDialog_wrapper");
}}
kind="danger_outline"
>
{ _t("Developer tools") }
</AccessibleButton>
) }
<h4 className="mx_QuickSettingsButton_pinToSidebarHeading">
<PinUprightIcon className="mx_QuickSettingsButton_icon" />
{ _t("Pin to sidebar") }

View File

@ -931,7 +931,6 @@
"Enable URL previews by default for participants in this room": "Enable URL previews by default for participants in this room",
"Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets",
"Prompt before sending invites to potentially invalid matrix IDs": "Prompt before sending invites to potentially invalid matrix IDs",
"Show developer tools": "Show developer tools",
"Order rooms by name": "Order rooms by name",
"Show rooms with unread notifications first": "Show rooms with unread notifications first",
"Show shortcuts to recently viewed rooms above the room list": "Show shortcuts to recently viewed rooms above the room list",
@ -1104,6 +1103,7 @@
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
"Quick settings": "Quick settings",
"All settings": "All settings",
"Developer tools": "Developer tools",
"Pin to sidebar": "Pin to sidebar",
"More options": "More options",
"Settings": "Settings",
@ -1523,8 +1523,6 @@
"Internal room ID": "Internal room ID",
"Room version": "Room version",
"Room version:": "Room version:",
"Developer options": "Developer options",
"Open Devtools": "Open Devtools",
"This room is bridging messages to the following platforms. <a>Learn more.</a>": "This room is bridging messages to the following platforms. <a>Learn more.</a>",
"This room isn't bridging messages to any platforms. <a>Learn more.</a>": "This room isn't bridging messages to any platforms. <a>Learn more.</a>",
"Bridges": "Bridges",
@ -2447,48 +2445,19 @@
"Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.",
"Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.",
"Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)",
"Send": "Send",
"Send Custom Event": "Send Custom Event",
"You must specify an event type!": "You must specify an event type!",
"Event sent!": "Event sent!",
"Failed to send custom event.": "Failed to send custom event.",
"Event Type": "Event Type",
"State Key": "State Key",
"Event Content": "Event Content",
"Send Account Data": "Send Account Data",
"Filter results": "Filter results",
"Explore Room State": "Explore Room State",
"<%(count)s spaces>|other": "<%(count)s spaces>",
"<%(count)s spaces>|one": "<space>",
"<%(count)s spaces>|zero": "<empty string>",
"Explore Account Data": "Explore Account Data",
"View Servers in Room": "View Servers in Room",
"Verification Requests": "Verification Requests",
"Room": "Room",
"Send custom timeline event": "Send custom timeline event",
"Explore room state": "Explore room state",
"Explore room account data": "Explore room account data",
"View servers in room": "View servers in room",
"Verification explorer": "Verification explorer",
"Active Widgets": "Active Widgets",
"There was an error finding this widget.": "There was an error finding this widget.",
"Settings Explorer": "Settings Explorer",
"Failed to save settings": "Failed to save settings",
"Setting ID": "Setting ID",
"Value": "Value",
"Value in this room": "Value in this room",
"Edit setting": "Edit setting",
"Setting:": "Setting:",
"Caution:": "Caution:",
"This UI does NOT check the types of the values. Use at your own risk.": "This UI does NOT check the types of the values. Use at your own risk.",
"Setting definition:": "Setting definition:",
"Level": "Level",
"Settable at global": "Settable at global",
"Settable at room": "Settable at room",
"Values at explicit levels": "Values at explicit levels",
"Values at explicit levels in this room": "Values at explicit levels in this room",
"Save setting values": "Save setting values",
"Value:": "Value:",
"Value in this room:": "Value in this room:",
"Values at explicit levels:": "Values at explicit levels:",
"Values at explicit levels in this room:": "Values at explicit levels in this room:",
"Edit Values": "Edit Values",
"Explore account data": "Explore account data",
"Settings explorer": "Settings explorer",
"Server info": "Server info",
"Toolbox": "Toolbox",
"Developer Tools": "Developer Tools",
"Room ID: %(roomId)s": "Room ID: %(roomId)s",
"The poll has ended. No votes were cast.": "The poll has ended. No votes were cast.",
"The poll has ended. Top answer: %(topAnswer)s": "The poll has ended. Top answer: %(topAnswer)s",
"Failed to end poll": "Failed to end poll",
@ -2527,6 +2496,7 @@
"Sending": "Sending",
"Sent": "Sent",
"Open link": "Open link",
"Send": "Send",
"Forward message": "Forward message",
"Message preview": "Message preview",
"Search for rooms or people": "Search for rooms or people",
@ -2825,6 +2795,59 @@
"<b>Warning</b>: You should only set up key backup from a trusted computer.": "<b>Warning</b>: You should only set up key backup from a trusted computer.",
"Access your secure message history and set up secure messaging by entering your Security Key.": "Access your secure message history and set up secure messaging by entering your Security Key.",
"If you've forgotten your Security Key you can <button>set up new recovery options</button>": "If you've forgotten your Security Key you can <button>set up new recovery options</button>",
"Send custom account data event": "Send custom account data event",
"Send custom room account data event": "Send custom room account data event",
"Event Type": "Event Type",
"State Key": "State Key",
"Doesn't look like valid JSON.": "Doesn't look like valid JSON.",
"Failed to send event!": "Failed to send event!",
"Event sent!": "Event sent!",
"Event Content": "Event Content",
"Filter results": "Filter results",
"No results found": "No results found",
"<%(count)s spaces>|other": "<%(count)s spaces>",
"<%(count)s spaces>|one": "<space>",
"<%(count)s spaces>|zero": "<empty string>",
"Send custom state event": "Send custom state event",
"Capabilities": "Capabilities",
"Failed to load.": "Failed to load.",
"Client Versions": "Client Versions",
"Server Versions": "Server Versions",
"Server": "Server",
"Number of users": "Number of users",
"Failed to save settings.": "Failed to save settings.",
"Save setting values": "Save setting values",
"Setting:": "Setting:",
"Caution:": "Caution:",
"This UI does NOT check the types of the values. Use at your own risk.": "This UI does NOT check the types of the values. Use at your own risk.",
"Setting definition:": "Setting definition:",
"Level": "Level",
"Settable at global": "Settable at global",
"Settable at room": "Settable at room",
"Values at explicit levels": "Values at explicit levels",
"Values at explicit levels in this room": "Values at explicit levels in this room",
"Edit values": "Edit values",
"Value:": "Value:",
"Value in this room:": "Value in this room:",
"Values at explicit levels:": "Values at explicit levels:",
"Values at explicit levels in this room:": "Values at explicit levels in this room:",
"Setting ID": "Setting ID",
"Value": "Value",
"Value in this room": "Value in this room",
"Edit setting": "Edit setting",
"Unsent": "Unsent",
"Requested": "Requested",
"Ready": "Ready",
"Started": "Started",
"Cancelled": "Cancelled",
"Transaction": "Transaction",
"Phase": "Phase",
"Timeout": "Timeout",
"Methods": "Methods",
"Requester": "Requester",
"Observe only": "Observe only",
"No verification requests found": "No verification requests found",
"There was an error finding this widget.": "There was an error finding this widget.",
"Resume": "Resume",
"Hold": "Hold",
"Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)",
@ -2838,7 +2861,6 @@
"Forget": "Forget",
"Mentions only": "Mentions only",
"See room timeline (devtools)": "See room timeline (devtools)",
"Room": "Room",
"Space": "Space",
"Space home": "Space home",
"Manage & explore rooms": "Manage & explore rooms",
@ -3019,7 +3041,6 @@
"Mark as suggested": "Mark as suggested",
"Failed to load list of rooms.": "Failed to load list of rooms.",
"Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
"No results found": "No results found",
"You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.",
"Results": "Results",
"Rooms and spaces": "Rooms and spaces",
@ -3087,6 +3108,7 @@
"Could not load user profile": "Could not load user profile",
"Decrypted event source": "Decrypted event source",
"Original event source": "Original event source",
"Event ID: %(eventId)s": "Event ID: %(eventId)s",
"Unable to verify this device": "Unable to verify this device",
"Verify this device": "Verify this device",
"Device verified": "Device verified",

View File

@ -736,11 +736,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
displayName: _td('Prompt before sending invites to potentially invalid matrix IDs'),
default: true,
},
"showDeveloperTools": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Show developer tools'),
default: false,
},
"widgetOpenIDPermissions": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: {