Refactor all of Devtools and tidy it up (#8097)
This commit is contained in:
parent
64871c057b
commit
306ddd51e4
24 changed files with 1516 additions and 1562 deletions
|
@ -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()) {
|
||||
|
|
|
@ -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
117
src/components/views/dialogs/devtools/AccountData.tsx
Normal file
117
src/components/views/dialogs/devtools/AccountData.tsx
Normal 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}
|
||||
/>;
|
||||
};
|
88
src/components/views/dialogs/devtools/BaseTool.tsx
Normal file
88
src/components/views/dialogs/devtools/BaseTool.tsx
Normal 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);
|
209
src/components/views/dialogs/devtools/Event.tsx
Normal file
209
src/components/views/dialogs/devtools/Event.tsx
Normal 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} />;
|
||||
};
|
90
src/components/views/dialogs/devtools/FilteredList.tsx
Normal file
90
src/components/views/dialogs/devtools/FilteredList.tsx
Normal 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;
|
132
src/components/views/dialogs/devtools/RoomState.tsx
Normal file
132
src/components/views/dialogs/devtools/RoomState.tsx
Normal 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>;
|
||||
};
|
95
src/components/views/dialogs/devtools/ServerInfo.tsx
Normal file
95
src/components/views/dialogs/devtools/ServerInfo.tsx
Normal 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;
|
56
src/components/views/dialogs/devtools/ServersInRoom.tsx
Normal file
56
src/components/views/dialogs/devtools/ServersInRoom.tsx
Normal 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;
|
305
src/components/views/dialogs/devtools/SettingExplorer.tsx
Normal file
305
src/components/views/dialogs/devtools/SettingExplorer.tsx
Normal 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:") }
|
||||
<code>{ renderSettingValue(SettingsStore.getValue(setting)) }</code>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{ _t("Value in this room:") }
|
||||
<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>;
|
||||
};
|
101
src/components/views/dialogs/devtools/VerificationExplorer.tsx
Normal file
101
src/components/views/dialogs/devtools/VerificationExplorer.tsx
Normal 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;
|
68
src/components/views/dialogs/devtools/WidgetExplorer.tsx
Normal file
68
src/components/views/dialogs/devtools/WidgetExplorer.tsx
Normal 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;
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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") }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue