Add channel joining UI, closes #37
This commit is contained in:
parent
f25594e962
commit
24b26aa85f
File diff suppressed because one or more lines are too long
@ -237,7 +237,8 @@ i[class*=' icon-']:before {
|
|||||||
top: 0;
|
top: 0;
|
||||||
bottom: 50px;
|
bottom: 50px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: auto;
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tablist p {
|
.tablist p {
|
||||||
@ -245,6 +246,9 @@ i[class*=' icon-']:before {
|
|||||||
padding: 3px 15px;
|
padding: 3px 15px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tablist p:last-child {
|
.tablist p:last-child {
|
||||||
@ -260,12 +264,6 @@ i[class*=' icon-']:before {
|
|||||||
border-left: 5px solid #6bb758;
|
border-left: 5px solid #6bb758;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-content {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-server {
|
.tab-server {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -279,11 +277,29 @@ i[class*=' icon-']:before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tab-label {
|
.tab-label {
|
||||||
margin-top: 10px;
|
margin: 5px;
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
margin-bottom: 5px;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #999;
|
color: #999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-label span {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-label button {
|
||||||
|
width: 24px;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 20px;
|
||||||
|
background: none;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-label button:hover {
|
||||||
|
color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-buttons {
|
.side-buttons {
|
||||||
@ -859,11 +875,24 @@ input.message-input-nick.invalid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.suspense-fallback {
|
.suspense-fallback {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font: 700 64px 'Montserrat', sans-serif;
|
font: 700 64px 'Montserrat', sans-serif;
|
||||||
height: 100%;
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suspense-modal-fallback {
|
||||||
|
position: fixed;
|
||||||
|
right: 15px;
|
||||||
|
bottom: 3px;
|
||||||
|
z-index: 1;
|
||||||
|
font: 700 64px 'Montserrat', sans-serif;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -874,6 +903,7 @@ input.message-input-nick.invalid {
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: rgba(0, 0, 0, 0.33);
|
background: rgba(0, 0, 0, 0.33);
|
||||||
@ -890,7 +920,8 @@ input.message-input-nick.invalid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
max-width: 600px;
|
width: 600px;
|
||||||
|
min-width: 0;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
@ -923,6 +954,82 @@ input.message-input-nick.invalid {
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-channel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - 120px);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-channel input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-channel-button-join {
|
||||||
|
margin: 0 !important;
|
||||||
|
width: 50px !important;
|
||||||
|
height: 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-channel-input-wrap {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-channel-close {
|
||||||
|
padding: 15px;
|
||||||
|
background: #fff;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-channel-close:hover {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-channel-result {
|
||||||
|
margin: 15px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-channel-result-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-channel-topic {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: Roboto Mono, monospace;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-channel-name {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-channel-users {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #444;
|
||||||
|
margin: 0 15px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-channel-users i {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-channel-results {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-channel-button-more {
|
||||||
|
margin-bottom: 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.tablist {
|
.tablist {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
@ -992,4 +1099,15 @@ input.message-input-nick.invalid {
|
|||||||
.button-install {
|
.button-install {
|
||||||
margin-left: 50px;
|
margin-left: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-channel {
|
||||||
|
margin: 0;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: fixed;
|
||||||
|
top: 5px;
|
||||||
|
left: 5px;
|
||||||
|
right: 5px;
|
||||||
|
bottom: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ const App = ({
|
|||||||
select,
|
select,
|
||||||
push,
|
push,
|
||||||
hideMenu,
|
hideMenu,
|
||||||
|
openModal,
|
||||||
newVersionAvailable,
|
newVersionAvailable,
|
||||||
hasOpenModals
|
hasOpenModals
|
||||||
}) => {
|
}) => {
|
||||||
@ -59,10 +60,10 @@ const App = ({
|
|||||||
showTabList={showTabList}
|
showTabList={showTabList}
|
||||||
select={select}
|
select={select}
|
||||||
push={push}
|
push={push}
|
||||||
|
openModal={openModal}
|
||||||
/>
|
/>
|
||||||
<div className={mainClass}>
|
<div className={mainClass}>
|
||||||
<Suspense fallback={<div className="suspense-fallback">...</div>}>
|
<Suspense fallback={<div className="suspense-fallback">...</div>}>
|
||||||
{renderModals && <Modals />}
|
|
||||||
<Route name="chat">
|
<Route name="chat">
|
||||||
<Chat />
|
<Chat />
|
||||||
</Route>
|
</Route>
|
||||||
@ -73,6 +74,11 @@ const App = ({
|
|||||||
<Settings />
|
<Settings />
|
||||||
</Route>
|
</Route>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
<Suspense
|
||||||
|
fallback={<div className="suspense-modal-fallback">...</div>}
|
||||||
|
>
|
||||||
|
{renderModals && <Modals />}
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,7 +11,14 @@ export default class TabList extends PureComponent {
|
|||||||
handleSettingsClick = () => this.props.push('/settings');
|
handleSettingsClick = () => this.props.push('/settings');
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { tab, channels, servers, privateChats, showTabList } = this.props;
|
const {
|
||||||
|
tab,
|
||||||
|
channels,
|
||||||
|
servers,
|
||||||
|
privateChats,
|
||||||
|
showTabList,
|
||||||
|
openModal
|
||||||
|
} = this.props;
|
||||||
const tabs = [];
|
const tabs = [];
|
||||||
|
|
||||||
const className = classnames('tablist', {
|
const className = classnames('tablist', {
|
||||||
@ -32,6 +39,17 @@ export default class TabList extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
tabs.push(
|
||||||
|
<div
|
||||||
|
key={`${address}-chans}`}
|
||||||
|
className="tab-label"
|
||||||
|
onClick={() => openModal('channel', { server: address })}
|
||||||
|
>
|
||||||
|
<span>CHANNELS ({server.channels.length})</span>
|
||||||
|
<Button>+</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
server.channels.forEach(name =>
|
server.channels.forEach(name =>
|
||||||
tabs.push(
|
tabs.push(
|
||||||
<TabListItem
|
<TabListItem
|
||||||
@ -48,7 +66,8 @@ export default class TabList extends PureComponent {
|
|||||||
if (privateChats[address] && privateChats[address].length > 0) {
|
if (privateChats[address] && privateChats[address].length > 0) {
|
||||||
tabs.push(
|
tabs.push(
|
||||||
<div key={`${address}-pm}`} className="tab-label">
|
<div key={`${address}-pm}`} className="tab-label">
|
||||||
Private messages
|
<span>DIRECT MESSAGES ({privateChats[address].length})</span>
|
||||||
|
{/*<Button>+</Button>*/}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
153
client/js/components/modals/AddChannel.js
Normal file
153
client/js/components/modals/AddChannel.js
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import React, { memo, useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import withModal from 'components/modals/withModal';
|
||||||
|
import Button from 'components/ui/Button';
|
||||||
|
import { join } from 'state/channels';
|
||||||
|
import { select } from 'state/tab';
|
||||||
|
import { searchChannels } from 'state/channelSearch';
|
||||||
|
|
||||||
|
const Channel = memo(({ server, name, topic, userCount, joined, ...props }) => {
|
||||||
|
const handleJoinClick = useCallback(() => props.join([name], server), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-channel-result">
|
||||||
|
<div className="modal-channel-result-header">
|
||||||
|
<h2 className="modal-channel-name" onClick={handleJoinClick}>
|
||||||
|
{name}
|
||||||
|
</h2>
|
||||||
|
<span className="modal-channel-users">
|
||||||
|
<i className="icon-user" />
|
||||||
|
{userCount}
|
||||||
|
</span>
|
||||||
|
{joined ? (
|
||||||
|
<span style={{ color: '#6bb758' }}>Joined</span>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
className="modal-channel-button-join"
|
||||||
|
category="normal"
|
||||||
|
onClick={handleJoinClick}
|
||||||
|
>
|
||||||
|
Join
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="modal-channel-topic">{topic}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const AddChannel = ({ search, payload: { server }, onClose, ...props }) => {
|
||||||
|
const [q, setQ] = useState('');
|
||||||
|
|
||||||
|
const inputEl = useRef();
|
||||||
|
const resultsEl = useRef();
|
||||||
|
const prevSearch = useRef('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
inputEl.current.focus();
|
||||||
|
props.searchChannels(server, '');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSearch = useCallback(
|
||||||
|
e => {
|
||||||
|
let nextQ = e.target.value.trim().toLowerCase();
|
||||||
|
setQ(nextQ);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[q]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleKey = useCallback(e => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
let channel = e.target.value.trim();
|
||||||
|
|
||||||
|
if (channel !== '') {
|
||||||
|
onClose(false);
|
||||||
|
|
||||||
|
if (channel.charAt(0) !== '#') {
|
||||||
|
channel = `#${channel}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
props.join([channel], server);
|
||||||
|
props.select(server, channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLoadMore = useCallback(
|
||||||
|
() => props.searchChannels(server, q, search.results.length),
|
||||||
|
[q, search.results.length]
|
||||||
|
);
|
||||||
|
|
||||||
|
let hasMore = !search.end;
|
||||||
|
if (hasMore) {
|
||||||
|
if (search.results.length < 10) {
|
||||||
|
hasMore = false;
|
||||||
|
} else if (
|
||||||
|
search.results.length > 10 &&
|
||||||
|
(search.results.length - 10) % 50 !== 0
|
||||||
|
) {
|
||||||
|
hasMore = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="modal-channel-input-wrap">
|
||||||
|
<input
|
||||||
|
ref={inputEl}
|
||||||
|
type="text"
|
||||||
|
value={q}
|
||||||
|
placeholder="Enter channel name"
|
||||||
|
onKeyDown={handleKey}
|
||||||
|
onChange={handleSearch}
|
||||||
|
/>
|
||||||
|
<i className="icon-cancel modal-channel-close" onClick={onClose} />
|
||||||
|
</div>
|
||||||
|
<div ref={resultsEl} className="modal-channel-results">
|
||||||
|
{search.results.map(channel => (
|
||||||
|
<Channel
|
||||||
|
key={`${server} ${channel.name}`}
|
||||||
|
server={server}
|
||||||
|
join={props.join}
|
||||||
|
joined={get(
|
||||||
|
props.channels,
|
||||||
|
[server, channel.name, 'joined'],
|
||||||
|
false
|
||||||
|
)}
|
||||||
|
{...channel}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{hasMore && (
|
||||||
|
<Button
|
||||||
|
className="modal-channel-button-more"
|
||||||
|
onClick={handleLoadMore}
|
||||||
|
>
|
||||||
|
Load more
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withModal({
|
||||||
|
name: 'channel',
|
||||||
|
state: {
|
||||||
|
channels: state => state.channels,
|
||||||
|
search: state => state.channelSearch
|
||||||
|
},
|
||||||
|
actions: { searchChannels, join, select }
|
||||||
|
})(AddChannel);
|
@ -1,8 +1,10 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
|
import AddChannel from 'components/modals/AddChannel';
|
||||||
import Confirm from 'components/modals/Confirm';
|
import Confirm from 'components/modals/Confirm';
|
||||||
|
|
||||||
const Modals = () => (
|
const Modals = () => (
|
||||||
<>
|
<>
|
||||||
|
<AddChannel />
|
||||||
<Confirm />
|
<Confirm />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import Modal from 'react-modal';
|
import Modal from 'react-modal';
|
||||||
import { createSelector } from 'reselect';
|
import { createStructuredSelector } from 'reselect';
|
||||||
|
import get from 'lodash/get';
|
||||||
import { getModals, closeModal } from 'state/modals';
|
import { getModals, closeModal } from 'state/modals';
|
||||||
import connect from 'utils/connect';
|
import connect from 'utils/connect';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
|
||||||
Modal.setAppElement('#root');
|
Modal.setAppElement('#root');
|
||||||
|
|
||||||
@ -47,15 +49,23 @@ export default function withModal({ name, ...modalProps }) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapState = createSelector(
|
const mapState = createStructuredSelector({
|
||||||
getModals,
|
isOpen: state => get(getModals(state), [name, 'isOpen'], false),
|
||||||
modals => modals[name] || { payload: {} }
|
payload: state => get(getModals(state), [name, 'payload'], {}),
|
||||||
);
|
...modalProps.state
|
||||||
|
|
||||||
const mapDispatch = dispatch => ({
|
|
||||||
onRequestClose: () => dispatch(closeModal(name))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mapDispatch = dispatch => {
|
||||||
|
let actions = { onRequestClose: () => dispatch(closeModal(name)) };
|
||||||
|
if (modalProps.actions) {
|
||||||
|
return {
|
||||||
|
...actions,
|
||||||
|
...bindActionCreators(modalProps.actions, dispatch)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return actions;
|
||||||
|
};
|
||||||
|
|
||||||
return connect(
|
return connect(
|
||||||
mapState,
|
mapState,
|
||||||
mapDispatch
|
mapDispatch
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import cn from 'classnames';
|
||||||
|
|
||||||
const Button = ({ children, category, ...props }) => (
|
const Button = ({ children, category, className, ...props }) => (
|
||||||
<button className={`button-${category}`} type="button" {...props}>
|
<button
|
||||||
|
className={cn(`button-${category}`, className)}
|
||||||
|
type="button"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,7 @@ import { createStructuredSelector } from 'reselect';
|
|||||||
import App from 'components/App';
|
import App from 'components/App';
|
||||||
import { getConnected } from 'state/app';
|
import { getConnected } from 'state/app';
|
||||||
import { getSortedChannels } from 'state/channels';
|
import { getSortedChannels } from 'state/channels';
|
||||||
import { getHasOpenModals } from 'state/modals';
|
import { openModal, getHasOpenModals } from 'state/modals';
|
||||||
import { getPrivateChats } from 'state/privateChats';
|
import { getPrivateChats } from 'state/privateChats';
|
||||||
import { getServers } from 'state/servers';
|
import { getServers } from 'state/servers';
|
||||||
import { getSelectedTab, select } from 'state/tab';
|
import { getSelectedTab, select } from 'state/tab';
|
||||||
@ -21,7 +21,7 @@ const mapState = createStructuredSelector({
|
|||||||
hasOpenModals: getHasOpenModals
|
hasOpenModals: getHasOpenModals
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatch = { push, select, hideMenu };
|
const mapDispatch = { push, select, hideMenu, openModal };
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapState,
|
mapState,
|
||||||
|
@ -6,6 +6,8 @@ export const KICK = 'KICK';
|
|||||||
export const PART = 'PART';
|
export const PART = 'PART';
|
||||||
export const SET_TOPIC = 'SET_TOPIC';
|
export const SET_TOPIC = 'SET_TOPIC';
|
||||||
|
|
||||||
|
export const CHANNEL_SEARCH = 'CHANNEL_SEARCH';
|
||||||
|
|
||||||
export const INPUT_HISTORY_ADD = 'INPUT_HISTORY_ADD';
|
export const INPUT_HISTORY_ADD = 'INPUT_HISTORY_ADD';
|
||||||
export const INPUT_HISTORY_DECREMENT = 'INPUT_HISTORY_DECREMENT';
|
export const INPUT_HISTORY_DECREMENT = 'INPUT_HISTORY_DECREMENT';
|
||||||
export const INPUT_HISTORY_INCREMENT = 'INPUT_HISTORY_INCREMENT';
|
export const INPUT_HISTORY_INCREMENT = 'INPUT_HISTORY_INCREMENT';
|
||||||
@ -64,6 +66,7 @@ export const socket = createSocketActions([
|
|||||||
'cert_fail',
|
'cert_fail',
|
||||||
'cert_success',
|
'cert_success',
|
||||||
'channels',
|
'channels',
|
||||||
|
'channel_search',
|
||||||
'connection_update',
|
'connection_update',
|
||||||
'join',
|
'join',
|
||||||
'message',
|
'message',
|
||||||
|
41
client/js/state/channelSearch.js
Normal file
41
client/js/state/channelSearch.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import createReducer from 'utils/createReducer';
|
||||||
|
import * as actions from 'state/actions';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
results: [],
|
||||||
|
end: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createReducer(initialState, {
|
||||||
|
[actions.socket.CHANNEL_SEARCH](state, { results, start }) {
|
||||||
|
if (results) {
|
||||||
|
state.end = false;
|
||||||
|
|
||||||
|
if (start > 0) {
|
||||||
|
state.results.push(...results);
|
||||||
|
} else {
|
||||||
|
state.results = results;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.end = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.OPEN_MODAL](state, { name }) {
|
||||||
|
if (name === 'channel') {
|
||||||
|
return initialState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function searchChannels(server, q, start) {
|
||||||
|
return {
|
||||||
|
type: actions.CHANNEL_SEARCH,
|
||||||
|
server,
|
||||||
|
q,
|
||||||
|
socket: {
|
||||||
|
type: 'channel_search',
|
||||||
|
data: { server, q, start }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
import app from './app';
|
import app from './app';
|
||||||
import channels from './channels';
|
import channels from './channels';
|
||||||
|
import channelSearch from './channelSearch';
|
||||||
import input from './input';
|
import input from './input';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import modals from './modals';
|
import modals from './modals';
|
||||||
@ -19,6 +20,7 @@ export default function createReducer(router) {
|
|||||||
router,
|
router,
|
||||||
app,
|
app,
|
||||||
channels,
|
channels,
|
||||||
|
channelSearch,
|
||||||
input,
|
input,
|
||||||
messages,
|
messages,
|
||||||
modals,
|
modals,
|
||||||
|
@ -3,6 +3,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ type ircHandler struct {
|
|||||||
whois WhoisReply
|
whois WhoisReply
|
||||||
userBuffers map[string][]string
|
userBuffers map[string][]string
|
||||||
motdBuffer MOTD
|
motdBuffer MOTD
|
||||||
|
listBuffer storage.ChannelListIndex
|
||||||
|
|
||||||
handlers map[string]func(*irc.Message)
|
handlers map[string]func(*irc.Message)
|
||||||
}
|
}
|
||||||
@ -183,6 +185,12 @@ func (i *ircHandler) info(msg *irc.Message) {
|
|||||||
New: msg.Params[0],
|
New: msg.Params[0],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
_, needsUpdate := channelIndexes.Get(i.client.Host)
|
||||||
|
if needsUpdate {
|
||||||
|
i.listBuffer = storage.NewMapChannelListIndex()
|
||||||
|
i.client.List()
|
||||||
|
}
|
||||||
|
|
||||||
go i.state.user.SetNick(msg.Params[0], i.client.Host)
|
go i.state.user.SetNick(msg.Params[0], i.client.Host)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,6 +289,34 @@ func (i *ircHandler) motdEnd(msg *irc.Message) {
|
|||||||
i.motdBuffer = MOTD{}
|
i.motdBuffer = MOTD{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *ircHandler) list(msg *irc.Message) {
|
||||||
|
if i.listBuffer == nil && i.state.Bool("update_chanlist_"+i.client.Host) {
|
||||||
|
i.listBuffer = storage.NewMapChannelListIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.listBuffer != nil {
|
||||||
|
c, _ := strconv.Atoi(msg.Params[2])
|
||||||
|
i.listBuffer.Add(&storage.ChannelListItem{
|
||||||
|
Name: msg.Params[1],
|
||||||
|
UserCount: c,
|
||||||
|
Topic: msg.LastParam(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ircHandler) listEnd(msg *irc.Message) {
|
||||||
|
if i.listBuffer != nil {
|
||||||
|
i.state.Set("update_chanlist_"+i.client.Host, false)
|
||||||
|
|
||||||
|
go func(idx storage.ChannelListIndex) {
|
||||||
|
idx.Finish()
|
||||||
|
channelIndexes.Set(i.client.Host, idx)
|
||||||
|
}(i.listBuffer)
|
||||||
|
|
||||||
|
i.listBuffer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (i *ircHandler) badNick(msg *irc.Message) {
|
func (i *ircHandler) badNick(msg *irc.Message) {
|
||||||
i.state.sendJSON("nick_fail", NickFail{
|
i.state.sendJSON("nick_fail", NickFail{
|
||||||
Server: i.client.Host,
|
Server: i.client.Host,
|
||||||
@ -321,6 +357,8 @@ func (i *ircHandler) initHandlers() {
|
|||||||
irc.ReplyMotdStart: i.motdStart,
|
irc.ReplyMotdStart: i.motdStart,
|
||||||
irc.ReplyMotd: i.motd,
|
irc.ReplyMotd: i.motd,
|
||||||
irc.ReplyEndOfMotd: i.motdEnd,
|
irc.ReplyEndOfMotd: i.motdEnd,
|
||||||
|
irc.ReplyList: i.list,
|
||||||
|
irc.ReplyListEnd: i.listEnd,
|
||||||
irc.ErrErroneousNickname: i.badNick,
|
irc.ErrErroneousNickname: i.badNick,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -192,3 +192,14 @@ type Error struct {
|
|||||||
Server string
|
Server string
|
||||||
Message string
|
Message string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChannelSearch struct {
|
||||||
|
Server string
|
||||||
|
Q string
|
||||||
|
Start int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChannelSearchResult struct {
|
||||||
|
Results []*storage.ChannelListItem
|
||||||
|
Start int
|
||||||
|
}
|
||||||
|
@ -3001,7 +3001,298 @@ func (v *ClientCert) UnmarshalJSON(data []byte) error {
|
|||||||
func (v *ClientCert) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
func (v *ClientCert) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||||
easyjson42239ddeDecodeGithubComKhliengDispatchServer26(l, v)
|
easyjson42239ddeDecodeGithubComKhliengDispatchServer26(l, v)
|
||||||
}
|
}
|
||||||
func easyjson42239ddeDecodeGithubComKhliengDispatchServer27(in *jlexer.Lexer, out *Away) {
|
func easyjson42239ddeDecodeGithubComKhliengDispatchServer27(in *jlexer.Lexer, out *ChannelSearchResult) {
|
||||||
|
isTopLevel := in.IsStart()
|
||||||
|
if in.IsNull() {
|
||||||
|
if isTopLevel {
|
||||||
|
in.Consumed()
|
||||||
|
}
|
||||||
|
in.Skip()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
in.Delim('{')
|
||||||
|
for !in.IsDelim('}') {
|
||||||
|
key := in.UnsafeString()
|
||||||
|
in.WantColon()
|
||||||
|
if in.IsNull() {
|
||||||
|
in.Skip()
|
||||||
|
in.WantComma()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch key {
|
||||||
|
case "results":
|
||||||
|
if in.IsNull() {
|
||||||
|
in.Skip()
|
||||||
|
out.Results = nil
|
||||||
|
} else {
|
||||||
|
in.Delim('[')
|
||||||
|
if out.Results == nil {
|
||||||
|
if !in.IsDelim(']') {
|
||||||
|
out.Results = make([]*storage.ChannelListItem, 0, 8)
|
||||||
|
} else {
|
||||||
|
out.Results = []*storage.ChannelListItem{}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Results = (out.Results)[:0]
|
||||||
|
}
|
||||||
|
for !in.IsDelim(']') {
|
||||||
|
var v22 *storage.ChannelListItem
|
||||||
|
if in.IsNull() {
|
||||||
|
in.Skip()
|
||||||
|
v22 = nil
|
||||||
|
} else {
|
||||||
|
if v22 == nil {
|
||||||
|
v22 = new(storage.ChannelListItem)
|
||||||
|
}
|
||||||
|
easyjson42239ddeDecodeGithubComKhliengDispatchStorage1(in, &*v22)
|
||||||
|
}
|
||||||
|
out.Results = append(out.Results, v22)
|
||||||
|
in.WantComma()
|
||||||
|
}
|
||||||
|
in.Delim(']')
|
||||||
|
}
|
||||||
|
case "start":
|
||||||
|
out.Start = int(in.Int())
|
||||||
|
default:
|
||||||
|
in.SkipRecursive()
|
||||||
|
}
|
||||||
|
in.WantComma()
|
||||||
|
}
|
||||||
|
in.Delim('}')
|
||||||
|
if isTopLevel {
|
||||||
|
in.Consumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func easyjson42239ddeEncodeGithubComKhliengDispatchServer27(out *jwriter.Writer, in ChannelSearchResult) {
|
||||||
|
out.RawByte('{')
|
||||||
|
first := true
|
||||||
|
_ = first
|
||||||
|
if len(in.Results) != 0 {
|
||||||
|
const prefix string = ",\"results\":"
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
out.RawString(prefix[1:])
|
||||||
|
} else {
|
||||||
|
out.RawString(prefix)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
out.RawByte('[')
|
||||||
|
for v23, v24 := range in.Results {
|
||||||
|
if v23 > 0 {
|
||||||
|
out.RawByte(',')
|
||||||
|
}
|
||||||
|
if v24 == nil {
|
||||||
|
out.RawString("null")
|
||||||
|
} else {
|
||||||
|
easyjson42239ddeEncodeGithubComKhliengDispatchStorage1(out, *v24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.RawByte(']')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if in.Start != 0 {
|
||||||
|
const prefix string = ",\"start\":"
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
out.RawString(prefix[1:])
|
||||||
|
} else {
|
||||||
|
out.RawString(prefix)
|
||||||
|
}
|
||||||
|
out.Int(int(in.Start))
|
||||||
|
}
|
||||||
|
out.RawByte('}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON supports json.Marshaler interface
|
||||||
|
func (v ChannelSearchResult) MarshalJSON() ([]byte, error) {
|
||||||
|
w := jwriter.Writer{}
|
||||||
|
easyjson42239ddeEncodeGithubComKhliengDispatchServer27(&w, v)
|
||||||
|
return w.Buffer.BuildBytes(), w.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalEasyJSON supports easyjson.Marshaler interface
|
||||||
|
func (v ChannelSearchResult) MarshalEasyJSON(w *jwriter.Writer) {
|
||||||
|
easyjson42239ddeEncodeGithubComKhliengDispatchServer27(w, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON supports json.Unmarshaler interface
|
||||||
|
func (v *ChannelSearchResult) UnmarshalJSON(data []byte) error {
|
||||||
|
r := jlexer.Lexer{Data: data}
|
||||||
|
easyjson42239ddeDecodeGithubComKhliengDispatchServer27(&r, v)
|
||||||
|
return r.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
||||||
|
func (v *ChannelSearchResult) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||||
|
easyjson42239ddeDecodeGithubComKhliengDispatchServer27(l, v)
|
||||||
|
}
|
||||||
|
func easyjson42239ddeDecodeGithubComKhliengDispatchStorage1(in *jlexer.Lexer, out *storage.ChannelListItem) {
|
||||||
|
isTopLevel := in.IsStart()
|
||||||
|
if in.IsNull() {
|
||||||
|
if isTopLevel {
|
||||||
|
in.Consumed()
|
||||||
|
}
|
||||||
|
in.Skip()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
in.Delim('{')
|
||||||
|
for !in.IsDelim('}') {
|
||||||
|
key := in.UnsafeString()
|
||||||
|
in.WantColon()
|
||||||
|
if in.IsNull() {
|
||||||
|
in.Skip()
|
||||||
|
in.WantComma()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch key {
|
||||||
|
case "name":
|
||||||
|
out.Name = string(in.String())
|
||||||
|
case "userCount":
|
||||||
|
out.UserCount = int(in.Int())
|
||||||
|
case "topic":
|
||||||
|
out.Topic = string(in.String())
|
||||||
|
default:
|
||||||
|
in.SkipRecursive()
|
||||||
|
}
|
||||||
|
in.WantComma()
|
||||||
|
}
|
||||||
|
in.Delim('}')
|
||||||
|
if isTopLevel {
|
||||||
|
in.Consumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func easyjson42239ddeEncodeGithubComKhliengDispatchStorage1(out *jwriter.Writer, in storage.ChannelListItem) {
|
||||||
|
out.RawByte('{')
|
||||||
|
first := true
|
||||||
|
_ = first
|
||||||
|
if in.Name != "" {
|
||||||
|
const prefix string = ",\"name\":"
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
out.RawString(prefix[1:])
|
||||||
|
} else {
|
||||||
|
out.RawString(prefix)
|
||||||
|
}
|
||||||
|
out.String(string(in.Name))
|
||||||
|
}
|
||||||
|
if in.UserCount != 0 {
|
||||||
|
const prefix string = ",\"userCount\":"
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
out.RawString(prefix[1:])
|
||||||
|
} else {
|
||||||
|
out.RawString(prefix)
|
||||||
|
}
|
||||||
|
out.Int(int(in.UserCount))
|
||||||
|
}
|
||||||
|
if in.Topic != "" {
|
||||||
|
const prefix string = ",\"topic\":"
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
out.RawString(prefix[1:])
|
||||||
|
} else {
|
||||||
|
out.RawString(prefix)
|
||||||
|
}
|
||||||
|
out.String(string(in.Topic))
|
||||||
|
}
|
||||||
|
out.RawByte('}')
|
||||||
|
}
|
||||||
|
func easyjson42239ddeDecodeGithubComKhliengDispatchServer28(in *jlexer.Lexer, out *ChannelSearch) {
|
||||||
|
isTopLevel := in.IsStart()
|
||||||
|
if in.IsNull() {
|
||||||
|
if isTopLevel {
|
||||||
|
in.Consumed()
|
||||||
|
}
|
||||||
|
in.Skip()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
in.Delim('{')
|
||||||
|
for !in.IsDelim('}') {
|
||||||
|
key := in.UnsafeString()
|
||||||
|
in.WantColon()
|
||||||
|
if in.IsNull() {
|
||||||
|
in.Skip()
|
||||||
|
in.WantComma()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch key {
|
||||||
|
case "server":
|
||||||
|
out.Server = string(in.String())
|
||||||
|
case "q":
|
||||||
|
out.Q = string(in.String())
|
||||||
|
case "start":
|
||||||
|
out.Start = int(in.Int())
|
||||||
|
default:
|
||||||
|
in.SkipRecursive()
|
||||||
|
}
|
||||||
|
in.WantComma()
|
||||||
|
}
|
||||||
|
in.Delim('}')
|
||||||
|
if isTopLevel {
|
||||||
|
in.Consumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func easyjson42239ddeEncodeGithubComKhliengDispatchServer28(out *jwriter.Writer, in ChannelSearch) {
|
||||||
|
out.RawByte('{')
|
||||||
|
first := true
|
||||||
|
_ = first
|
||||||
|
if in.Server != "" {
|
||||||
|
const prefix string = ",\"server\":"
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
out.RawString(prefix[1:])
|
||||||
|
} else {
|
||||||
|
out.RawString(prefix)
|
||||||
|
}
|
||||||
|
out.String(string(in.Server))
|
||||||
|
}
|
||||||
|
if in.Q != "" {
|
||||||
|
const prefix string = ",\"q\":"
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
out.RawString(prefix[1:])
|
||||||
|
} else {
|
||||||
|
out.RawString(prefix)
|
||||||
|
}
|
||||||
|
out.String(string(in.Q))
|
||||||
|
}
|
||||||
|
if in.Start != 0 {
|
||||||
|
const prefix string = ",\"start\":"
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
out.RawString(prefix[1:])
|
||||||
|
} else {
|
||||||
|
out.RawString(prefix)
|
||||||
|
}
|
||||||
|
out.Int(int(in.Start))
|
||||||
|
}
|
||||||
|
out.RawByte('}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON supports json.Marshaler interface
|
||||||
|
func (v ChannelSearch) MarshalJSON() ([]byte, error) {
|
||||||
|
w := jwriter.Writer{}
|
||||||
|
easyjson42239ddeEncodeGithubComKhliengDispatchServer28(&w, v)
|
||||||
|
return w.Buffer.BuildBytes(), w.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalEasyJSON supports easyjson.Marshaler interface
|
||||||
|
func (v ChannelSearch) MarshalEasyJSON(w *jwriter.Writer) {
|
||||||
|
easyjson42239ddeEncodeGithubComKhliengDispatchServer28(w, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON supports json.Unmarshaler interface
|
||||||
|
func (v *ChannelSearch) UnmarshalJSON(data []byte) error {
|
||||||
|
r := jlexer.Lexer{Data: data}
|
||||||
|
easyjson42239ddeDecodeGithubComKhliengDispatchServer28(&r, v)
|
||||||
|
return r.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
||||||
|
func (v *ChannelSearch) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||||
|
easyjson42239ddeDecodeGithubComKhliengDispatchServer28(l, v)
|
||||||
|
}
|
||||||
|
func easyjson42239ddeDecodeGithubComKhliengDispatchServer29(in *jlexer.Lexer, out *Away) {
|
||||||
isTopLevel := in.IsStart()
|
isTopLevel := in.IsStart()
|
||||||
if in.IsNull() {
|
if in.IsNull() {
|
||||||
if isTopLevel {
|
if isTopLevel {
|
||||||
@ -3034,7 +3325,7 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer27(in *jlexer.Lexer, ou
|
|||||||
in.Consumed()
|
in.Consumed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func easyjson42239ddeEncodeGithubComKhliengDispatchServer27(out *jwriter.Writer, in Away) {
|
func easyjson42239ddeEncodeGithubComKhliengDispatchServer29(out *jwriter.Writer, in Away) {
|
||||||
out.RawByte('{')
|
out.RawByte('{')
|
||||||
first := true
|
first := true
|
||||||
_ = first
|
_ = first
|
||||||
@ -3064,23 +3355,23 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer27(out *jwriter.Writer,
|
|||||||
// MarshalJSON supports json.Marshaler interface
|
// MarshalJSON supports json.Marshaler interface
|
||||||
func (v Away) MarshalJSON() ([]byte, error) {
|
func (v Away) MarshalJSON() ([]byte, error) {
|
||||||
w := jwriter.Writer{}
|
w := jwriter.Writer{}
|
||||||
easyjson42239ddeEncodeGithubComKhliengDispatchServer27(&w, v)
|
easyjson42239ddeEncodeGithubComKhliengDispatchServer29(&w, v)
|
||||||
return w.Buffer.BuildBytes(), w.Error
|
return w.Buffer.BuildBytes(), w.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalEasyJSON supports easyjson.Marshaler interface
|
// MarshalEasyJSON supports easyjson.Marshaler interface
|
||||||
func (v Away) MarshalEasyJSON(w *jwriter.Writer) {
|
func (v Away) MarshalEasyJSON(w *jwriter.Writer) {
|
||||||
easyjson42239ddeEncodeGithubComKhliengDispatchServer27(w, v)
|
easyjson42239ddeEncodeGithubComKhliengDispatchServer29(w, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON supports json.Unmarshaler interface
|
// UnmarshalJSON supports json.Unmarshaler interface
|
||||||
func (v *Away) UnmarshalJSON(data []byte) error {
|
func (v *Away) UnmarshalJSON(data []byte) error {
|
||||||
r := jlexer.Lexer{Data: data}
|
r := jlexer.Lexer{Data: data}
|
||||||
easyjson42239ddeDecodeGithubComKhliengDispatchServer27(&r, v)
|
easyjson42239ddeDecodeGithubComKhliengDispatchServer29(&r, v)
|
||||||
return r.Error()
|
return r.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
||||||
func (v *Away) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
func (v *Away) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||||
easyjson42239ddeDecodeGithubComKhliengDispatchServer27(l, v)
|
easyjson42239ddeDecodeGithubComKhliengDispatchServer29(l, v)
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var channelStore = storage.NewChannelStore()
|
var channelStore = storage.NewChannelStore()
|
||||||
|
var channelIndexes = storage.NewChannelIndexManager()
|
||||||
|
|
||||||
type Dispatch struct {
|
type Dispatch struct {
|
||||||
Store storage.Store
|
Store storage.Store
|
||||||
|
@ -13,10 +13,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// AnonymousUserExpiration is the time to wait before removing an anonymous
|
||||||
|
// user that has no irc or websocket connections
|
||||||
AnonymousUserExpiration = 1 * time.Minute
|
AnonymousUserExpiration = 1 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// State is the live state of a single user
|
||||||
type State struct {
|
type State struct {
|
||||||
|
stateData
|
||||||
|
|
||||||
irc map[string]*irc.Client
|
irc map[string]*irc.Client
|
||||||
connectionState map[string]irc.ConnectionState
|
connectionState map[string]irc.ConnectionState
|
||||||
ircLock sync.Mutex
|
ircLock sync.Mutex
|
||||||
@ -33,6 +38,7 @@ type State struct {
|
|||||||
|
|
||||||
func NewState(user *storage.User, srv *Dispatch) *State {
|
func NewState(user *storage.User, srv *Dispatch) *State {
|
||||||
return &State{
|
return &State{
|
||||||
|
stateData: stateData{m: map[string]interface{}{}},
|
||||||
irc: make(map[string]*irc.Client),
|
irc: make(map[string]*irc.Client),
|
||||||
connectionState: make(map[string]irc.ConnectionState),
|
connectionState: make(map[string]irc.ConnectionState),
|
||||||
ws: make(map[string]*wsConn),
|
ws: make(map[string]*wsConn),
|
||||||
@ -225,6 +231,36 @@ func (s *State) run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type stateData struct {
|
||||||
|
m map[string]interface{}
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stateData) Get(key string) interface{} {
|
||||||
|
s.lock.Lock()
|
||||||
|
v := s.m[key]
|
||||||
|
s.lock.Unlock()
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stateData) Set(key string, value interface{}) {
|
||||||
|
s.lock.Lock()
|
||||||
|
s.m[key] = value
|
||||||
|
s.lock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stateData) String(key string) string {
|
||||||
|
return s.Get(key).(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stateData) Int(key string) int {
|
||||||
|
return s.Get(key).(int)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stateData) Bool(key string) bool {
|
||||||
|
return s.Get(key).(bool)
|
||||||
|
}
|
||||||
|
|
||||||
type stateStore struct {
|
type stateStore struct {
|
||||||
states map[uint64]*State
|
states map[uint64]*State
|
||||||
sessions map[string]*session.Session
|
sessions map[string]*session.Session
|
||||||
|
@ -97,6 +97,8 @@ func (h *wsHandler) connect(b []byte) {
|
|||||||
var data Server
|
var data Server
|
||||||
data.UnmarshalJSON(b)
|
data.UnmarshalJSON(b)
|
||||||
|
|
||||||
|
data.Host = strings.ToLower(data.Host)
|
||||||
|
|
||||||
if _, ok := h.state.getIRC(data.Host); !ok {
|
if _, ok := h.state.getIRC(data.Host); !ok {
|
||||||
log.Println(h.addr, "[IRC] Add server", data.Host)
|
log.Println(h.addr, "[IRC] Add server", data.Host)
|
||||||
|
|
||||||
@ -281,6 +283,29 @@ func (h *wsHandler) setSettings(b []byte) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *wsHandler) channelSearch(b []byte) {
|
||||||
|
var data ChannelSearch
|
||||||
|
data.UnmarshalJSON(b)
|
||||||
|
|
||||||
|
index, needsUpdate := channelIndexes.Get(data.Server)
|
||||||
|
if index != nil {
|
||||||
|
n := 10
|
||||||
|
if data.Start > 0 {
|
||||||
|
n = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
h.state.sendJSON("channel_search", ChannelSearchResult{
|
||||||
|
Results: index.SearchN(data.Q, data.Start, n),
|
||||||
|
Start: data.Start,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if i, ok := h.state.getIRC(data.Server); ok && needsUpdate {
|
||||||
|
h.state.Set("update_chanlist_"+data.Server, true)
|
||||||
|
i.List()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *wsHandler) initHandlers() {
|
func (h *wsHandler) initHandlers() {
|
||||||
h.handlers = map[string]func([]byte){
|
h.handlers = map[string]func([]byte){
|
||||||
"connect": h.connect,
|
"connect": h.connect,
|
||||||
@ -301,6 +326,7 @@ func (h *wsHandler) initHandlers() {
|
|||||||
"fetch_messages": h.fetchMessages,
|
"fetch_messages": h.fetchMessages,
|
||||||
"set_server_name": h.setServerName,
|
"set_server_name": h.setServerName,
|
||||||
"settings_set": h.setSettings,
|
"settings_set": h.setSettings,
|
||||||
|
"channel_search": h.channelSearch,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
148
storage/channel_index.go
Normal file
148
storage/channel_index.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChannelListItem struct {
|
||||||
|
Name string
|
||||||
|
UserCount int
|
||||||
|
Topic string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChannelListIndex interface {
|
||||||
|
Add(item *ChannelListItem)
|
||||||
|
Finish()
|
||||||
|
Search(q string) []*ChannelListItem
|
||||||
|
SearchN(q string, start, n int) []*ChannelListItem
|
||||||
|
}
|
||||||
|
|
||||||
|
type MapChannelListIndex struct {
|
||||||
|
channels chanList
|
||||||
|
m map[string][]*ChannelListItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMapChannelListIndex() *MapChannelListIndex {
|
||||||
|
return &MapChannelListIndex{
|
||||||
|
m: map[string][]*ChannelListItem{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (idx *MapChannelListIndex) Add(item *ChannelListItem) {
|
||||||
|
idx.channels = append(idx.channels, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (idx *MapChannelListIndex) Finish() {
|
||||||
|
sort.Sort(idx.channels)
|
||||||
|
|
||||||
|
for _, ch := range idx.channels {
|
||||||
|
key := strings.TrimLeft(strings.ToLower(ch.Name), "#")
|
||||||
|
|
||||||
|
for i := 1; i <= len(key); i++ {
|
||||||
|
k := key[:i]
|
||||||
|
if _, ok := idx.m[k]; ok {
|
||||||
|
idx.m[k] = append(idx.m[k], ch)
|
||||||
|
} else {
|
||||||
|
idx.m[k] = chanList{ch}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (idx *MapChannelListIndex) Search(q string) []*ChannelListItem {
|
||||||
|
if q == "" {
|
||||||
|
return idx.channels
|
||||||
|
}
|
||||||
|
return idx.m[q]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (idx *MapChannelListIndex) SearchN(q string, start, n int) []*ChannelListItem {
|
||||||
|
if q == "" {
|
||||||
|
if start >= len(idx.channels) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return idx.channels[start:min(start+n, len(idx.channels))]
|
||||||
|
}
|
||||||
|
|
||||||
|
res := idx.m[q]
|
||||||
|
if start >= len(res) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return res[start:min(start+n, len(res))]
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(x, y int) int {
|
||||||
|
if x < y {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
|
||||||
|
type chanList []*ChannelListItem
|
||||||
|
|
||||||
|
func (c chanList) Len() int {
|
||||||
|
return len(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c chanList) Less(i, j int) bool {
|
||||||
|
return c[i].UserCount > c[j].UserCount ||
|
||||||
|
(c[i].UserCount == c[j].UserCount &&
|
||||||
|
strings.ToLower(c[i].Name) < strings.ToLower(c[j].Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c chanList) Swap(i, j int) {
|
||||||
|
ch := c[i]
|
||||||
|
c[i] = c[j]
|
||||||
|
c[j] = ch
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChannelListUpdateInterval = time.Hour * 24
|
||||||
|
|
||||||
|
type ChannelIndexManager struct {
|
||||||
|
indexes map[string]*managedChannelIndex
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChannelIndexManager() *ChannelIndexManager {
|
||||||
|
return &ChannelIndexManager{
|
||||||
|
indexes: map[string]*managedChannelIndex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type managedChannelIndex struct {
|
||||||
|
index ChannelListIndex
|
||||||
|
updatedAt time.Time
|
||||||
|
updating bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ChannelIndexManager) Get(server string) (ChannelListIndex, bool) {
|
||||||
|
m.lock.Lock()
|
||||||
|
defer m.lock.Unlock()
|
||||||
|
|
||||||
|
idx, ok := m.indexes[server]
|
||||||
|
if !ok {
|
||||||
|
m.indexes[server] = &managedChannelIndex{
|
||||||
|
updating: true,
|
||||||
|
}
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !idx.updating && time.Since(idx.updatedAt) > ChannelListUpdateInterval {
|
||||||
|
idx.updating = true
|
||||||
|
return idx.index, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return idx.index, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ChannelIndexManager) Set(server string, index ChannelListIndex) {
|
||||||
|
m.lock.Lock()
|
||||||
|
m.indexes[server] = &managedChannelIndex{
|
||||||
|
index: index,
|
||||||
|
updatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
m.lock.Unlock()
|
||||||
|
}
|
44
storage/channel_index_test.go
Normal file
44
storage/channel_index_test.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMapChannelListIndex(t *testing.T) {
|
||||||
|
i := NewMapChannelListIndex()
|
||||||
|
i.Add(&ChannelListItem{
|
||||||
|
Name: "#apples",
|
||||||
|
UserCount: 120,
|
||||||
|
})
|
||||||
|
i.Add(&ChannelListItem{
|
||||||
|
Name: "#cake",
|
||||||
|
UserCount: 150,
|
||||||
|
})
|
||||||
|
i.Add(&ChannelListItem{
|
||||||
|
Name: "#beans",
|
||||||
|
UserCount: 12,
|
||||||
|
})
|
||||||
|
i.Add(&ChannelListItem{
|
||||||
|
Name: "#pie",
|
||||||
|
UserCount: 1200,
|
||||||
|
})
|
||||||
|
i.Add(&ChannelListItem{
|
||||||
|
Name: "#Pork",
|
||||||
|
UserCount: 1200,
|
||||||
|
})
|
||||||
|
i.Finish()
|
||||||
|
|
||||||
|
assert.Len(t, i.Search(""), 5)
|
||||||
|
assert.Len(t, i.SearchN("", 0, 20), 5)
|
||||||
|
assert.Len(t, i.SearchN("", 0, 1), 1)
|
||||||
|
assert.Len(t, i.SearchN("", 1, 1), 1)
|
||||||
|
assert.Len(t, i.SearchN("", 0, 0), 0)
|
||||||
|
|
||||||
|
assert.Equal(t, "#pie", i.Search("")[0].Name)
|
||||||
|
assert.Equal(t, "#Pork", i.Search("")[1].Name)
|
||||||
|
|
||||||
|
assert.Len(t, i.Search("p"), 2)
|
||||||
|
assert.Equal(t, "#Pork", i.Search("p")[1].Name)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user