Convert withModal to useModal
This commit is contained in:
parent
9cf42df1ea
commit
530e08b9ee
32 changed files with 791 additions and 737 deletions
|
@ -1,4 +1,4 @@
|
|||
import React, { Suspense, lazy, useState } from 'react';
|
||||
import React, { Suspense, lazy, useState, useEffect } from 'react';
|
||||
import Route from 'containers/Route';
|
||||
import AppInfo from 'components/AppInfo';
|
||||
import TabList from 'components/TabList';
|
||||
|
@ -28,6 +28,11 @@ const App = ({
|
|||
setRenderModals(true);
|
||||
}
|
||||
|
||||
const [starting, setStarting] = useState(true);
|
||||
useEffect(() => {
|
||||
setTimeout(() => setStarting(false), 1000);
|
||||
}, []);
|
||||
|
||||
const mainClass = cn('main-container', {
|
||||
'off-canvas': showTabList
|
||||
});
|
||||
|
@ -40,7 +45,7 @@ const App = ({
|
|||
|
||||
return (
|
||||
<div className="wrap" onClick={handleClick}>
|
||||
{!connected && (
|
||||
{!starting && !connected && (
|
||||
<AppInfo type="error">
|
||||
Connection lost, attempting to reconnect...
|
||||
</AppInfo>
|
||||
|
|
|
@ -61,7 +61,7 @@ export default class TabList extends PureComponent {
|
|||
<div
|
||||
key={`${address}-chans}`}
|
||||
className="tab-label"
|
||||
onClick={() => openModal('channel', { server: address })}
|
||||
onClick={() => openModal('channel', address)}
|
||||
>
|
||||
<span>CHANNELS {chanLabel}</span>
|
||||
<Button title="Join Channel">+</Button>
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
import React, { memo, useState, useEffect, useCallback, useRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import React, { memo, useState, useEffect, useRef } from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { FiUsers, FiX } from 'react-icons/fi';
|
||||
import withModal from 'components/modals/withModal';
|
||||
import useModal from 'components/modals/useModal';
|
||||
import Button from 'components/ui/Button';
|
||||
import { join } from 'state/channels';
|
||||
import { select } from 'state/tab';
|
||||
import { searchChannels } from 'state/channelSearch';
|
||||
import { linkify } from 'utils';
|
||||
|
||||
const Channel = memo(({ server, name, topic, userCount, joined, ...props }) => {
|
||||
const handleJoinClick = useCallback(() => props.join([name], server), []);
|
||||
const Channel = memo(({ server, name, topic, userCount, joined }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = () => dispatch(join([name], server));
|
||||
|
||||
return (
|
||||
<div className="modal-channel-result">
|
||||
<div className="modal-channel-result-header">
|
||||
<h2 className="modal-channel-name" onClick={handleJoinClick}>
|
||||
<h2 className="modal-channel-name" onClick={handleClick}>
|
||||
{name}
|
||||
</h2>
|
||||
<FiUsers />
|
||||
|
@ -25,7 +28,7 @@ const Channel = memo(({ server, name, topic, userCount, joined, ...props }) => {
|
|||
<Button
|
||||
className="modal-channel-button-join"
|
||||
category="normal"
|
||||
onClick={handleJoinClick}
|
||||
onClick={handleClick}
|
||||
>
|
||||
Join
|
||||
</Button>
|
||||
|
@ -36,7 +39,12 @@ const Channel = memo(({ server, name, topic, userCount, joined, ...props }) => {
|
|||
);
|
||||
});
|
||||
|
||||
const AddChannel = ({ search, payload: { server }, onClose, ...props }) => {
|
||||
const AddChannel = () => {
|
||||
const [modal, server, closeModal] = useModal('channel');
|
||||
|
||||
const channels = useSelector(state => state.channels);
|
||||
const search = useSelector(state => state.channelSearch);
|
||||
const dispatch = useDispatch();
|
||||
const [q, setQ] = useState('');
|
||||
|
||||
const inputEl = useRef();
|
||||
|
@ -44,52 +52,51 @@ const AddChannel = ({ search, payload: { server }, onClose, ...props }) => {
|
|||
const prevSearch = useRef('');
|
||||
|
||||
useEffect(() => {
|
||||
inputEl.current.focus();
|
||||
props.searchChannels(server, '');
|
||||
}, []);
|
||||
if (modal.isOpen) {
|
||||
dispatch(searchChannels(server, ''));
|
||||
setTimeout(() => inputEl.current.focus(), 0);
|
||||
} else {
|
||||
setQ('');
|
||||
}
|
||||
}, [modal.isOpen]);
|
||||
|
||||
const handleSearch = useCallback(
|
||||
e => {
|
||||
let nextQ = e.target.value.trim().toLowerCase();
|
||||
setQ(nextQ);
|
||||
const handleSearch = e => {
|
||||
let nextQ = e.target.value.trim().toLowerCase();
|
||||
setQ(nextQ);
|
||||
|
||||
if (nextQ !== q) {
|
||||
resultsEl.current.scrollTop = 0;
|
||||
if (nextQ !== q) {
|
||||
resultsEl.current.scrollTop = 0;
|
||||
|
||||
while (nextQ.charAt(0) === '#') {
|
||||
nextQ = nextQ.slice(1);
|
||||
}
|
||||
|
||||
if (nextQ !== prevSearch.current) {
|
||||
prevSearch.current = nextQ;
|
||||
props.searchChannels(server, nextQ);
|
||||
}
|
||||
while (nextQ.charAt(0) === '#') {
|
||||
nextQ = nextQ.slice(1);
|
||||
}
|
||||
},
|
||||
[q]
|
||||
);
|
||||
|
||||
const handleKey = useCallback(e => {
|
||||
if (nextQ !== prevSearch.current) {
|
||||
prevSearch.current = nextQ;
|
||||
dispatch(searchChannels(server, nextQ));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleKey = e => {
|
||||
if (e.key === 'Enter') {
|
||||
let channel = e.target.value.trim();
|
||||
|
||||
if (channel !== '') {
|
||||
onClose(false);
|
||||
closeModal(false);
|
||||
|
||||
if (channel.charAt(0) !== '#') {
|
||||
channel = `#${channel}`;
|
||||
}
|
||||
|
||||
props.join([channel], server);
|
||||
props.select(server, channel);
|
||||
dispatch(join([channel], server));
|
||||
dispatch(select(server, channel));
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
};
|
||||
|
||||
const handleLoadMore = useCallback(
|
||||
() => props.searchChannels(server, q, search.results.length),
|
||||
[q, search.results.length]
|
||||
);
|
||||
const handleLoadMore = () =>
|
||||
dispatch(searchChannels(server, q, search.results.length));
|
||||
|
||||
let hasMore = !search.end;
|
||||
if (hasMore) {
|
||||
|
@ -104,7 +111,7 @@ const AddChannel = ({ search, payload: { server }, onClose, ...props }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal {...modal}>
|
||||
<div className="modal-channel-input-wrap">
|
||||
<input
|
||||
ref={inputEl}
|
||||
|
@ -117,7 +124,7 @@ const AddChannel = ({ search, payload: { server }, onClose, ...props }) => {
|
|||
<Button
|
||||
icon={FiX}
|
||||
className="modal-close modal-channel-close"
|
||||
onClick={onClose}
|
||||
onClick={closeModal}
|
||||
/>
|
||||
</div>
|
||||
<div ref={resultsEl} className="modal-channel-results">
|
||||
|
@ -125,12 +132,7 @@ const AddChannel = ({ search, payload: { server }, onClose, ...props }) => {
|
|||
<Channel
|
||||
key={`${server} ${channel.name}`}
|
||||
server={server}
|
||||
join={props.join}
|
||||
joined={get(
|
||||
props.channels,
|
||||
[server, channel.name, 'joined'],
|
||||
false
|
||||
)}
|
||||
joined={channels[server]?.[channel.name]?.joined}
|
||||
{...channel}
|
||||
/>
|
||||
))}
|
||||
|
@ -143,15 +145,8 @@ const AddChannel = ({ search, payload: { server }, onClose, ...props }) => {
|
|||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default withModal({
|
||||
name: 'channel',
|
||||
state: {
|
||||
channels: state => state.channels,
|
||||
search: state => state.channelSearch
|
||||
},
|
||||
actions: { searchChannels, join, select }
|
||||
})(AddChannel);
|
||||
export default AddChannel;
|
||||
|
|
|
@ -1,27 +1,26 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import withModal from 'components/modals/withModal';
|
||||
import React from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import useModal from 'components/modals/useModal';
|
||||
import Button from 'components/ui/Button';
|
||||
|
||||
const Confirm = ({
|
||||
payload: { question, confirmation, onConfirm },
|
||||
onClose
|
||||
}) => {
|
||||
const handleConfirm = useCallback(() => {
|
||||
onClose(false);
|
||||
const Confirm = () => {
|
||||
const [modal, payload, closeModal] = useModal('confirm');
|
||||
const { question, confirmation, onConfirm } = payload;
|
||||
|
||||
const handleConfirm = () => {
|
||||
closeModal(false);
|
||||
onConfirm();
|
||||
}, []);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal {...modal}>
|
||||
<p>{question}</p>
|
||||
<Button onClick={handleConfirm}>{confirmation || 'OK'}</Button>
|
||||
<Button category="normal" onClick={onClose}>
|
||||
<Button category="normal" onClick={closeModal}>
|
||||
Cancel
|
||||
</Button>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default withModal({
|
||||
name: 'confirm'
|
||||
})(Confirm);
|
||||
export default Confirm;
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
import React from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FiX } from 'react-icons/fi';
|
||||
import Button from 'components/ui/Button';
|
||||
import withModal from 'components/modals/withModal';
|
||||
import useModal from 'components/modals/useModal';
|
||||
import { getSelectedChannel } from 'state/channels';
|
||||
import { linkify } from 'utils';
|
||||
|
||||
const Topic = ({ payload: { topic, channel }, onClose }) => {
|
||||
const Topic = () => {
|
||||
const [modal, channel, closeModal] = useModal('topic');
|
||||
|
||||
const topic = useSelector(state => getSelectedChannel(state)?.topic);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal {...modal}>
|
||||
<div className="modal-header">
|
||||
<h2>Topic in {channel}</h2>
|
||||
<Button icon={FiX} className="modal-close" onClick={onClose} />
|
||||
<Button icon={FiX} className="modal-close" onClick={closeModal} />
|
||||
</div>
|
||||
<p className="modal-content">{linkify(topic)}</p>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default withModal({
|
||||
name: 'topic'
|
||||
})(Topic);
|
||||
export default Topic;
|
||||
|
|
46
client/js/components/modals/useModal.js
Normal file
46
client/js/components/modals/useModal.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { useCallback } from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { closeModal } from 'state/modals';
|
||||
|
||||
Modal.setAppElement('#root');
|
||||
|
||||
const defaultPayload = {};
|
||||
|
||||
export default function useModal(name) {
|
||||
const isOpen = useSelector(state => state.modals[name]?.isOpen || false);
|
||||
const payload = useSelector(
|
||||
state => state.modals[name]?.payload || defaultPayload
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleRequestClose = useCallback(
|
||||
(dismissed = true) => {
|
||||
dispatch(closeModal(name));
|
||||
|
||||
if (dismissed && payload.onDismiss) {
|
||||
payload.onDismiss();
|
||||
}
|
||||
},
|
||||
[payload.onDismiss]
|
||||
);
|
||||
|
||||
const modalProps = {
|
||||
isOpen,
|
||||
contentLabel: name,
|
||||
onRequestClose: handleRequestClose,
|
||||
className: {
|
||||
base: `modal modal-${name}`,
|
||||
afterOpen: 'modal-opening',
|
||||
beforeClose: 'modal-closing'
|
||||
},
|
||||
overlayClassName: {
|
||||
base: 'modal-overlay',
|
||||
afterOpen: 'modal-overlay-opening',
|
||||
beforeClose: 'modal-overlay-closing'
|
||||
},
|
||||
closeTimeoutMS: 200
|
||||
};
|
||||
|
||||
return [modalProps, payload, handleRequestClose];
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import get from 'lodash/get';
|
||||
import { getModals, closeModal } from 'state/modals';
|
||||
import connect from 'utils/connect';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
Modal.setAppElement('#root');
|
||||
|
||||
export default function withModal({ name, ...modalProps }) {
|
||||
modalProps = {
|
||||
className: {
|
||||
base: `modal modal-${name}`,
|
||||
afterOpen: 'modal-opening',
|
||||
beforeClose: 'modal-closing'
|
||||
},
|
||||
overlayClassName: {
|
||||
base: 'modal-overlay',
|
||||
afterOpen: 'modal-overlay-opening',
|
||||
beforeClose: 'modal-overlay-closing'
|
||||
},
|
||||
closeTimeoutMS: 200,
|
||||
...modalProps
|
||||
};
|
||||
|
||||
return WrappedComponent => {
|
||||
const ReduxModal = ({ onRequestClose, ...props }) => {
|
||||
const handleRequestClose = useCallback(
|
||||
(dismissed = true) => {
|
||||
onRequestClose();
|
||||
|
||||
if (dismissed && props.payload.onDismiss) {
|
||||
props.payload.onDismiss();
|
||||
}
|
||||
},
|
||||
[props.payload.onDismiss]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
contentLabel={name}
|
||||
onRequestClose={handleRequestClose}
|
||||
{...modalProps}
|
||||
{...props}
|
||||
>
|
||||
<WrappedComponent onClose={handleRequestClose} {...props} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const mapState = createStructuredSelector({
|
||||
isOpen: state => get(getModals(state), [name, 'isOpen'], false),
|
||||
payload: state => get(getModals(state), [name, 'payload'], {}),
|
||||
...modalProps.state
|
||||
});
|
||||
|
||||
const mapDispatch = dispatch => {
|
||||
const actions = { onRequestClose: () => dispatch(closeModal(name)) };
|
||||
if (modalProps.actions) {
|
||||
return {
|
||||
...actions,
|
||||
...bindActionCreators(modalProps.actions, dispatch)
|
||||
};
|
||||
}
|
||||
return actions;
|
||||
};
|
||||
|
||||
return connect(mapState, mapDispatch)(ReduxModal);
|
||||
};
|
||||
}
|
|
@ -50,12 +50,7 @@ const ChatTitle = ({
|
|||
{channel && channel.topic && (
|
||||
<span
|
||||
className="chat-topic"
|
||||
onClick={() =>
|
||||
openModal('topic', {
|
||||
topic: channel.topic,
|
||||
channel: channel.name
|
||||
})
|
||||
}
|
||||
onClick={() => openModal('topic', channel.name)}
|
||||
>
|
||||
{channel.topic}
|
||||
</span>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { socket as socketActions } from 'state/actions';
|
||||
import { getWrapWidth, appSet } from 'state/app';
|
||||
import { getConnected, getWrapWidth, appSet } from 'state/app';
|
||||
import { searchChannels } from 'state/channelSearch';
|
||||
import { addMessages } from 'state/messages';
|
||||
import { setSettings } from 'state/settings';
|
||||
import { when } from 'utils/observe';
|
||||
|
@ -12,6 +13,13 @@ function loadState({ store }, env) {
|
|||
type: socketActions.SERVERS,
|
||||
data: env.servers
|
||||
});
|
||||
|
||||
when(store, getConnected, () =>
|
||||
// Cache top channels for each server
|
||||
env.servers.forEach(({ host }) =>
|
||||
store.dispatch(searchChannels(host, ''))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (env.channels) {
|
||||
|
|
|
@ -9,7 +9,7 @@ export const getWindowWidth = state => state.app.windowWidth;
|
|||
export const getConnectDefaults = state => state.app.connectDefaults;
|
||||
|
||||
const initialState = {
|
||||
connected: true,
|
||||
connected: false,
|
||||
wrapWidth: 0,
|
||||
charWidth: 0,
|
||||
windowWidth: 0,
|
||||
|
|
|
@ -3,11 +3,12 @@ import * as actions from 'state/actions';
|
|||
|
||||
const initialState = {
|
||||
results: [],
|
||||
end: false
|
||||
end: false,
|
||||
topCache: {}
|
||||
};
|
||||
|
||||
export default createReducer(initialState, {
|
||||
[actions.socket.CHANNEL_SEARCH](state, { results, start }) {
|
||||
[actions.socket.CHANNEL_SEARCH](state, { results, start, server, q }) {
|
||||
if (results) {
|
||||
state.end = false;
|
||||
|
||||
|
@ -15,15 +16,20 @@ export default createReducer(initialState, {
|
|||
state.results.push(...results);
|
||||
} else {
|
||||
state.results = results;
|
||||
|
||||
if (!q) {
|
||||
state.topCache[server] = results;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state.end = true;
|
||||
}
|
||||
},
|
||||
|
||||
[actions.OPEN_MODAL](state, { name }) {
|
||||
[actions.OPEN_MODAL](state, { name, payload }) {
|
||||
if (name === 'channel') {
|
||||
return initialState;
|
||||
state.results = state.topCache[payload] || [];
|
||||
state.end = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue