diff --git a/src/app/molecules/message/Message.jsx b/src/app/molecules/message/Message.jsx index d3205d69..c1ceda87 100644 --- a/src/app/molecules/message/Message.jsx +++ b/src/app/molecules/message/Message.jsx @@ -14,7 +14,7 @@ import colorMXID from '../../../util/colorMXID'; 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'; @@ -33,6 +33,7 @@ import EmojiAddIC from '../../../../public/res/ic/outlined/emoji-add.svg'; 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() { @@ -510,6 +511,12 @@ const MessageOptions = React.memo(({ > Read receipts + openViewSource(mEvent)} + > + View source + {(canIRedact || senderId === mx.getUserId()) && ( <> diff --git a/src/app/molecules/popup-window/PopupWindow.jsx b/src/app/molecules/popup-window/PopupWindow.jsx index 43e421ea..1133252a 100644 --- a/src/app/molecules/popup-window/PopupWindow.jsx +++ b/src/app/molecules/popup-window/PopupWindow.jsx @@ -51,7 +51,7 @@ PWContentSelector.propTypes = { function PopupWindow({ className, isOpen, title, contentTitle, drawer, drawerOptions, contentOptions, - onRequestClose, children, + onAfterClose, onRequestClose, children, }) { const haveDrawer = drawer !== null; const cTitle = contentTitle !== null ? contentTitle : title; @@ -60,6 +60,7 @@ function PopupWindow({ @@ -116,6 +117,7 @@ PopupWindow.defaultProps = { contentTitle: null, drawerOptions: null, contentOptions: null, + onAfterClose: null, onRequestClose: null, }; @@ -127,6 +129,7 @@ PopupWindow.propTypes = { drawer: PropTypes.node, drawerOptions: PropTypes.node, contentOptions: PropTypes.node, + onAfterClose: PropTypes.func, onRequestClose: PropTypes.func, children: PropTypes.node.isRequired, }; diff --git a/src/app/organisms/pw/Dialogs.jsx b/src/app/organisms/pw/Dialogs.jsx index b5038e56..55c37f36 100644 --- a/src/app/organisms/pw/Dialogs.jsx +++ b/src/app/organisms/pw/Dialogs.jsx @@ -4,11 +4,13 @@ import ReadReceipts from '../read-receipts/ReadReceipts'; 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 ( <> + diff --git a/src/app/organisms/view-source/ViewSource.jsx b/src/app/organisms/view-source/ViewSource.jsx new file mode 100644 index 00000000..a79a4d9c --- /dev/null +++ b/src/app/organisms/view-source/ViewSource.jsx @@ -0,0 +1,73 @@ +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 ( +
+ {title} + +
+          
+            {JSON.stringify(json, null, 2)}
+          
+        
+
+
+ ); +} +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 = () => ( +
+ {event.isEncrypted() && } + +
+ ); + + return ( + setIsOpen(false)} + contentOptions={ setIsOpen(false)} tooltip="Close" />} + > + {event && renderViewSource()} + + ); +} + +export default ViewSource; diff --git a/src/app/organisms/view-source/ViewSource.scss b/src/app/organisms/view-source/ViewSource.scss new file mode 100644 index 00000000..81b53f39 --- /dev/null +++ b/src/app/organisms/view-source/ViewSource.scss @@ -0,0 +1,17 @@ +@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; + } +} diff --git a/src/client/action/navigation.js b/src/client/action/navigation.js index 964a3f5b..ab01e38a 100644 --- a/src/client/action/navigation.js +++ b/src/client/action/navigation.js @@ -109,6 +109,13 @@ export function openReadReceipts(roomId, userIds) { }); } +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, diff --git a/src/client/state/cons.js b/src/client/state/cons.js index 2d20dbee..8ff6eede 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -42,6 +42,7 @@ const cons = { 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', @@ -82,6 +83,7 @@ const cons = { 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', diff --git a/src/client/state/navigation.js b/src/client/state/navigation.js index 52d5f150..91fca2da 100644 --- a/src/client/state/navigation.js +++ b/src/client/state/navigation.js @@ -137,6 +137,12 @@ class Navigation extends EventEmitter { 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,