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 = (
);
}
+ let tunnelbroker;
+ if (staffCanSee) {
+ tunnelbroker = (
+
+
Tunnerlbroker menu
+
+
+ -
+ Connected
+ {connected.toString()}
+
+ -
+ Send message to device
+
+
+ -
+ Trace received messages
+
+
+
+
+
+ );
+ }
return (
My Account
{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 = (
+
+ );
+ 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 (
+
+
+
+ );
+}
+
+export default TunnelbrokerTestScreen;