diff --git a/web/settings/account-settings.css b/web/settings/account-settings.css index 78ce600d3..e45af3dde 100644 --- a/web/settings/account-settings.css +++ b/web/settings/account-settings.css @@ -1,80 +1,81 @@ .container { flex: 1; background-color: var(--panel-background-primary-default); + overflow: auto; } .contentContainer { padding: 40px; width: 456px; overflow-y: auto; } .header { color: var(--text-background-primary-default); font-weight: var(--semi-bold); line-height: var(--line-height-display); padding-bottom: 55px; } .content { margin-top: 32px; } .content ul { list-style-type: none; } .content li { color: var(--text-background-tertiary-default); padding: 24px 16px 16px; display: flex; flex-direction: row; justify-content: space-between; } .content li:not(:last-child) { border-bottom: 1px solid var(--separator-background-primary-default); } .logoutContainer { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .logoutLabel { color: var(--text-background-tertiary-default); } .username { color: var(--text-background-primary-default); } .buttonText { color: var(--link-background-primary-default); line-height: var(--line-height-text); } .passwordContainer { display: flex; } .password { align-items: center; padding-right: 16px; } .editPasswordLink { color: var(--text-background-tertiary-default); cursor: pointer; } .preferencesContainer { padding-top: 24px; } .preferencesHeader { color: var(--text-background-primary-default); font-weight: var(--semi-bold); line-height: var(--line-height-display); } diff --git a/web/settings/account-settings.react.js b/web/settings/account-settings.react.js index 68191947f..ca4fd3e54 100644 --- a/web/settings/account-settings.react.js +++ b/web/settings/account-settings.react.js @@ -1,144 +1,197 @@ // @flow import * as React from 'react'; import { logOut, logOutActionTypes } from 'lib/actions/user-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { useStringForUser } from 'lib/hooks/ens-cache.js'; import { preRequestUserStateSelector } from 'lib/selectors/account-selectors.js'; import { accountHasPassword } from 'lib/shared/account-utils.js'; +import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.js'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils.js'; import css from './account-settings.css'; import AppearanceChangeModal from './appearance-change-modal.react.js'; import PasswordChangeModal from './password-change-modal.js'; import BlockListModal from './relationship/block-list-modal.react.js'; import FriendListModal from './relationship/friend-list-modal.react.js'; +import TunnelbrokerMessagesScreen from './tunnelbroker-message-list.react.js'; +import TunnelbrokerTestScreen from './tunnelbroker-test.react.js'; import EditUserAvatar from '../avatars/edit-user-avatar.react.js'; import Button from '../components/button.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; function AccountSettings(): React.Node { const sendLogoutRequest = useServerCall(logOut); const preRequestUserState = useSelector(preRequestUserStateSelector); const dispatchActionPromise = useDispatchActionPromise(); const logOutUser = React.useCallback( () => dispatchActionPromise( logOutActionTypes, sendLogoutRequest(preRequestUserState), ), [dispatchActionPromise, preRequestUserState, sendLogoutRequest], ); const { pushModal, popModal } = useModalContext(); const showPasswordChangeModal = React.useCallback( () => pushModal(), [pushModal], ); const openFriendList = React.useCallback( () => pushModal(), [popModal, pushModal], ); const openBlockList = React.useCallback( () => pushModal(), [popModal, pushModal], ); const isAccountWithPassword = useSelector(state => accountHasPassword(state.currentUserInfo), ); const currentUserInfo = useSelector(state => state.currentUserInfo); const stringForUser = useStringForUser(currentUserInfo); const staffCanSee = useStaffCanSee(); + const { sendMessage, connected, addListener, removeListener } = + useTunnelbroker(); + const openTunnelbrokerModal = React.useCallback( + () => + pushModal( + , + ), + [popModal, pushModal, sendMessage], + ); + + const openTunnelbrokerMessagesModal = React.useCallback( + () => + pushModal( + , + ), + [addListener, popModal, pushModal, removeListener], + ); const showAppearanceModal = React.useCallback( () => pushModal(), [pushModal], ); if (!currentUserInfo || currentUserInfo.anonymous) { return null; } let changePasswordSection; if (isAccountWithPassword) { changePasswordSection = (
  • Password ******
  • ); } let preferences; if (staffCanSee) { preferences = (

    Preferences

    • Appearance
    ); } + let tunnelbroker; + if (staffCanSee) { + tunnelbroker = ( +
    +

    Tunnerlbroker menu

    +
    +
      +
    • + Connected + {connected.toString()} +
    • +
    • + Send message to device + +
    • +
    • + Trace received messages + +
    • +
    +
    +
    + ); + } return (

    My Account

    • {'Logged in as '} {stringForUser}

    • {changePasswordSection}
    • Friend List
    • Block List
    {preferences} + {tunnelbroker}
    ); } export default AccountSettings; diff --git a/web/settings/tunnelbroker-message-list.css b/web/settings/tunnelbroker-message-list.css new file mode 100644 index 000000000..9b409c2d4 --- /dev/null +++ b/web/settings/tunnelbroker-message-list.css @@ -0,0 +1,21 @@ +.messageList { + display: flex; + flex-direction: column; + color: #fff; + height: 100%; + width: 100%; + overflow-y: auto; +} + +.messageRow { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 10px; + border-bottom: 1px solid #ddd; +} + +.messageCol { + flex: 1; + padding: 15px; +} diff --git a/web/settings/tunnelbroker-message-list.react.js b/web/settings/tunnelbroker-message-list.react.js new file mode 100644 index 000000000..916f98d4f --- /dev/null +++ b/web/settings/tunnelbroker-message-list.react.js @@ -0,0 +1,50 @@ +// @flow + +import * as React from 'react'; + +import type { TunnelbrokerSocketListener } from 'lib/tunnelbroker/tunnelbroker-context.js'; +import type { TunnelbrokerMessage } from 'lib/types/tunnelbroker/messages.js'; + +import css from './tunnelbroker-message-list.css'; +import Modal from '../modals/modal.react.js'; + +type Props = { + +addListener: (listener: TunnelbrokerSocketListener) => void, + +removeListener: (listener: TunnelbrokerSocketListener) => void, + +onClose: () => void, +}; + +function TunnelbrokerMessagesScreen(props: Props): React.Node { + const { addListener, onClose, removeListener } = props; + const [messages, setMessages] = React.useState([]); + + const listener = React.useCallback((msg: TunnelbrokerMessage) => { + setMessages(prev => [...prev, msg]); + }, []); + + React.useEffect(() => { + addListener(listener); + return () => removeListener(listener); + }, [addListener, listener, removeListener]); + + let messageList = ( +
    +
    No messages
    +
    + ); + if (messages.length) { + messageList = messages.map(message => ( +
    +
    {JSON.stringify(message)}
    +
    + )); + } + + return ( + +
    {messageList}
    +
    + ); +} + +export default TunnelbrokerMessagesScreen; diff --git a/web/settings/tunnelbroker-test.css b/web/settings/tunnelbroker-test.css new file mode 100644 index 000000000..bfb878280 --- /dev/null +++ b/web/settings/tunnelbroker-test.css @@ -0,0 +1,25 @@ +.modalBody { + padding: 24px 40px 32px; + color: var(--fg); +} + +.content { + display: flex; + flex-direction: column; + gap: 10px; +} + +.footer { + display: flex; + flex-direction: row-reverse; + justify-content: space-between; + padding-top: 8px; +} + +.modalError { + font-size: var(--xs-font-12); + color: var(--error); + font-style: italic; + padding-left: 6px; + align-self: center; +} diff --git a/web/settings/tunnelbroker-test.react.js b/web/settings/tunnelbroker-test.react.js new file mode 100644 index 000000000..47a3649ca --- /dev/null +++ b/web/settings/tunnelbroker-test.react.js @@ -0,0 +1,94 @@ +// @flow + +import invariant from 'invariant'; +import * as React from 'react'; + +import { type ClientMessageToDevice } from 'lib/tunnelbroker/tunnelbroker-context.js'; + +import css from './tunnelbroker-test.css'; +import Button from '../components/button.react.js'; +import Input from '../modals/input.react.js'; +import Modal from '../modals/modal.react.js'; + +type Props = { + +sendMessage: (message: ClientMessageToDevice) => Promise, + +onClose: () => void, +}; + +function TunnelbrokerTestScreen(props: Props): React.Node { + const { sendMessage, onClose } = props; + const [recipient, setRecipient] = React.useState(''); + const [message, setMessage] = React.useState(''); + const [loading, setLoading] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState(''); + const recipientInput = React.useRef(null); + const messageInput = React.useRef(null); + + const onSubmit = React.useCallback( + async event => { + event.preventDefault(); + + setLoading(true); + try { + await sendMessage({ deviceID: recipient, payload: message }); + } catch (e) { + setErrorMessage(e.message); + } + setLoading(false); + }, + [message, recipient, sendMessage], + ); + + let errorMsg; + if (errorMessage) { + errorMsg =
    {errorMessage}
    ; + } + + return ( + +
    +
    + ) => { + const target = event.target; + invariant(target instanceof HTMLInputElement, 'target not input'); + setRecipient(target.value); + }} + disabled={loading} + ref={recipientInput} + label="Recipient" + /> + ) => { + const target = event.target; + invariant(target instanceof HTMLInputElement, 'target not input'); + setMessage(target.value); + }} + disabled={loading} + ref={messageInput} + label="Message" + /> +
    +
    + + {errorMsg} +
    +
    +
    + ); +} + +export default TunnelbrokerTestScreen;