Support changing the server name by clicking it in the status tab

This commit is contained in:
Ken-Håvard Lieng 2017-06-12 06:18:32 +02:00
parent 3b33957161
commit b639ba6846
14 changed files with 259 additions and 56 deletions

File diff suppressed because one or more lines are too long

View File

@ -85,6 +85,7 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
}
.tablist p {
height: 30px;
padding: 3px 15px;
padding-right: 10px;
cursor: pointer;
@ -322,7 +323,7 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
.chat-title {
margin-left: 15px;
font-size: 24px;
font-size: 24px !important;
white-space: nowrap;
}

View File

@ -1,11 +1,14 @@
import React, { PureComponent } from 'react';
import { List } from 'immutable';
import Navicon from '../containers/Navicon';
import Editable from './ui/Editable';
import { isValidServerName } from '../state/servers';
import { linkify } from '../util';
export default class ChatTitle extends PureComponent {
render() {
const { title, tab, channel, onToggleSearch, onToggleUserList, onCloseClick } = this.props;
const { title, tab, channel, onTitleChange,
onToggleSearch, onToggleUserList, onCloseClick } = this.props;
let closeTitle;
if (tab.isChannel()) {
@ -20,7 +23,15 @@ export default class ChatTitle extends PureComponent {
<div>
<div className="chat-title-bar">
<Navicon />
<span className="chat-title">{title}</span>
<Editable
className="chat-title"
editable={!tab.name}
value={title}
validate={isValidServerName}
onChange={onTitleChange}
>
<span className="chat-title">{title}</span>
</Editable>
<div className="chat-topic-wrap">
<span className="chat-topic">{linkify(channel.get('topic')) || null}</span>
</div>

View File

@ -31,6 +31,11 @@ export default class Chat extends Component {
select(tab.server, nick);
};
handleTitleChange = title => {
const { setServerName, tab } = this.props;
setServerName(title, tab.server);
};
render() {
const {
channel,
@ -69,6 +74,7 @@ export default class Chat extends Component {
tab={tab}
title={title}
onCloseClick={this.handleCloseClick}
onTitleChange={this.handleTitleChange}
onToggleSearch={toggleSearch}
onToggleUserList={toggleUserList}
/>

View File

@ -0,0 +1,54 @@
import React, { PureComponent } from 'react';
const style = {
background: 'none',
font: 'inherit'
};
export default class Editable extends PureComponent {
state = { editing: false };
startEditing = () => {
if (this.props.editable) {
this.initialValue = this.props.value;
this.setState({ editing: true });
}
};
stopEditing = () => {
const { validate, value, onChange } = this.props;
if (validate && !validate(value)) {
onChange(this.initialValue);
}
this.setState({ editing: false });
};
handleKey = e => {
if (e.key === 'Enter') {
this.stopEditing();
}
};
handleChange = e => this.props.onChange(e.target.value);
render() {
const { children, className, value } = this.props;
return (
<div onClick={this.startEditing}>
{this.state.editing ?
<input
autoFocus
className={className}
style={style}
type="text"
value={value}
onBlur={this.stopEditing}
onChange={this.handleChange}
onKeyDown={this.handleKey}
/> :
children
}
</div>
);
}
}

View File

@ -10,7 +10,7 @@ import { getSelectedMessages, getHasMoreMessages,
runCommand, sendMessage, fetchMessages, addFetchedMessages } from '../state/messages';
import { openPrivateChat, closePrivateChat } from '../state/privateChats';
import { getSearch, searchMessages, toggleSearch } from '../state/search';
import { getCurrentNick, disconnect } from '../state/servers';
import { getCurrentNick, disconnect, setServerName } from '../state/servers';
import { getSelectedTab, select } from '../state/tab';
import { getShowUserList, toggleUserList } from '../state/ui';
@ -39,6 +39,7 @@ const mapDispatch = dispatch => ({
searchMessages,
select,
sendMessage,
setServerName,
toggleSearch,
toggleUserList
}, dispatch),

View File

@ -1,9 +1,36 @@
export default function createSocketMiddleware(socket) {
return () => next => action => {
if (action.socket) {
socket.send(action.socket.type, action.socket.data);
}
import debounce from 'lodash/debounce';
return next(action);
const debounceKey = action => {
const key = action.socket.debounce.key;
if (key) {
return `${action.type} ${key}`;
}
return action.type;
};
export default function createSocketMiddleware(socket) {
return () => next => {
const debounced = {};
return action => {
if (action.socket) {
if (action.socket.debounce) {
const key = debounceKey(action);
if (!debounced[key]) {
debounced[key] = debounce((type, data) => {
socket.send(type, data);
debounced[key] = undefined;
}, action.socket.debounce.delay);
}
debounced[key](action.socket.type, action.socket.data);
} else {
socket.send(action.socket.type, action.socket.data);
}
}
return next(action);
};
};
}

View File

@ -0,0 +1,20 @@
import { setServerName } from '../servers';
describe('setServerName()', () => {
it('passes valid names to the server', () => {
const name = 'cake';
const server = 'srv';
expect(setServerName(name, server)).toMatchObject({
socket: {
type: 'set_server_name',
data: { name, server }
}
});
});
it('does not pass invalid names to the server', () => {
expect(setServerName('', 'srv').socket).toBeUndefined();
expect(setServerName(' ', 'srv').socket).toBeUndefined();
});
});

View File

@ -1,5 +1,5 @@
import Immutable from 'immutable';
import reducer, { connect } from '../servers';
import reducer, { connect, setServerName } from '../servers';
import * as actions from '../actions';
describe('reducers/servers', () => {
@ -58,6 +58,22 @@ describe('reducers/servers', () => {
});
});
it('handles SET_SERVER_NAME', () => {
let state = Immutable.fromJS({
srv: {
name: 'cake'
}
});
state = reducer(state, setServerName('pie', 'srv'));
expect(state.toJS()).toEqual({
srv: {
name: 'pie'
}
});
});
it('updates the nick on SOCKET_NICK', () => {
let state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
state = reducer(state, {

View File

@ -29,6 +29,7 @@ export const AWAY = 'AWAY';
export const CONNECT = 'CONNECT';
export const DISCONNECT = 'DISCONNECT';
export const SET_NICK = 'SET_NICK';
export const SET_SERVER_NAME = 'SET_SERVER_NAME';
export const WHOIS = 'WHOIS';
export const SET_CERT = 'SET_CERT';

View File

@ -25,9 +25,7 @@ export const getCurrentServerName = createSelector(
);
export default createReducer(Map(), {
[actions.CONNECT](state, action) {
const { host, nick, options } = action;
[actions.CONNECT](state, { host, nick, options }) {
if (!state.has(host)) {
return state.set(host, new Server({
nick,
@ -38,8 +36,12 @@ export default createReducer(Map(), {
return state;
},
[actions.DISCONNECT](state, action) {
return state.delete(action.server);
[actions.DISCONNECT](state, { server }) {
return state.delete(server);
},
[actions.SET_SERVER_NAME](state, { server, name }) {
return state.setIn([server, 'name'], name);
},
[actions.socket.NICK](state, action) {
@ -50,13 +52,13 @@ export default createReducer(Map(), {
return state;
},
[actions.socket.SERVERS](state, action) {
if (!action.data) {
[actions.socket.SERVERS](state, { data }) {
if (!data) {
return state;
}
return state.withMutations(s => {
action.data.forEach(server => {
data.forEach(server => {
s.set(server.host, new Server(server));
});
});
@ -152,3 +154,31 @@ export function setNick(nick, server) {
}
};
}
export function isValidServerName(name) {
return name.trim() !== '';
}
export function setServerName(name, server) {
const action = {
type: actions.SET_SERVER_NAME,
name,
server
};
if (isValidServerName(name)) {
action.socket = {
type: 'set_server_name',
data: {
name,
server
},
debounce: {
delay: 1000,
key: server
}
};
}
return action;
}

View File

@ -283,22 +283,36 @@ func (h *wsHandler) fetchMessages(b []byte) {
h.session.sendMessages(data.Server, data.Channel, 200, data.Next)
}
func (h *wsHandler) initHandlers() {
h.handlers = map[string]func([]byte){
"connect": h.connect,
"join": h.join,
"part": h.part,
"quit": h.quit,
"message": h.message,
"nick": h.nick,
"topic": h.topic,
"invite": h.invite,
"kick": h.kick,
"whois": h.whois,
"away": h.away,
"raw": h.raw,
"search": h.search,
"cert": h.cert,
"fetch_messages": h.fetchMessages,
func (h *wsHandler) setServerName(b []byte) {
var data Connect
json.Unmarshal(b, &data)
if isValidServerName(data.Name) {
h.session.user.SetServerName(data.Name, data.Server)
}
}
func (h *wsHandler) initHandlers() {
h.handlers = map[string]func([]byte){
"connect": h.connect,
"join": h.join,
"part": h.part,
"quit": h.quit,
"message": h.message,
"nick": h.nick,
"topic": h.topic,
"invite": h.invite,
"kick": h.kick,
"whois": h.whois,
"away": h.away,
"raw": h.raw,
"search": h.search,
"cert": h.cert,
"fetch_messages": h.fetchMessages,
"set_server_name": h.setServerName,
}
}
func isValidServerName(name string) bool {
return strings.TrimSpace(name) != ""
}

View File

@ -178,6 +178,25 @@ func (u *User) SetNick(nick, address string) {
})
}
func (u *User) SetServerName(name, address string) {
db.Batch(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketServers)
id := u.serverID(address)
server := Server{}
v := b.Get(id)
if v != nil {
server.Unmarshal(v)
server.Name = name
data, _ := server.Marshal(nil)
b.Put(id, data)
}
return nil
})
}
func (u *User) RemoveServer(address string) {
db.Batch(func(tx *bolt.Tx) error {
serverID := u.serverID(address)

View File

@ -63,6 +63,9 @@ func TestUser(t *testing.T) {
user.SetNick("bob", srv.Host)
assert.Equal(t, "bob", user.GetServers()[0].Nick)
user.SetServerName("cake", srv.Host)
assert.Equal(t, "cake", user.GetServers()[0].Name)
user.RemoveChannel(srv.Host, chan1.Name)
channels = user.GetChannels()
assert.Len(t, channels, 1)