From 997e4c9b1738e4bec12b391391a8f16d1d7fa0fb Mon Sep 17 00:00:00 2001 From: ph1p Date: Fri, 1 May 2020 16:04:29 +0200 Subject: [PATCH] feat(mobx): complete store rewrite and better socket handling --- package-lock.json | 31 +++-- package.json | 3 +- src/Ui.tsx | 171 ++++++++++++++---------- src/code.ts | 12 -- src/components/Chatbar.tsx | 27 ++-- src/components/ColorPicker.tsx | 13 +- src/components/Message.tsx | 3 +- src/components/Notification.tsx | 2 +- src/components/Notifications.tsx | 16 ++- src/shared/state.ts | 160 ----------------------- src/shared/utils.ts | 4 +- src/store/index.tsx | 218 +++++++++++++++++++++++++++++++ src/views/Chat.tsx | 144 ++++++++++++-------- src/views/Minimized.tsx | 74 ++++++----- src/views/Settings.tsx | 50 ++++--- src/views/UserList.tsx | 11 +- tsconfig.json | 1 + 17 files changed, 543 insertions(+), 397 deletions(-) delete mode 100644 src/shared/state.ts create mode 100644 src/store/index.tsx diff --git a/package-lock.json b/package-lock.json index ffb048e..da6a36f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -169,11 +169,6 @@ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, - "@nx-js/observer-util": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@nx-js/observer-util/-/observer-util-4.2.2.tgz", - "integrity": "sha512-9OayX1xkdGjdnsDiO2YdaYJ6aMyCF7/NY4QWVgIgjSAZJ4OX2fD766Ts79hEzBscenQy2DCaSoY8VkguIMB1ZA==" - }, "@types/prop-types": { "version": "15.7.3", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", @@ -4122,6 +4117,24 @@ "minimist": "^1.2.5" } }, + "mobx": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-5.15.4.tgz", + "integrity": "sha512-xRFJxSU2Im3nrGCdjSuOTFmxVDGeqOHL+TyADCGbT0k4HHqGmx5u2yaHNryvoORpI4DfbzjJ5jPmuv+d7sioFw==" + }, + "mobx-react": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-6.2.2.tgz", + "integrity": "sha512-Us6V4ng/iKIRJ8pWxdbdysC6bnS53ZKLKlVGBqzHx6J+gYPYbOotWvhHZnzh/W5mhpYXxlXif4kL2cxoWJOplQ==", + "requires": { + "mobx-react-lite": "2" + } + }, + "mobx-react-lite": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-2.0.6.tgz", + "integrity": "sha512-h/5GqxNIoSqnjt7SHxVtU7i1Kg0Xoxj853amzmzLgLRZKK9WwPc9tMuawW79ftmFSQhML0Zwt8kEuG1DIjQNBA==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -4874,14 +4887,6 @@ "scheduler": "^0.19.1" } }, - "react-easy-state": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/react-easy-state/-/react-easy-state-6.1.3.tgz", - "integrity": "sha512-uWQ7ittvJylwn/Xgz7Ub1jjsbpthQ9Ad1KDLxXfbXCb2OPnov4porVdnOJU2PKeRezcam3ZgfPUtf9L9rjtyWg==", - "requires": { - "@nx-js/observer-util": "^4.2.2" - } - }, "react-is": { "version": "16.12.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", diff --git a/package.json b/package.json index 8830fe5..d618f1e 100644 --- a/package.json +++ b/package.json @@ -57,10 +57,11 @@ }, "dependencies": { "@types/react-transition-group": "^4.2.4", + "mobx": "^5.15.4", + "mobx-react": "^6.2.2", "prop-types": "^15.7.2", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-easy-state": "^6.1.3", "react-router-dom": "^5.1.2", "react-timeago": "^4.4.0", "react-transition-group": "^4.3.0", diff --git a/src/Ui.tsx b/src/Ui.tsx index cba4fda..4211780 100644 --- a/src/Ui.tsx +++ b/src/Ui.tsx @@ -1,4 +1,6 @@ -import React from 'react'; +import 'mobx-react-lite/batchingForReactDom'; +import React, { useEffect, useState } from 'react'; +import { observer } from 'mobx-react'; import * as ReactDOM from 'react-dom'; import styled, { createGlobalStyle } from 'styled-components'; import { @@ -17,13 +19,15 @@ import Notifications from './components/Notifications'; import { DEFAULT_SERVER_URL } from './shared/constants'; import { ConnectionEnum } from './shared/interfaces'; import { SocketProvider } from './shared/SocketProvider'; -import { state, view } from './shared/state'; import { sendMainMessage } from './shared/utils'; // views import ChatView from './views/Chat'; import MinimizedView from './views/Minimized'; import UserListView from './views/UserList'; +import { StoreProvider, useStore } from './store'; +import { reaction } from 'mobx'; + onmessage = (message) => { if (message.data.pluginMessage) { const { type, payload } = message.data.pluginMessage; @@ -52,77 +56,105 @@ const AppWrapper = styled.div` overflow: hidden; `; -let socket: SocketIOClient.Socket; - -const initSocketConnection = function (url) { - state.status = ConnectionEnum.NONE; - state.settings.url = url; - - if (socket) { - socket.removeAllListeners(); - socket.disconnect(); - } - - socket = io(url, { - reconnectionAttempts: 3, - forceNew: true, - transports: ['websocket'], - }); - - socket.on('connected', () => { - state.status = ConnectionEnum.CONNECTED; - - socket.emit('set user', state.settings); - socket.emit('join room', { - room: state.roomName, - settings: state.settings, - }); - - sendMainMessage('ask-for-relaunch-message'); - }); - - socket.on('connect_error', () => { - state.status = ConnectionEnum.ERROR; - }); - - socket.on('reconnect_error', () => { - state.status = ConnectionEnum.ERROR; - }); - - socket.on('chat message', (data) => { - state.addMessage(data); - }); +const init = (serverUrl) => { + const App = observer(() => { + const store = useStore(); + let [socket, setSocket] = useState(undefined); - socket.on('join leave message', (data) => { - const username = data.user.name || 'Anon'; - let message = 'joins the conversation'; + function onFocus() { + sendMainMessage('focus', false); - if (data.type === 'LEAVE') { - message = 'leaves the conversation'; + store.isFocused = false; + } + function onFocusOut() { + sendMainMessage('focus', false); + store.isFocused = false; } - state.addNotification(`${username} ${message}`); - }); - socket.on('online', (data) => (state.online = data)); + function initSocketConnection() { + const url = store.settings.url || serverUrl; + store.status = ConnectionEnum.NONE; - sendMainMessage('get-root-data'); -}; + if (socket) { + socket.removeAllListeners(); + socket.disconnect(); + } -const init = (serverUrl) => { - initSocketConnection(serverUrl); + setSocket( + io(url, { + reconnectionAttempts: 3, + forceNew: true, + transports: ['websocket'], + }) + ); - // check focus - window.addEventListener('focus', () => { - sendMainMessage('focus', true); - state.isFocused = true; - }); + sendMainMessage('get-root-data'); + } - window.addEventListener('blur', () => { - sendMainMessage('focus', false); - state.isFocused = false; - }); + useEffect(() => { + if (socket && store.status === ConnectionEnum.NONE) { + socket.on('connected', () => { + store.status = ConnectionEnum.CONNECTED; + + socket.emit('set user', store.settings); + socket.emit('join room', { + room: store.roomName, + settings: store.settings, + }); + + sendMainMessage('ask-for-relaunch-message'); + }); + + socket.on('connect_error', () => { + store.status = ConnectionEnum.ERROR; + }); + + socket.on('reconnect_error', () => { + store.status = ConnectionEnum.ERROR; + }); + + socket.on('chat message', (data) => { + store.addMessage(data); + }); + + socket.on('join leave message', (data) => { + const username = data.user.name || 'Anon'; + let message = 'joins the conversation'; + + if (data.type === 'LEAVE') { + message = 'leaves the conversation'; + } + store.addNotification(`${username} ${message}`); + }); + + socket.on('online', (data) => (store.online = data)); + } + + return () => { + if (socket) { + socket.removeAllListeners(); + socket.disconnect(); + } + }; + }, [socket]); + + useEffect(() => { + const serverUrlDisposer = reaction( + () => store.settings.url, + initSocketConnection + ); + + // check focus + window.addEventListener('focus', onFocus); + window.addEventListener('blur', onFocusOut); + + return () => { + window.removeEventListener('focus', onFocus); + window.removeEventListener('blur', onFocusOut); + serverUrlDisposer(); + }; + }, []); - const App = view(() => { return ( @@ -130,7 +162,7 @@ const init = (serverUrl) => { - {state.isMinimized && } + {store.isMinimized && } @@ -139,7 +171,7 @@ const init = (serverUrl) => { - + @@ -148,5 +180,10 @@ const init = (serverUrl) => { ); }); - ReactDOM.render(, document.getElementById('app')); + ReactDOM.render( + + + , + document.getElementById('app') + ); }; diff --git a/src/code.ts b/src/code.ts index 5b7d45d..9c1ee9c 100644 --- a/src/code.ts +++ b/src/code.ts @@ -100,18 +100,6 @@ const isValidShape = (node) => node.type === 'INSTANCE' || node.type === 'POLYGON'; -function iterateOverFile(node, cb) { - if ('children' in node) { - if (node.type !== 'INSTANCE') { - cb(node); - for (const child of node.children) { - cb(child); - iterateOverFile(child, cb); - } - } - } -} - let previousSelection = figma.currentPage.selection || []; main().then(({ roomName, secret, history, instanceId }) => { diff --git a/src/components/Chatbar.tsx b/src/components/Chatbar.tsx index 5e9b8c5..ba144ff 100644 --- a/src/components/Chatbar.tsx +++ b/src/components/Chatbar.tsx @@ -1,10 +1,10 @@ import React, { FunctionComponent, useState, useEffect, useRef } from 'react'; import { useRouteMatch } from 'react-router-dom'; import styled from 'styled-components'; -import { state, view } from '../shared/state'; -import { sendMainMessage } from '../shared/utils'; import ColorPicker from './ColorPicker'; -import { ConnectionEnum } from '../shared/interfaces'; +import { ConnectionEnum } from '../shared/interfaces'; // store +import { observer } from 'mobx-react'; +import { useStore } from '../store'; interface ChatProps { sendMessage: (event: any) => void; @@ -12,18 +12,18 @@ interface ChatProps { textMessage: string; setSelectionIsChecked: (event: any) => void; selectionIsChecked: boolean; - init?: (url: string) => void; } const ChatBar: FunctionComponent = (props) => { + const store = useStore(); const isSettings = useRouteMatch('/settings'); - const selection = state.selection.length; + const selection = store.selection.length; const hasSelection = Boolean(selection); const [show, setShow] = useState(hasSelection); const chatTextInput = useRef(null); - const isFailed = state.status === ConnectionEnum.ERROR; - const isConnected = state.status === ConnectionEnum.CONNECTED; + const isFailed = store.status === ConnectionEnum.ERROR; + const isConnected = store.status === ConnectionEnum.CONNECTED; useEffect(() => { if (hasSelection) { @@ -48,8 +48,7 @@ const ChatBar: FunctionComponent = (props) => { {isFailed ? ( <> - connection failed{' '} - props.init(state.url)}>retry + connection failed 🙈 ) : ( 'connecting...' @@ -60,7 +59,7 @@ const ChatBar: FunctionComponent = (props) => { - (state.settings.enableNotificationSound = !state.settings + (store.settings.enableNotificationSound = !store.settings .enableNotificationSound) } > @@ -69,16 +68,16 @@ const ChatBar: FunctionComponent = (props) => { fill="none" viewBox="0 0 15 16" > - {state.settings.enableNotificationSound ? ( + {store.settings.enableNotificationSound ? ( ) : ( = view((props) => { +const ColorPicker: FunctionComponent = observer((props) => { + const store = useStore(); const [isOpen, setIsOpen] = useState(false); const wrapperRef = useRef(null); @@ -27,7 +30,7 @@ const ColorPicker: FunctionComponent = view((props) => { return ( setIsOpen(!isOpen)} /> @@ -37,14 +40,14 @@ const ColorPicker: FunctionComponent = view((props) => { key={color} onClick={() => { setIsOpen(false); - state.persistSettings( + store.persistSettings( { color, }, props.socket ); }} - className={`color ${state.settings.color === color && ' active'}`} + className={`color ${store.settings.color === color && ' active'}`} style={{ backgroundColor: color }} /> ))} diff --git a/src/components/Message.tsx b/src/components/Message.tsx index 0a0d215..66addc3 100644 --- a/src/components/Message.tsx +++ b/src/components/Message.tsx @@ -5,6 +5,7 @@ import { sendMainMessage } from '../shared/utils'; import TimeAgo from 'react-timeago'; import nowStrings from 'react-timeago/lib/language-strings/en-short'; import buildFormatter from 'react-timeago/lib/formatters/buildFormatter'; +import { toJS } from 'mobx'; const formatter = buildFormatter(nowStrings); @@ -36,7 +37,7 @@ const Message: FunctionComponent = ({ data, instanceId }) => { sendMainMessage('focus-nodes', { - ids: selection, + ids: toJS(selection), }) } > diff --git a/src/components/Notification.tsx b/src/components/Notification.tsx index 9ca76ab..6bc6036 100644 --- a/src/components/Notification.tsx +++ b/src/components/Notification.tsx @@ -21,7 +21,7 @@ const Notification: FunctionComponent< if (props.type === 'success') { typeClass = 'success'; } else if (props.type === 'error') { - typeClass = 'visual-bell--error'; + typeClass = 'error'; } return ( diff --git a/src/components/Notifications.tsx b/src/components/Notifications.tsx index 1cb3c61..a8e0c06 100644 --- a/src/components/Notifications.tsx +++ b/src/components/Notifications.tsx @@ -1,21 +1,25 @@ import React, { FunctionComponent } from 'react'; import styled from 'styled-components'; import { NotificationParams } from '../shared/interfaces'; -import { state, view } from '../shared/state'; import Notification from './Notification'; +// store +import { observer } from 'mobx-react'; +import { useStore } from '../store'; const Notifications: FunctionComponent = () => { + const store = useStore(); + const deleteNotification = (id: string) => - state.notifications.splice( - state.notifications.findIndex((n) => n.id === id), + store.notifications.splice( + store.notifications.findIndex((n) => n.id === id), 1 ); - if (state.notifications.length === 0) return null; + if (store.notifications.length === 0) return null; return ( - {state.notifications.map((data: NotificationParams, key) => ( + {store.notifications.map((data: NotificationParams, key) => ( +export const sendMainMessage = (action, payload = {}) => { parent.postMessage( { pluginMessage: { @@ -8,7 +8,7 @@ export const sendMainMessage = (action, payload = {}) => }, '*' ); - +} export const generateString = (length: number = 40): string => { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; diff --git a/src/store/index.tsx b/src/store/index.tsx new file mode 100644 index 0000000..eed9eea --- /dev/null +++ b/src/store/index.tsx @@ -0,0 +1,218 @@ +import { computed, observable, action, toJS } from 'mobx'; +import { createRef } from 'react'; + +import SimpleEncryptor from 'simple-encryptor'; +import { DEFAULT_SERVER_URL, colors } from '../shared/constants'; +import { sendMainMessage } from '../shared/utils'; +import { ConnectionEnum } from '../shared/interfaces'; + +import MessageSound from '../assets/sound.mp3'; +import React from 'react'; + +class RootStore { + @computed + get encryptor() { + return SimpleEncryptor(this.secret); + } + + @observable + status = ConnectionEnum.NONE; + // --- + @observable + secret = ''; + + @observable + roomName = ''; + + @observable + instanceId = ''; + // --- + @observable + online = []; + // --- + @observable + messages = []; + + @observable + messagesRef = createRef(); + + @observable + disableAutoScroll = false; + + @observable + selection = []; + + @action + scrollToBottom() { + if (!this.disableAutoScroll) { + const ref = toJS(this.messagesRef); + // scroll to bottom + if (ref?.current) { + ref.current.scrollTop = ref.current.scrollHeight; + } + } + } + + // --- + @observable + isFocused = true; + + @observable + isMinimized = false; + + @observable + settings = { + name: '', + color: colors['#4F4F4F'], + url: DEFAULT_SERVER_URL, + enableNotificationTooltip: true, + enableNotificationSound: true, + }; + + @observable + notifications = []; + + @action + addNotification(text: string, type?: string) { + this.notifications.push({ + id: Math.random(), + text, + type, + }); + } + // --- + @action + toggleMinimizeChat() { + this.isMinimized = !this.isMinimized; + sendMainMessage('minimize', this.isMinimized); + } + + @action + removeAllMessages() { + if ( + (window as any).confirm('Remove all messages? (This cannot be undone)') + ) { + sendMainMessage('remove-all-messages'); + (window as any).alert('Messages successfully removed'); + this.messages = []; + } + } + + @action + persistSettings(settings, socket, init?) { + const oldUrl = this.settings.url; + this.settings = { + ...this.settings, + ...settings, + }; + + // save user settings in main + sendMainMessage('save-user-settings', Object.assign({}, this.settings)); + + if (settings.url && settings.url !== oldUrl) { + // set server URL + sendMainMessage('set-server-url', settings.url); + + // re init main app and disconnect socket + // to prevent multiple sign in + if (init) { + if (socket && socket.connected) { + socket.disconnect(); + } + + init(settings.url); + + this.addNotification('Updated server-URL'); + } + } + + if (socket && socket.connected) { + // set user data on server + socket.emit('set user', this.settings); + } + } + + @action + addMessage(messageData) { + const isLocal = !messageData.user; + const decryptedMessage = this.encryptor.decrypt( + isLocal ? messageData : messageData.message + ); + + // silent on error + try { + const data = JSON.parse(decryptedMessage); + let newMessage: {} = { + message: { + ...data, + }, + }; + + // is local sender + if (isLocal) { + newMessage = { + id: this.instanceId, + user: { + color: this.settings.color, + name: this.settings.name, + }, + message: { + ...data, + }, + }; + + sendMainMessage('add-message-to-history', newMessage); + } else { + newMessage = { + id: messageData.id, + user: messageData.user, + message: { + ...data, + }, + }; + + if (this.settings.enableNotificationSound) { + const audio = new Audio(MessageSound); + audio.play(); + } + if (this.settings.enableNotificationTooltip) { + sendMainMessage( + 'notification', + messageData?.user?.name + ? `New chat message from ${messageData.user.name}` + : `New chat message` + ); + } + } + + this.messages.push(newMessage); + + setTimeout(() => this.scrollToBottom(), 0); + } catch (e) { + console.log(e); + } + } +} + +export function createStore() { + return new RootStore(); +} + +export type TStore = ReturnType; + +const StoreContext = React.createContext(null); + +export const StoreProvider = ({ children }) => { + const store = createStore(); + return ( + {children} + ); +}; + +export const useStore = () => { + const store = React.useContext(StoreContext); + if (!store) { + throw new Error('useStore must be used within a StoreProvider.'); + } + return store; +}; diff --git a/src/views/Chat.tsx b/src/views/Chat.tsx index 5c7411b..af59ce9 100644 --- a/src/views/Chat.tsx +++ b/src/views/Chat.tsx @@ -1,5 +1,9 @@ -import React, { FunctionComponent, useEffect, Fragment, useState } from 'react'; -import { store } from 'react-easy-state'; +import React, { + FunctionComponent, + useEffect, + useState, + createRef, +} from 'react'; import { useHistory, useRouteMatch } from 'react-router-dom'; import styled, { keyframes } from 'styled-components'; // components @@ -10,73 +14,76 @@ import Chatbar from '../components/Chatbar'; import { IS_PROD, MAX_MESSAGES } from '../shared/constants'; import { ConnectionEnum } from '../shared/interfaces'; import { withSocketContext } from '../shared/SocketProvider'; -import { state, view } from '../shared/state'; import { sendMainMessage } from '../shared/utils'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; +// store +import { observer, useLocalStore } from 'mobx-react'; +import { useStore } from '../store'; +import { toJS } from 'mobx'; interface ChatProps { socket: SocketIOClient.Socket; - init?: (url: string) => void; } const ChatView: FunctionComponent = (props) => { - const isConnected = state.status === ConnectionEnum.CONNECTED; + const store = useStore(); + const isConnected = store.status === ConnectionEnum.CONNECTED; const [animationEnabled, enableAnimation] = useState(false); const [containerIsHidden, setContainerIsHidden] = useState(false); const isSettings = useRouteMatch('/settings'); const history = useHistory(); - const chatState = store({ + const chatState = useLocalStore(() => ({ textMessage: '', selectionIsChecked: false, messagesToShow: MAX_MESSAGES, get hideMoreButton() { return ( - chatState.messagesToShow >= state.messages.length || - chatState.filteredMessages.length >= state.messages.length + chatState.messagesToShow >= store.messages.length || + chatState.filteredMessages.length >= store.messages.length ); }, get filteredMessages() { - return [...state.messages].slice(-chatState.messagesToShow); + return [...store.messages].slice(-chatState.messagesToShow); }, - }); + })); const sendMessage = (e = null) => { if (e) { e.preventDefault(); } - if (state.roomName) { + if (store.roomName) { let data = { text: chatState.textMessage, date: new Date(), }; - if (state.selection.length > 0) { + if (store.selection.length > 0) { if (chatState.selectionIsChecked) { data = { ...data, ...{ - selection: state.selection, + selection: store.selection, }, }; } } if (!chatState.textMessage && !chatState.selectionIsChecked) { - state.addNotification( + store.addNotification( 'Please enter a text or select something', 'error' ); } else { - const message = state.encryptor.encrypt(JSON.stringify(data)); + const message = store.encryptor.encrypt(JSON.stringify(data)); props.socket.emit('chat message', { - roomName: state.roomName, + roomName: store.roomName, message, }); - state.addMessage(message); + store.addMessage(message); chatState.textMessage = ''; chatState.selectionIsChecked = false; @@ -92,7 +99,7 @@ const ChatView: FunctionComponent = (props) => { // set selection if (pmessage.type === 'selection') { const hasSelection = pmessage.payload.length > 0; - state.selection = hasSelection ? pmessage.payload : []; + store.selection = hasSelection ? pmessage.payload : []; if (!hasSelection) { chatState.selectionIsChecked = false; @@ -117,20 +124,20 @@ const ChatView: FunctionComponent = (props) => { : {}), }; - state.secret = dataSecret; - state.roomName = dataRoomName; - state.messages = messages; - state.instanceId = instanceId; - state.selection = selection; + store.secret = dataSecret; + store.roomName = dataRoomName; + store.messages = messages; + store.instanceId = instanceId; + store.selection = selection; - state.persistSettings(settings, props.socket); + store.persistSettings(settings, props.socket); } if (pmessage.type === 'relaunch-message') { chatState.selectionIsChecked = true; - state.selection = pmessage.payload.selection || []; + store.selection = pmessage.payload.selection || []; - if (state.selection.length) { + if (store.selection.length) { sendMessage(); sendMainMessage('notify', 'Selection sent successfully'); } @@ -139,15 +146,15 @@ const ChatView: FunctionComponent = (props) => { }; useEffect(() => { - setTimeout(state.scrollToBottom, 100); + setTimeout(() => store.scrollToBottom(), 100); }, []); const showMore = () => { if ( chatState.filteredMessages.length + MAX_MESSAGES >= - state.messages.length + store.messages.length ) { - chatState.messagesToShow = state.messages.length; + chatState.messagesToShow = store.messages.length; } else { chatState.messagesToShow += MAX_MESSAGES; } @@ -156,15 +163,15 @@ const ChatView: FunctionComponent = (props) => { return ( <> 0} + color={store.settings.color} + hasSelection={store.selection.length > 0} > {isConnected && ( history.push('/user-list')}> - {state.online.length} + {store.online.length} - + store.toggleMinimizeChat()} /> )} @@ -181,11 +188,11 @@ const ChatView: FunctionComponent = (props) => { { - const { current } = state.messagesRef; + const { current } = toJS(store.messagesRef); - state.disableAutoScroll = + store.disableAutoScroll = current.scrollHeight - (current.scrollTop + current.clientHeight) > 0; @@ -196,10 +203,12 @@ const ChatView: FunctionComponent = (props) => { <> - + {(i + 1) % MAX_MESSAGES === 0 && i + 1 !== chatState.filteredMessages.length ? ( @@ -243,11 +252,10 @@ const ChatView: FunctionComponent = (props) => { {(isSettings || (!isSettings && !containerIsHidden)) && ( - + )} (chatState.textMessage = text)} textMessage={chatState.textMessage} @@ -361,23 +369,45 @@ const Messages = styled.div` } } .message { - &-enter { - opacity: 0; - transform: translateX(60px); - } - &-enter-active { - opacity: 1; - transition: opacity 200ms ease-in, transform 200ms ease-in; - transform: translateX(0px); - } - &-exit { - opacity: 1; - transform: translateX(0px); + &-self { + &-enter { + opacity: 0; + transform: translateX(60px); + } + &-enter-active { + opacity: 1; + transition: opacity 200ms ease-in, transform 200ms ease-in; + transform: translateX(0px); + } + &-exit { + opacity: 1; + transform: translateX(0px); + } + &-exit-active { + opacity: 0; + transition: opacity 200ms ease-in, transform 200ms ease-in; + transform: translateX(60px); + } } - &-exit-active { - opacity: 0; - transition: opacity 200ms ease-in, transform 200ms ease-in; - transform: translateX(60px); + &-other { + &-enter { + opacity: 0; + transform: translateX(-60px); + } + &-enter-active { + opacity: 1; + transition: opacity 200ms ease-in, transform 200ms ease-in; + transform: translateX(0px); + } + &-exit { + opacity: 1; + transform: translateX(0px); + } + &-exit-active { + opacity: 0; + transition: opacity 200ms ease-in, transform 200ms ease-in; + transform: translateX(-60px); + } } } `; @@ -420,4 +450,4 @@ const Minimize = styled.div` } `; -export default withSocketContext(view(ChatView)); +export default withSocketContext(observer(ChatView)); diff --git a/src/views/Minimized.tsx b/src/views/Minimized.tsx index b14a78b..901bc2d 100644 --- a/src/views/Minimized.tsx +++ b/src/views/Minimized.tsx @@ -3,39 +3,45 @@ import { Redirect } from 'react-router-dom'; import styled from 'styled-components'; import Header from '../components/Header'; import { ConnectionEnum } from '../shared/interfaces'; -import { state, view } from '../shared/state'; import { SharedIcon } from '../shared/style'; +// store +import { observer } from 'mobx-react'; +import { useStore } from '../store'; -const MinimizedView: FunctionComponent = () => ( - <> -
- {state.settings.name} - - } - left={} - right={ - -
- - } - /> - - {!state.isMinimized && } - {state.status === ConnectionEnum.ERROR && ( - - )} - - {state.online.map(user => ( - - {user.name.substr(0, 2)} - - ))} - - - -); +const MinimizedView: FunctionComponent = () => { + const store = useStore(); + + return ( + <> +
+ {store.settings.name} + + } + left={} + right={ + store.toggleMinimizeChat()}> +
+ + } + /> + + {!store.isMinimized && } + {store.status === ConnectionEnum.ERROR && ( + + )} + + {store.online.map((user) => ( + + {user.name.substr(0, 2)} + + ))} + + + + ); +}; const Minimized = styled.div` display: grid; @@ -53,7 +59,7 @@ const Minimized = styled.div` const Title = styled.div` margin-left: 10px; - color: ${props => props.color}; + color: ${(props) => props.color}; `; const Users = styled.div` @@ -72,7 +78,7 @@ const User = styled.div` text-align: center; border-radius: 100%; color: #fff; - background-color: ${props => props.color}; + background-color: ${(props) => props.color}; `; -export default view(MinimizedView); +export default observer(MinimizedView); diff --git a/src/views/Settings.tsx b/src/views/Settings.tsx index 01fad00..e445a34 100644 --- a/src/views/Settings.tsx +++ b/src/views/Settings.tsx @@ -1,19 +1,18 @@ import React, { FunctionComponent, useEffect, useState } from 'react'; -import { store } from 'react-easy-state'; import { useHistory } from 'react-router-dom'; import styled from 'styled-components'; import { version, repository } from '../../package.json'; // shared -import { ConnectionEnum } from '../shared/interfaces'; import { withSocketContext } from '../shared/SocketProvider'; -import { state, view } from '../shared/state'; import { DEFAULT_SERVER_URL } from '../shared/constants'; // components import Checkbox from '../components/Checkbox'; +// store +import { observer, useLocalStore } from 'mobx-react'; +import { useStore } from '../store'; interface SettingsProps { socket: SocketIOClient.Socket; - init?: (serverUrl: any) => void; } const Flag = (props) => { @@ -32,20 +31,22 @@ const Flag = (props) => { }; const SettingsView: FunctionComponent = (props) => { + const store = useStore(); + const [flags, setFlag] = useState({ username: false, }); const history = useHistory(); - const settings = store({ + const settings = useLocalStore(() => ({ name: '', url: '', enableNotificationTooltip: true, - }); + })); useEffect(() => { - if (state.isMinimized) { - state.toggleMinimizeChat(); + if (store.isMinimized) { + store.toggleMinimizeChat(); } return () => @@ -55,21 +56,21 @@ const SettingsView: FunctionComponent = (props) => { }, []); useEffect(() => { - settings.name = state.settings.name; - settings.url = state.settings.url; + settings.name = store.settings.name; + settings.url = store.settings.url; settings.enableNotificationTooltip = - state.settings.enableNotificationTooltip; - }, [state.settings]); + store.settings.enableNotificationTooltip; + }, [store.settings]); const saveSettings = (shouldClose: boolean = true) => { - if (state.settings.name !== settings.name) { + if (store.settings.name !== settings.name) { setFlag({ ...flags, username: true, }); } - state.persistSettings(settings, props.socket, props.init); + store.persistSettings(settings, props.socket); if (shouldClose) { history.push('/'); @@ -86,10 +87,11 @@ const SettingsView: FunctionComponent = (props) => { saveSettings(false)} + onBlur={() => saveSettings()} onChange={({ target }: any) => (settings.name = target.value.substr(0, 20)) } + onKeyDown={(e: any) => e.keyCode == 13 && e.target.blur()} placeholder="Username ..." />
@@ -101,14 +103,19 @@ const SettingsView: FunctionComponent = (props) => { checked={settings.enableNotificationTooltip} onChange={() => { settings.enableNotificationTooltip = !settings.enableNotificationTooltip; - saveSettings(false); + saveSettings(); }} />

Server URL - (settings.url = DEFAULT_SERVER_URL)}> + { + settings.url = DEFAULT_SERVER_URL; + saveSettings(settings.url !== store.settings.url); + }} + > reset

@@ -116,10 +123,13 @@ const SettingsView: FunctionComponent = (props) => { saveSettings()} + onBlur={({ target }: any) => + saveSettings(target.value !== store.settings.url) + } onChange={({ target }: any) => (settings.url = target.value.substr(0, 255)) } + onKeyDown={(e: any) => e.keyCode == 13 && e.target.blur()} placeholder="Server-URL ..." />
@@ -127,7 +137,7 @@ const SettingsView: FunctionComponent = (props) => {