Add early search prototype on the client, add leave button
This commit is contained in:
parent
deba58dbff
commit
7aae7685c5
10
client/src/css/fontello.css
vendored
10
client/src/css/fontello.css
vendored
@ -49,6 +49,12 @@
|
||||
/* Uncomment for 3D effect */
|
||||
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
|
||||
}
|
||||
|
||||
.icon-cog:before { content: '\e800'; } /* '' */
|
||||
.icon-ellipsis:before { content: '\e801'; } /* '' */
|
||||
.icon-ellipsis:before { content: '\e801'; } /* '' */
|
||||
.icon-search:before { content: '\e802'; } /* '' */
|
||||
.icon-user:before { content: '\e803'; } /* '' */
|
||||
.icon-users:before { content: '\e804'; } /* '' */
|
||||
.icon-logout:before { content: '\e805'; } /* '' */
|
||||
.icon-menu:before { content: '\e806'; } /* '' */
|
||||
.icon-plus:before { content: '\e807'; } /* '' */
|
||||
.icon-cancel:before { content: '\e808'; } /* '' */
|
@ -86,9 +86,8 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
|
||||
|
||||
.side-buttons i {
|
||||
color: #999;
|
||||
margin: 5px;
|
||||
margin: 25px;
|
||||
line-height: 40px;
|
||||
width: 40px;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
}
|
||||
@ -171,38 +170,75 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
|
||||
position: fixed;
|
||||
left: 200px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
right: 200px;
|
||||
height: 50px;
|
||||
padding: 0 15px;
|
||||
padding-left: 15px;
|
||||
line-height: 50px;
|
||||
border-bottom: 1px solid #DDD;
|
||||
display: flex;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.chat-title-bar div {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 100px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
.chat-title-bar i {
|
||||
padding: 0 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button-leave {
|
||||
border-left: 1px solid #DDD;
|
||||
}
|
||||
|
||||
.button-leave:hover {
|
||||
background: #DDD;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
margin-left: 15px;
|
||||
font-size: 28px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chat-topic {
|
||||
display: none;
|
||||
font: 16px Droid Sans Mono, monospace;
|
||||
.userlist-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 200px;
|
||||
height: 50px;
|
||||
border-left: 1px solid #DDD;
|
||||
border-bottom: 1px solid #DDD;
|
||||
line-height: 50px;
|
||||
vertical-align: top;
|
||||
color: #222;
|
||||
margin-left: 15px;
|
||||
text-align: center;
|
||||
padding: 0 15px;
|
||||
font-family: Montserrat, sans-serif;
|
||||
}
|
||||
|
||||
.chat-usercount {
|
||||
font-size: 20px;
|
||||
float: right;
|
||||
.userlist-bar i {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.search {
|
||||
display: none;
|
||||
position: fixed;
|
||||
left: 200px;
|
||||
top: 50px;
|
||||
bottom: 50px;
|
||||
right: 200px;
|
||||
z-index: 2;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #DDD;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.messagebox {
|
||||
|
Binary file not shown.
@ -8,6 +8,13 @@
|
||||
<missing-glyph horiz-adv-x="1000" />
|
||||
<glyph glyph-name="cog" unicode="" d="m571 350q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 15 20 15h124q7 0 13-4t7-12l15-103q28-9 50-21l80 60q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-13 0-6-4-12-9-12-29-38t-30-39q14-28 23-55l102-15q7-1 12-7t4-13z" horiz-adv-x="857.1" />
|
||||
<glyph glyph-name="ellipsis" unicode="" d="m214 439v-107q0-22-15-38t-38-15h-107q-23 0-38 15t-16 38v107q0 23 16 38t38 16h107q22 0 38-16t15-38z m286 0v-107q0-22-16-38t-38-15h-107q-22 0-38 15t-15 38v107q0 23 15 38t38 16h107q23 0 38-16t16-38z m286 0v-107q0-22-16-38t-38-15h-107q-22 0-38 15t-16 38v107q0 23 16 38t38 16h107q22 0 38-16t16-38z" horiz-adv-x="785.7" />
|
||||
<glyph glyph-name="search" unicode="" d="m643 386q0 103-74 176t-176 74-177-74-73-176 73-177 177-73 176 73 74 177z m286-465q0-29-22-50t-50-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 152-31 126-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
|
||||
<glyph glyph-name="user" unicode="" d="m786 66q0-67-41-106t-108-39h-488q-67 0-108 39t-41 106q0 30 2 58t8 61 15 60 24 55 34 45 48 30 62 11q5 0 24-12t41-27 60-27 75-12 74 12 61 27 41 27 24 12q34 0 62-11t48-30 34-45 24-55 15-60 8-61 2-58z m-179 498q0-88-63-151t-151-63-152 63-62 151 62 152 152 63 151-63 63-152z" horiz-adv-x="785.7" />
|
||||
<glyph glyph-name="users" unicode="" d="m331 350q-90-3-148-71h-75q-45 0-77 22t-31 66q0 197 69 197 4 0 25-11t54-24 66-12q38 0 75 13-3-21-3-37 0-78 45-143z m598-356q0-66-41-105t-108-39h-488q-68 0-108 39t-41 105q0 30 2 58t8 61 14 61 24 54 35 45 48 30 62 11q6 0 24-12t41-26 59-27 76-12 75 12 60 27 41 26 23 12q35 0 63-11t47-30 35-45 24-54 15-61 8-61 2-58z m-572 713q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z m393-214q0-89-63-152t-151-62-152 62-63 152 63 151 152 63 151-63 63-151z m321-126q0-43-31-66t-77-22h-75q-57 68-147 71 45 65 45 143 0 16-3 37 37-13 74-13 33 0 67 12t54 24 24 11q69 0 69-197z m-71 340q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z" horiz-adv-x="1071.4" />
|
||||
<glyph glyph-name="logout" unicode="" d="m502 0l0 100 98 0 0-100q0-40-29-70t-71-30l-400 0q-40 0-70 30t-30 70l0 700q0 42 30 71t70 29l400 0q42 0 71-29t29-71l0-150-98 0 0 150-402 0 0-700 402 0z m398 326l-198-196 0 120-450 0 0 150 450 0 0 120z" horiz-adv-x="900" />
|
||||
<glyph glyph-name="menu" unicode="" d="m650 400q22 0 36-15t14-35-15-35-35-15l-600 0q-20 0-35 15t-15 35 14 35 36 15l600 0z m-600 100q-20 0-35 15t-15 35 14 35 36 15l600 0q22 0 36-15t14-35-15-35-35-15l-600 0z m600-300q22 0 36-15t14-35-15-35-35-15l-600 0q-20 0-35 15t-15 35 14 35 36 15l600 0z" horiz-adv-x="700" />
|
||||
<glyph glyph-name="plus" unicode="" d="m786 439v-107q0-22-16-38t-38-15h-232v-233q0-22-16-37t-38-16h-107q-22 0-38 16t-15 37v233h-232q-23 0-38 15t-16 38v107q0 23 16 38t38 16h232v232q0 22 15 38t38 16h107q23 0 38-16t16-38v-232h232q22 0 38-16t16-38z" horiz-adv-x="785.7" />
|
||||
<glyph glyph-name="cancel" unicode="" d="m724 112q0-22-15-38l-76-76q-16-15-38-15t-38 15l-164 165-164-165q-16-15-38-15t-38 15l-76 76q-16 16-16 38t16 38l164 164-164 164q-16 16-16 38t16 38l76 76q16 16 38 16t38-16l164-164 164 164q16 16 38 16t38-16l76-76q15-15 15-38t-15-38l-164-164 164-164q15-15 15-38z" horiz-adv-x="785.7" />
|
||||
</font>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
Binary file not shown.
15
client/src/js/actions/search.js
Normal file
15
client/src/js/actions/search.js
Normal file
@ -0,0 +1,15 @@
|
||||
var Reflux = require('reflux');
|
||||
|
||||
var socket = require('../socket');
|
||||
|
||||
var searchActions = Reflux.createActions([
|
||||
'search',
|
||||
'searchDone',
|
||||
'toggle'
|
||||
]);
|
||||
|
||||
searchActions.search.preEmit = (server, channel, phrase) => {
|
||||
socket.send('search', { server, channel, phrase });
|
||||
};
|
||||
|
||||
module.exports = searchActions;
|
@ -3,6 +3,7 @@ var Reflux = require('reflux');
|
||||
var Router = require('react-router');
|
||||
|
||||
var ChatTitle = require('./ChatTitle.jsx');
|
||||
var Search = require('./Search.jsx');
|
||||
var MessageBox = require('./MessageBox.jsx');
|
||||
var MessageInput = require('./MessageInput.jsx');
|
||||
var UserList = require('./UserList.jsx');
|
||||
@ -27,6 +28,7 @@ var Chat = React.createClass({
|
||||
return (
|
||||
<div>
|
||||
<ChatTitle />
|
||||
<Search />
|
||||
<MessageBox indent={window.messageIndent} />
|
||||
<MessageInput />
|
||||
<UserList />
|
||||
|
@ -3,6 +3,9 @@ var Reflux = require('reflux');
|
||||
|
||||
var channelStore = require('../stores/channel');
|
||||
var selectedTabStore = require('../stores/selectedTab');
|
||||
var channelActions = require('../actions/channel');
|
||||
var searchActions = require('../actions/search');
|
||||
var privateChatActions = require('../actions/privateChat');
|
||||
|
||||
var ChatTitle = React.createClass({
|
||||
mixins: [
|
||||
@ -17,16 +20,40 @@ var ChatTitle = React.createClass({
|
||||
};
|
||||
},
|
||||
|
||||
handleLeaveClick: function() {
|
||||
var tab = this.state.selectedTab;
|
||||
|
||||
if (tab.channel[0] === '#') {
|
||||
channelActions.part([tab.channel], tab.server);
|
||||
} else if (tab.channel) {
|
||||
privateChatActions.close(tab.server, tab.channel);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var tab = this.state.selectedTab;
|
||||
var usercount = channelStore.getUsers(tab.server, tab.channel).length;
|
||||
var iconStyle = {};
|
||||
var userListStyle = {};
|
||||
|
||||
if (!tab.channel) {
|
||||
iconStyle.display = 'none';
|
||||
userListStyle.display = 'none';
|
||||
} else if (tab.channel[0] !== '#') {
|
||||
userListStyle.display = 'none';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chat-title-bar">
|
||||
<div>
|
||||
<div>
|
||||
<div className="chat-title-bar">
|
||||
<span className="chat-title">{tab.name}</span>
|
||||
<i className="icon-search" title="Search" style={iconStyle} onClick={searchActions.toggle}></i>
|
||||
<i className="icon-logout button-leave" title="Leave" style={iconStyle} onClick={this.handleLeaveClick}></i>
|
||||
</div>
|
||||
<div className="userlist-bar">
|
||||
<i className="icon-user" style={userListStyle}></i>
|
||||
<span className="chat-usercount" style={userListStyle}>{usercount || null}</span>
|
||||
</div>
|
||||
<span className="chat-usercount">{usercount || null}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
61
client/src/js/components/Search.jsx
Normal file
61
client/src/js/components/Search.jsx
Normal file
@ -0,0 +1,61 @@
|
||||
var React = require('react');
|
||||
var Reflux = require('reflux');
|
||||
var _ = require('lodash');
|
||||
|
||||
var util = require('../util');
|
||||
var searchStore = require('../stores/search');
|
||||
var selectedTabStore = require('../stores/selectedTab');
|
||||
var searchActions = require('../actions/search');
|
||||
|
||||
var Search = React.createClass({
|
||||
mixins: [
|
||||
Reflux.connect(searchStore),
|
||||
Reflux.connect(selectedTabStore, 'selectedTab')
|
||||
],
|
||||
|
||||
getInitialState: function() {
|
||||
var state = _.extend({}, searchStore.getState());
|
||||
state.selectedTab = selectedTabStore.getState();
|
||||
|
||||
return state;
|
||||
},
|
||||
|
||||
componentDidUpdate: function(prevProps, prevState) {
|
||||
if (!prevState.show && this.state.show) {
|
||||
this.refs.input.getDOMNode().focus();
|
||||
}
|
||||
},
|
||||
|
||||
handleChange: function(e) {
|
||||
var tab = this.state.selectedTab;
|
||||
|
||||
if (tab.channel) {
|
||||
searchActions.search(tab.server, tab.channel, e.target.value);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var style = {
|
||||
display: this.state.show ? 'block' : 'none'
|
||||
};
|
||||
|
||||
var results = _.map(this.state.results, (result) => {
|
||||
return (
|
||||
<p key={result.id}>{util.timestamp(new Date(result.time * 1000))} {result.from} {result.content}</p>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="search" style={style}>
|
||||
<input
|
||||
ref="input"
|
||||
className="search-input"
|
||||
type="text"
|
||||
onChange={this.handleChange} />
|
||||
<div className="search-results">{results}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Search;
|
@ -69,6 +69,7 @@ var TabList = React.createClass({
|
||||
<button className="button-connect" onClick={this.handleConnectClick}>Connect</button>
|
||||
{tabs}
|
||||
<div className="side-buttons">
|
||||
<i className="icon-user"></i>
|
||||
<i className="icon-cog" onClick={this.handleSettingsClick}></i>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,6 +7,7 @@ var messageActions = require('./actions/message');
|
||||
var serverActions = require('./actions/server');
|
||||
var routeActions = require('./actions/route');
|
||||
var tabActions = require('./actions/tab');
|
||||
var searchActions = require('./actions/search');
|
||||
|
||||
socket.on('join', function(data) {
|
||||
channelActions.addUser(data.user, data.server, data.channels[0]);
|
||||
@ -85,6 +86,10 @@ socket.on('channels', function(data) {
|
||||
channelActions.load(data);
|
||||
});
|
||||
|
||||
socket.on('search', function(data) {
|
||||
searchActions.searchDone(data.results);
|
||||
});
|
||||
|
||||
serverActions.connect.listen(function(server, nick, opts) {
|
||||
messageActions.inform('Connecting...', server);
|
||||
});
|
||||
|
@ -34,7 +34,7 @@ var privateChatStore = Reflux.createStore({
|
||||
},
|
||||
|
||||
close: function(server, nick) {
|
||||
delete privateChat[server][nick];
|
||||
delete privateChats[server][nick];
|
||||
this.trigger(privateChats);
|
||||
},
|
||||
|
||||
|
30
client/src/js/stores/search.js
Normal file
30
client/src/js/stores/search.js
Normal file
@ -0,0 +1,30 @@
|
||||
var Reflux = require('reflux');
|
||||
|
||||
var actions = require('../actions/search');
|
||||
|
||||
var state = {
|
||||
show: false,
|
||||
results: []
|
||||
};
|
||||
|
||||
var searchStore = Reflux.createStore({
|
||||
init: function() {
|
||||
this.listenToMany(actions);
|
||||
},
|
||||
|
||||
searchDone: function(results) {
|
||||
state.results = results;
|
||||
this.trigger(state);
|
||||
},
|
||||
|
||||
toggle: function() {
|
||||
state.show = !state.show;
|
||||
this.trigger(state);
|
||||
},
|
||||
|
||||
getState: function() {
|
||||
return state;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = searchStore;
|
@ -6,6 +6,7 @@ var actions = require('../actions/tab');
|
||||
var channelActions = require('../actions/channel');
|
||||
var serverActions = require('../actions/server');
|
||||
var routeActions = require('../actions/route');
|
||||
var privateChatActions = require('../actions/privateChat');
|
||||
|
||||
var selectedTab = {};
|
||||
|
||||
@ -13,6 +14,7 @@ var selectedTabStore = Reflux.createStore({
|
||||
init: function() {
|
||||
this.listenToMany(actions);
|
||||
this.listenTo(channelActions.part, 'part');
|
||||
this.listenTo(privateChatActions.close, 'close');
|
||||
this.listenTo(serverActions.disconnect, 'disconnect');
|
||||
this.listenTo(channelActions.addUser, 'userAdded');
|
||||
this.listenTo(channelActions.load, 'loadChannels');
|
||||
@ -43,6 +45,16 @@ var selectedTabStore = Reflux.createStore({
|
||||
}
|
||||
},
|
||||
|
||||
close: function(server, nick) {
|
||||
if (server === selectedTab.server &&
|
||||
nick === selectedTab.channel) {
|
||||
selectedTab.server = null;
|
||||
selectedTab.channel = null;
|
||||
selectedTab.name = null;
|
||||
this.trigger(selectedTab);
|
||||
}
|
||||
},
|
||||
|
||||
disconnect: function(server) {
|
||||
if (server === selectedTab.server) {
|
||||
selectedTab = {};
|
||||
|
File diff suppressed because one or more lines are too long
@ -2,6 +2,8 @@ package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/khlieng/name_pending/storage"
|
||||
)
|
||||
|
||||
type WSRequest struct {
|
||||
@ -111,6 +113,18 @@ type Away struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type SearchRequest struct {
|
||||
Server string `json:"server"`
|
||||
Channel string `json:"channel"`
|
||||
Phrase string `json:"phrase"`
|
||||
}
|
||||
|
||||
type SearchResult struct {
|
||||
Server string `json:"server"`
|
||||
Channel string `json:"channel"`
|
||||
Results []storage.Message `json:"results"`
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Server string `json:"server"`
|
||||
Message string `json:"message"`
|
||||
|
@ -202,6 +202,25 @@ func handleWS(ws *websocket.Conn) {
|
||||
if irc, ok := session.getIRC(data.Server); ok {
|
||||
irc.Away(data.Message)
|
||||
}
|
||||
|
||||
case "search":
|
||||
go func() {
|
||||
var data SearchRequest
|
||||
|
||||
json.Unmarshal(req.Request, &data)
|
||||
|
||||
results, err := session.user.SearchMessages(data.Server, data.Channel, data.Phrase)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
session.sendJSON("search", SearchResult{
|
||||
Server: data.Server,
|
||||
Channel: data.Channel,
|
||||
Results: results,
|
||||
})
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -235,11 +235,11 @@ func (u *User) GetMessages(server, channel string, count int, fromID uint64) ([]
|
||||
|
||||
func (u *User) SearchMessages(server, channel, phrase string) ([]Message, error) {
|
||||
serverQuery := bleve.NewMatchQuery(server)
|
||||
serverQuery.SetField("Server")
|
||||
serverQuery.SetField("server")
|
||||
channelQuery := bleve.NewMatchQuery(channel)
|
||||
channelQuery.SetField("To")
|
||||
channelQuery.SetField("to")
|
||||
contentQuery := bleve.NewMatchQuery(phrase)
|
||||
contentQuery.SetField("Content")
|
||||
contentQuery.SetField("content")
|
||||
|
||||
query := bleve.NewBooleanQuery([]bleve.Query{serverQuery, channelQuery, contentQuery}, nil, nil)
|
||||
|
||||
@ -249,7 +249,9 @@ func (u *User) SearchMessages(server, channel, phrase string) ([]Message, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var messages []Message
|
||||
log.Printf("%.3fms\n", searchResults.Took.Seconds()*1000)
|
||||
|
||||
messages := []Message{}
|
||||
u.messageLog.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket(bucketMessages)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user