import { getEventCords } from '../../../util/common';
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
import {
- openEmojiBoard, openProfileViewer, openReadReceipts, replyTo,
+ openEmojiBoard, openProfileViewer, openReadReceipts, openViewSource, replyTo,
} from '../../../client/action/navigation';
import { sanitizeCustomHtml } from '../../../util/sanitize';
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
+import CmdIC from '../../../../public/res/ic/outlined/cmd.svg';
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
function PlaceholderMessage() {
>
Read receipts
</MenuItem>
+ <MenuItem
+ iconSrc={CmdIC}
+ onClick={() => openViewSource(mEvent)}
+ >
+ View source
+ </MenuItem>
{(canIRedact || senderId === mx.getUserId()) && (
<>
<MenuBorder />
function PopupWindow({
className, isOpen, title, contentTitle,
drawer, drawerOptions, contentOptions,
- onRequestClose, children,
+ onAfterClose, onRequestClose, children,
}) {
const haveDrawer = drawer !== null;
const cTitle = contentTitle !== null ? contentTitle : title;
<RawModal
className={`${className === null ? '' : `${className} `}pw-model`}
isOpen={isOpen}
+ onAfterClose={onAfterClose}
onRequestClose={onRequestClose}
size={haveDrawer ? 'large' : 'medium'}
>
contentTitle: null,
drawerOptions: null,
contentOptions: null,
+ onAfterClose: null,
onRequestClose: null,
};
drawer: PropTypes.node,
drawerOptions: PropTypes.node,
contentOptions: PropTypes.node,
+ onAfterClose: PropTypes.func,
onRequestClose: PropTypes.func,
children: PropTypes.node.isRequired,
};
import ProfileViewer from '../profile-viewer/ProfileViewer';
import SpaceAddExisting from '../../molecules/space-add-existing/SpaceAddExisting';
import Search from '../search/Search';
+import ViewSource from '../view-source/ViewSource';
function Dialogs() {
return (
<>
<ReadReceipts />
+ <ViewSource />
<ProfileViewer />
<SpaceAddExisting />
<Search />
--- /dev/null
+import React, { useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import './ViewSource.scss';
+
+import cons from '../../../client/state/cons';
+import navigation from '../../../client/state/navigation';
+
+import IconButton from '../../atoms/button/IconButton';
+import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
+import ScrollView from '../../atoms/scroll/ScrollView';
+import PopupWindow from '../../molecules/popup-window/PopupWindow';
+
+import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
+
+function ViewSourceBlock({ title, json }) {
+ return (
+ <div className="view-source__card">
+ <MenuHeader>{title}</MenuHeader>
+ <ScrollView horizontal vertical={false} autoHide>
+ <pre className="text text-b1">
+ <code className="language-json">
+ {JSON.stringify(json, null, 2)}
+ </code>
+ </pre>
+ </ScrollView>
+ </div>
+ );
+}
+ViewSourceBlock.propTypes = {
+ title: PropTypes.string.isRequired,
+ json: PropTypes.shape({}).isRequired,
+};
+
+function ViewSource() {
+ const [isOpen, setIsOpen] = useState(false);
+ const [event, setEvent] = useState(null);
+
+ useEffect(() => {
+ const loadViewSource = (e) => {
+ setEvent(e);
+ setIsOpen(true);
+ };
+ navigation.on(cons.events.navigation.VIEWSOURCE_OPENED, loadViewSource);
+ return () => {
+ navigation.removeListener(cons.events.navigation.VIEWSOURCE_OPENED, loadViewSource);
+ };
+ }, []);
+
+ const handleAfterClose = () => {
+ setEvent(null);
+ };
+
+ const renderViewSource = () => (
+ <div className="view-source">
+ {event.isEncrypted() && <ViewSourceBlock title="Decrypted source" json={event.getEffectiveEvent()} />}
+ <ViewSourceBlock title="Original source" json={event.event} />
+ </div>
+ );
+
+ return (
+ <PopupWindow
+ isOpen={isOpen}
+ title="View source"
+ onAfterClose={handleAfterClose}
+ onRequestClose={() => setIsOpen(false)}
+ contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip="Close" />}
+ >
+ {event && renderViewSource()}
+ </PopupWindow>
+ );
+}
+
+export default ViewSource;
--- /dev/null
+@use '../../partials/dir';
+
+.view-source {
+ @include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
+
+ & pre {
+ padding: var(--sp-extra-tight);
+ }
+
+ &__card {
+ margin: var(--sp-normal) 0;
+ background-color: var(--bg-surface-hover);
+ border-radius: var(--bo-radius);
+ box-shadow: var(--bs-surface-border);
+ overflow: hidden;
+ }
+}
});
}
+export function openViewSource(event) {
+ appDispatcher.dispatch({
+ type: cons.actions.navigation.OPEN_VIEWSOURCE,
+ event,
+ });
+}
+
export function replyTo(userId, eventId, body) {
appDispatcher.dispatch({
type: cons.actions.navigation.CLICK_REPLY_TO,
OPEN_SETTINGS: 'OPEN_SETTINGS',
OPEN_EMOJIBOARD: 'OPEN_EMOJIBOARD',
OPEN_READRECEIPTS: 'OPEN_READRECEIPTS',
+ OPEN_VIEWSOURCE: 'OPEN_VIEWSOURCE',
CLICK_REPLY_TO: 'CLICK_REPLY_TO',
OPEN_SEARCH: 'OPEN_SEARCH',
OPEN_REUSABLE_CONTEXT_MENU: 'OPEN_REUSABLE_CONTEXT_MENU',
PROFILE_VIEWER_OPENED: 'PROFILE_VIEWER_OPENED',
EMOJIBOARD_OPENED: 'EMOJIBOARD_OPENED',
READRECEIPTS_OPENED: 'READRECEIPTS_OPENED',
+ VIEWSOURCE_OPENED: 'VIEWSOURCE_OPENED',
REPLY_TO_CLICKED: 'REPLY_TO_CLICKED',
SEARCH_OPENED: 'SEARCH_OPENED',
REUSABLE_CONTEXT_MENU_OPENED: 'REUSABLE_CONTEXT_MENU_OPENED',
action.userIds,
);
},
+ [cons.actions.navigation.OPEN_VIEWSOURCE]: () => {
+ this.emit(
+ cons.events.navigation.VIEWSOURCE_OPENED,
+ action.event,
+ );
+ },
[cons.actions.navigation.CLICK_REPLY_TO]: () => {
this.emit(
cons.events.navigation.REPLY_TO_CLICKED,