Support changing the server name by clicking it in the status tab
This commit is contained in:
parent
3b33957161
commit
b639ba6846
File diff suppressed because one or more lines are too long
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 />
|
||||
<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>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
54
client/src/js/components/ui/Editable.js
Normal file
54
client/src/js/components/ui/Editable.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -1,9 +1,36 @@
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
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 => action => {
|
||||
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);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
20
client/src/js/state/__tests__/actions-servers.test.js
Normal file
20
client/src/js/state/__tests__/actions-servers.test.js
Normal 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();
|
||||
});
|
||||
});
|
@ -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, {
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -283,6 +283,15 @@ func (h *wsHandler) fetchMessages(b []byte) {
|
||||
h.session.sendMessages(data.Server, data.Channel, 200, data.Next)
|
||||
}
|
||||
|
||||
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,
|
||||
@ -300,5 +309,10 @@ func (h *wsHandler) initHandlers() {
|
||||
"search": h.search,
|
||||
"cert": h.cert,
|
||||
"fetch_messages": h.fetchMessages,
|
||||
"set_server_name": h.setServerName,
|
||||
}
|
||||
}
|
||||
|
||||
func isValidServerName(name string) bool {
|
||||
return strings.TrimSpace(name) != ""
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user