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 {
|
.tablist p {
|
||||||
|
height: 30px;
|
||||||
padding: 3px 15px;
|
padding: 3px 15px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -322,7 +323,7 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
|
|||||||
|
|
||||||
.chat-title {
|
.chat-title {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
font-size: 24px;
|
font-size: 24px !important;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { List } from 'immutable';
|
import { List } from 'immutable';
|
||||||
import Navicon from '../containers/Navicon';
|
import Navicon from '../containers/Navicon';
|
||||||
|
import Editable from './ui/Editable';
|
||||||
|
import { isValidServerName } from '../state/servers';
|
||||||
import { linkify } from '../util';
|
import { linkify } from '../util';
|
||||||
|
|
||||||
export default class ChatTitle extends PureComponent {
|
export default class ChatTitle extends PureComponent {
|
||||||
render() {
|
render() {
|
||||||
const { title, tab, channel, onToggleSearch, onToggleUserList, onCloseClick } = this.props;
|
const { title, tab, channel, onTitleChange,
|
||||||
|
onToggleSearch, onToggleUserList, onCloseClick } = this.props;
|
||||||
|
|
||||||
let closeTitle;
|
let closeTitle;
|
||||||
if (tab.isChannel()) {
|
if (tab.isChannel()) {
|
||||||
@ -20,7 +23,15 @@ export default class ChatTitle extends PureComponent {
|
|||||||
<div>
|
<div>
|
||||||
<div className="chat-title-bar">
|
<div className="chat-title-bar">
|
||||||
<Navicon />
|
<Navicon />
|
||||||
|
<Editable
|
||||||
|
className="chat-title"
|
||||||
|
editable={!tab.name}
|
||||||
|
value={title}
|
||||||
|
validate={isValidServerName}
|
||||||
|
onChange={onTitleChange}
|
||||||
|
>
|
||||||
<span className="chat-title">{title}</span>
|
<span className="chat-title">{title}</span>
|
||||||
|
</Editable>
|
||||||
<div className="chat-topic-wrap">
|
<div className="chat-topic-wrap">
|
||||||
<span className="chat-topic">{linkify(channel.get('topic')) || null}</span>
|
<span className="chat-topic">{linkify(channel.get('topic')) || null}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,6 +31,11 @@ export default class Chat extends Component {
|
|||||||
select(tab.server, nick);
|
select(tab.server, nick);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleTitleChange = title => {
|
||||||
|
const { setServerName, tab } = this.props;
|
||||||
|
setServerName(title, tab.server);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
channel,
|
channel,
|
||||||
@ -69,6 +74,7 @@ export default class Chat extends Component {
|
|||||||
tab={tab}
|
tab={tab}
|
||||||
title={title}
|
title={title}
|
||||||
onCloseClick={this.handleCloseClick}
|
onCloseClick={this.handleCloseClick}
|
||||||
|
onTitleChange={this.handleTitleChange}
|
||||||
onToggleSearch={toggleSearch}
|
onToggleSearch={toggleSearch}
|
||||||
onToggleUserList={toggleUserList}
|
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';
|
runCommand, sendMessage, fetchMessages, addFetchedMessages } from '../state/messages';
|
||||||
import { openPrivateChat, closePrivateChat } from '../state/privateChats';
|
import { openPrivateChat, closePrivateChat } from '../state/privateChats';
|
||||||
import { getSearch, searchMessages, toggleSearch } from '../state/search';
|
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 { getSelectedTab, select } from '../state/tab';
|
||||||
import { getShowUserList, toggleUserList } from '../state/ui';
|
import { getShowUserList, toggleUserList } from '../state/ui';
|
||||||
|
|
||||||
@ -39,6 +39,7 @@ const mapDispatch = dispatch => ({
|
|||||||
searchMessages,
|
searchMessages,
|
||||||
select,
|
select,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
|
setServerName,
|
||||||
toggleSearch,
|
toggleSearch,
|
||||||
toggleUserList
|
toggleUserList
|
||||||
}, dispatch),
|
}, 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) {
|
export default function createSocketMiddleware(socket) {
|
||||||
return () => next => action => {
|
return () => next => {
|
||||||
|
const debounced = {};
|
||||||
|
|
||||||
|
return action => {
|
||||||
if (action.socket) {
|
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);
|
socket.send(action.socket.type, action.socket.data);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return next(action);
|
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 Immutable from 'immutable';
|
||||||
import reducer, { connect } from '../servers';
|
import reducer, { connect, setServerName } from '../servers';
|
||||||
import * as actions from '../actions';
|
import * as actions from '../actions';
|
||||||
|
|
||||||
describe('reducers/servers', () => {
|
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', () => {
|
it('updates the nick on SOCKET_NICK', () => {
|
||||||
let state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
|
let state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
|
||||||
state = reducer(state, {
|
state = reducer(state, {
|
||||||
|
@ -29,6 +29,7 @@ export const AWAY = 'AWAY';
|
|||||||
export const CONNECT = 'CONNECT';
|
export const CONNECT = 'CONNECT';
|
||||||
export const DISCONNECT = 'DISCONNECT';
|
export const DISCONNECT = 'DISCONNECT';
|
||||||
export const SET_NICK = 'SET_NICK';
|
export const SET_NICK = 'SET_NICK';
|
||||||
|
export const SET_SERVER_NAME = 'SET_SERVER_NAME';
|
||||||
export const WHOIS = 'WHOIS';
|
export const WHOIS = 'WHOIS';
|
||||||
|
|
||||||
export const SET_CERT = 'SET_CERT';
|
export const SET_CERT = 'SET_CERT';
|
||||||
|
@ -25,9 +25,7 @@ export const getCurrentServerName = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export default createReducer(Map(), {
|
export default createReducer(Map(), {
|
||||||
[actions.CONNECT](state, action) {
|
[actions.CONNECT](state, { host, nick, options }) {
|
||||||
const { host, nick, options } = action;
|
|
||||||
|
|
||||||
if (!state.has(host)) {
|
if (!state.has(host)) {
|
||||||
return state.set(host, new Server({
|
return state.set(host, new Server({
|
||||||
nick,
|
nick,
|
||||||
@ -38,8 +36,12 @@ export default createReducer(Map(), {
|
|||||||
return state;
|
return state;
|
||||||
},
|
},
|
||||||
|
|
||||||
[actions.DISCONNECT](state, action) {
|
[actions.DISCONNECT](state, { server }) {
|
||||||
return state.delete(action.server);
|
return state.delete(server);
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.SET_SERVER_NAME](state, { server, name }) {
|
||||||
|
return state.setIn([server, 'name'], name);
|
||||||
},
|
},
|
||||||
|
|
||||||
[actions.socket.NICK](state, action) {
|
[actions.socket.NICK](state, action) {
|
||||||
@ -50,13 +52,13 @@ export default createReducer(Map(), {
|
|||||||
return state;
|
return state;
|
||||||
},
|
},
|
||||||
|
|
||||||
[actions.socket.SERVERS](state, action) {
|
[actions.socket.SERVERS](state, { data }) {
|
||||||
if (!action.data) {
|
if (!data) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.withMutations(s => {
|
return state.withMutations(s => {
|
||||||
action.data.forEach(server => {
|
data.forEach(server => {
|
||||||
s.set(server.host, new Server(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)
|
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() {
|
func (h *wsHandler) initHandlers() {
|
||||||
h.handlers = map[string]func([]byte){
|
h.handlers = map[string]func([]byte){
|
||||||
"connect": h.connect,
|
"connect": h.connect,
|
||||||
@ -300,5 +309,10 @@ func (h *wsHandler) initHandlers() {
|
|||||||
"search": h.search,
|
"search": h.search,
|
||||||
"cert": h.cert,
|
"cert": h.cert,
|
||||||
"fetch_messages": h.fetchMessages,
|
"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) {
|
func (u *User) RemoveServer(address string) {
|
||||||
db.Batch(func(tx *bolt.Tx) error {
|
db.Batch(func(tx *bolt.Tx) error {
|
||||||
serverID := u.serverID(address)
|
serverID := u.serverID(address)
|
||||||
|
@ -63,6 +63,9 @@ func TestUser(t *testing.T) {
|
|||||||
user.SetNick("bob", srv.Host)
|
user.SetNick("bob", srv.Host)
|
||||||
assert.Equal(t, "bob", user.GetServers()[0].Nick)
|
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)
|
user.RemoveChannel(srv.Host, chan1.Name)
|
||||||
channels = user.GetChannels()
|
channels = user.GetChannels()
|
||||||
assert.Len(t, channels, 1)
|
assert.Len(t, channels, 1)
|
||||||
|
Loading…
Reference in New Issue
Block a user