Add colored nicks settings option
This commit is contained in:
parent
ec03db4db6
commit
6c6a9e12cf
27 changed files with 577 additions and 109 deletions
|
@ -81,7 +81,7 @@
|
|||
"test:watch": "jest --watch",
|
||||
"gen:install": "GO111MODULE=off go get -u github.com/andyleap/gencode github.com/mailru/easyjson/... github.com/SlinSo/egon/cmd/egon",
|
||||
"gen:binary": "gencode go -package storage -schema ../storage/storage.schema -unsafe",
|
||||
"gen:json": "easyjson -all -lower_camel_case -omit_empty ../server/json.go ../server/index_data.go",
|
||||
"gen:json": "easyjson -all -lower_camel_case -omit_empty ../server/json.go ../server/index_data.go && easyjson -lower_camel_case -omit_empty ../storage/user.go",
|
||||
"gen:template": "egon -s -m ../server"
|
||||
},
|
||||
"jest": {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
body {
|
||||
font-family: Roboto Mono, monospace;
|
||||
background: #f0f0f0;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
h1,
|
||||
|
@ -16,11 +17,6 @@ h4,
|
|||
h5,
|
||||
h6 {
|
||||
font-family: Montserrat, sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
input {
|
||||
|
@ -28,6 +24,7 @@ input {
|
|||
border: none;
|
||||
outline: none;
|
||||
background: #fff;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
input::-ms-clear {
|
||||
|
@ -54,26 +51,44 @@ button:active {
|
|||
background: #6bb758;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
label {
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox.top-label {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.checkbox input {
|
||||
position: absolute;
|
||||
left: -99999px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
input[type='checkbox'] + span {
|
||||
.checkbox span {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #777;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
input[type='checkbox']:checked + span {
|
||||
.checkbox:not(.top-label) span {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.checkbox input:checked + span {
|
||||
background: #6bb758;
|
||||
border-color: #6bb758;
|
||||
}
|
||||
|
||||
input[type='checkbox']:checked + span:before {
|
||||
.checkbox input:checked + span:before {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
|
@ -456,6 +471,8 @@ input::-webkit-inner-spin-button {
|
|||
margin-left: 10px;
|
||||
padding: 0 5px;
|
||||
font: 24px Montserrat, sans-serif;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
white-space: nowrap;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
@ -692,48 +709,86 @@ input.message-input-nick.invalid {
|
|||
background: #ddd;
|
||||
}
|
||||
|
||||
.settings {
|
||||
text-align: center;
|
||||
.settings-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.settings p {
|
||||
color: #999;
|
||||
.settings {
|
||||
flex: 1;
|
||||
max-width: 692px;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
border: 1px solid #ddd;
|
||||
padding: 15px;
|
||||
margin: 0 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.settings .checkbox {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.settings h1 {
|
||||
text-align: center;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.settings h2 {
|
||||
margin: 15px;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.settings button {
|
||||
margin: 5px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.settings div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.settings .error {
|
||||
margin: 10px;
|
||||
margin-top: 15px;
|
||||
color: #f6546a;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input-file {
|
||||
color: #fff;
|
||||
background: #222 !important;
|
||||
padding: 10px;
|
||||
margin: 5px;
|
||||
width: 200px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.settings-file {
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
margin-top: 15px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 906px) {
|
||||
.settings-file {
|
||||
display: block;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-file p {
|
||||
margin-bottom: 5px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.settings-cert {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ReactVirtualized__List {
|
||||
box-sizing: content-box !important;
|
||||
outline: none;
|
||||
|
@ -813,4 +868,8 @@ input.message-input-nick.invalid {
|
|||
margin: auto 50px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
margin-left: 50px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ export default class Chat extends Component {
|
|||
render() {
|
||||
const {
|
||||
channel,
|
||||
coloredNicks,
|
||||
currentInputHistoryEntry,
|
||||
hasMoreMessages,
|
||||
messages,
|
||||
|
@ -69,7 +70,6 @@ export default class Chat extends Component {
|
|||
toggleSearch,
|
||||
toggleUserList
|
||||
} = this.props;
|
||||
|
||||
let chatClass;
|
||||
if (isChannel(tab)) {
|
||||
chatClass = 'chat-channel';
|
||||
|
@ -93,6 +93,7 @@ export default class Chat extends Component {
|
|||
/>
|
||||
<Search search={search} onSearch={this.handleSearch} />
|
||||
<MessageBox
|
||||
coloredNicks={coloredNicks}
|
||||
hasMoreMessages={hasMoreMessages}
|
||||
messages={messages}
|
||||
tab={tab}
|
||||
|
@ -111,6 +112,7 @@ export default class Chat extends Component {
|
|||
{...inputActions}
|
||||
/>
|
||||
<UserList
|
||||
coloredNicks={coloredNicks}
|
||||
showUserList={showUserList}
|
||||
users={users}
|
||||
onNickClick={this.handleNickClick}
|
||||
|
|
|
@ -6,7 +6,7 @@ export default class Message extends PureComponent {
|
|||
handleNickClick = () => this.props.onNickClick(this.props.message.from);
|
||||
|
||||
render() {
|
||||
const { message } = this.props;
|
||||
const { message, coloredNick } = this.props;
|
||||
|
||||
const className = classnames('message', {
|
||||
[`message-${message.type}`]: message.type
|
||||
|
@ -19,7 +19,7 @@ export default class Message extends PureComponent {
|
|||
};
|
||||
|
||||
const senderStyle = {};
|
||||
if (message.from) {
|
||||
if (message.from && coloredNick) {
|
||||
senderStyle.color = stringToRGB(message.from);
|
||||
}
|
||||
|
||||
|
|
|
@ -176,13 +176,14 @@ export default class MessageBox extends PureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { messages, onNickClick } = this.props;
|
||||
const { messages, coloredNicks, onNickClick } = this.props;
|
||||
const message = messages[index - 1];
|
||||
|
||||
return (
|
||||
<Message
|
||||
key={message.id}
|
||||
message={message}
|
||||
coloredNick={coloredNicks}
|
||||
style={style}
|
||||
onNickClick={onNickClick}
|
||||
/>
|
||||
|
|
|
@ -16,12 +16,13 @@ export default class UserList extends PureComponent {
|
|||
};
|
||||
|
||||
renderUser = ({ index, style, key }) => {
|
||||
const { users, onNickClick } = this.props;
|
||||
const { users, coloredNicks, onNickClick } = this.props;
|
||||
|
||||
return (
|
||||
<UserListItem
|
||||
key={key}
|
||||
user={users[index]}
|
||||
coloredNick={coloredNicks}
|
||||
style={style}
|
||||
onClick={onNickClick}
|
||||
/>
|
||||
|
|
|
@ -5,11 +5,15 @@ export default class UserListItem extends PureComponent {
|
|||
handleClick = () => this.props.onClick(this.props.user.nick);
|
||||
|
||||
render() {
|
||||
const { user } = this.props;
|
||||
const style = {
|
||||
color: stringToRGB(user.nick),
|
||||
...this.props.style
|
||||
};
|
||||
const { user, coloredNick } = this.props;
|
||||
let { style } = this.props;
|
||||
|
||||
if (coloredNick) {
|
||||
style = {
|
||||
color: stringToRGB(user.nick),
|
||||
...style
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<p style={style} onClick={this.handleClick}>
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
|||
import { createSelector } from 'reselect';
|
||||
import { Form, withFormik } from 'formik';
|
||||
import Navicon from 'containers/Navicon';
|
||||
import Checkbox from 'components/ui/Checkbox';
|
||||
import Checkbox from 'components/ui/formik/Checkbox';
|
||||
import TextInput from 'components/ui/TextInput';
|
||||
import Error from 'components/ui/formik/Error';
|
||||
import { isValidNick, isValidChannel, isValidUsername, isInt } from 'utils';
|
||||
|
@ -77,7 +77,12 @@ class Connect extends Component {
|
|||
<div className="connect-form-address">
|
||||
<TextInput name="host" placeholder="Host" />
|
||||
<TextInput name="port" type="number" placeholder="Port" />
|
||||
<Checkbox name="tls" label="SSL" onChange={this.handleSSLChange} />
|
||||
<Checkbox
|
||||
name="tls"
|
||||
label="SSL"
|
||||
topLabel
|
||||
onChange={this.handleSSLChange}
|
||||
/>
|
||||
</div>
|
||||
<Error name="host" />
|
||||
<Error name="port" />
|
||||
|
|
|
@ -1,32 +1,56 @@
|
|||
import React from 'react';
|
||||
import Navicon from 'containers/Navicon';
|
||||
import Checkbox from 'components/ui/Checkbox';
|
||||
import FileInput from 'components/ui/FileInput';
|
||||
|
||||
const Settings = ({ settings, onCertChange, onKeyChange, uploadCert }) => {
|
||||
const Settings = ({
|
||||
settings,
|
||||
setSetting,
|
||||
onCertChange,
|
||||
onKeyChange,
|
||||
uploadCert
|
||||
}) => {
|
||||
const status = settings.uploadingCert ? 'Uploading...' : 'Upload';
|
||||
const error = settings.certError;
|
||||
|
||||
return (
|
||||
<div className="settings">
|
||||
<Navicon />
|
||||
<h1>Settings</h1>
|
||||
<h2>Client Certificate</h2>
|
||||
<div>
|
||||
<p>Certificate</p>
|
||||
<FileInput
|
||||
name={settings.certFile || 'Select Certificate'}
|
||||
onChange={onCertChange}
|
||||
/>
|
||||
<div className="settings-container">
|
||||
<div className="settings">
|
||||
<Navicon />
|
||||
<h1>Settings</h1>
|
||||
<div className="settings-section">
|
||||
<h2>Visuals</h2>
|
||||
<Checkbox
|
||||
name="coloredNicks"
|
||||
label="Colored nicks"
|
||||
checked={settings.coloredNicks}
|
||||
onChange={e => setSetting('coloredNicks', e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="settings-section">
|
||||
<h2>Client Certificate</h2>
|
||||
<div className="settings-cert">
|
||||
<div className="settings-file">
|
||||
<p>Certificate</p>
|
||||
<FileInput
|
||||
name={settings.certFile || 'Select Certificate'}
|
||||
onChange={onCertChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="settings-file">
|
||||
<p>Private Key</p>
|
||||
<FileInput
|
||||
name={settings.keyFile || 'Select Key'}
|
||||
onChange={onKeyChange}
|
||||
/>
|
||||
</div>
|
||||
<button className="settings-button" onClick={uploadCert}>
|
||||
{status}
|
||||
</button>
|
||||
{error ? <p className="error">{error}</p> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p>Private Key</p>
|
||||
<FileInput
|
||||
name={settings.keyFile || 'Select Key'}
|
||||
onChange={onKeyChange}
|
||||
/>
|
||||
</div>
|
||||
<button onClick={uploadCert}>{status}</button>
|
||||
{error ? <p className="error">{error}</p> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,30 +1,18 @@
|
|||
import React from 'react';
|
||||
import { Field } from 'formik';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const Checkbox = ({ name, label, onChange, ...props }) => (
|
||||
<Field
|
||||
name={name}
|
||||
render={({ field, form }) => (
|
||||
<label htmlFor={name}>
|
||||
{label && <div>{label}</div>}
|
||||
<input
|
||||
type="checkbox"
|
||||
id={name}
|
||||
name={name}
|
||||
checked={field.value}
|
||||
onChange={e => {
|
||||
form.setFieldTouched(name, true);
|
||||
field.onChange(e);
|
||||
if (onChange) {
|
||||
onChange(e);
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
<span />
|
||||
</label>
|
||||
)}
|
||||
/>
|
||||
const Checkbox = ({ name, label, topLabel, ...props }) => (
|
||||
<label
|
||||
className={classnames('checkbox', {
|
||||
'top-label': topLabel
|
||||
})}
|
||||
htmlFor={name}
|
||||
>
|
||||
{topLabel && label}
|
||||
<input type="checkbox" id={name} name={name} {...props} />
|
||||
<span />
|
||||
{!topLabel && label}
|
||||
</label>
|
||||
);
|
||||
|
||||
export default Checkbox;
|
||||
|
|
25
client/src/js/components/ui/formik/Checkbox.js
Normal file
25
client/src/js/components/ui/formik/Checkbox.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
import { Field } from 'formik';
|
||||
import Checkbox from 'components/ui/Checkbox';
|
||||
|
||||
const FormikCheckbox = ({ name, onChange, ...props }) => (
|
||||
<Field
|
||||
name={name}
|
||||
render={({ field, form }) => (
|
||||
<Checkbox
|
||||
name={name}
|
||||
checked={field.value}
|
||||
onChange={e => {
|
||||
form.setFieldTouched(name, true);
|
||||
field.onChange(e);
|
||||
if (onChange) {
|
||||
onChange(e);
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
export default FormikCheckbox;
|
0
client/src/js/components/ui/formik/TextInput.js
Normal file
0
client/src/js/components/ui/formik/TextInput.js
Normal file
|
@ -32,6 +32,7 @@ import {
|
|||
setNick,
|
||||
setServerName
|
||||
} from 'state/servers';
|
||||
import { getSettings } from 'state/settings';
|
||||
import { getSelectedTab, select } from 'state/tab';
|
||||
import { getShowUserList, toggleUserList } from 'state/ui';
|
||||
|
||||
|
@ -46,7 +47,8 @@ const mapState = createStructuredSelector({
|
|||
status: getCurrentServerStatus,
|
||||
tab: getSelectedTab,
|
||||
title: getSelectedTabTitle,
|
||||
users: getSelectedChannelUsers
|
||||
users: getSelectedChannelUsers,
|
||||
coloredNicks: state => getSettings(state).coloredNicks
|
||||
});
|
||||
|
||||
const mapDispatch = dispatch => ({
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import Settings from 'components/pages/Settings';
|
||||
import { getSettings, setCert, setKey, uploadCert } from 'state/settings';
|
||||
import {
|
||||
getSettings,
|
||||
setSetting,
|
||||
setCert,
|
||||
setKey,
|
||||
uploadCert
|
||||
} from 'state/settings';
|
||||
|
||||
const mapState = createStructuredSelector({
|
||||
settings: getSettings
|
||||
|
@ -10,7 +16,8 @@ const mapState = createStructuredSelector({
|
|||
const mapDispatch = {
|
||||
onCertChange: setCert,
|
||||
onKeyChange: setKey,
|
||||
uploadCert
|
||||
uploadCert,
|
||||
setSetting
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
|
|
@ -2,6 +2,7 @@ import Cookie from 'js-cookie';
|
|||
import { socket as socketActions } from 'state/actions';
|
||||
import { getWrapWidth, setConnectDefaults, appSet } from 'state/app';
|
||||
import { addMessages } from 'state/messages';
|
||||
import { setSettings } from 'state/settings';
|
||||
import { select, updateSelection } from 'state/tab';
|
||||
import { find } from 'utils';
|
||||
import { when } from 'utils/observe';
|
||||
|
@ -12,6 +13,7 @@ export default function initialState({ store }) {
|
|||
|
||||
store.dispatch(setConnectDefaults(env.defaults));
|
||||
store.dispatch(appSet('hexIP', env.hexIP));
|
||||
store.dispatch(setSettings(env.settings, true));
|
||||
|
||||
if (env.servers) {
|
||||
store.dispatch({
|
||||
|
|
|
@ -37,6 +37,7 @@ export const SET_CERT = 'SET_CERT';
|
|||
export const SET_CERT_ERROR = 'SET_CERT_ERROR';
|
||||
export const SET_KEY = 'SET_KEY';
|
||||
export const UPLOAD_CERT = 'UPLOAD_CERT';
|
||||
export const SETTINGS_SET = 'SETTINGS_SET';
|
||||
|
||||
export const SELECT_TAB = 'SELECT_TAB';
|
||||
|
||||
|
|
|
@ -200,8 +200,8 @@ export function setServerName(name, server) {
|
|||
server
|
||||
},
|
||||
debounce: {
|
||||
delay: 1000,
|
||||
key: server
|
||||
delay: 500,
|
||||
key: `server_name:${server}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import assign from 'lodash/assign';
|
||||
import createReducer from 'utils/createReducer';
|
||||
import * as actions from './actions';
|
||||
|
||||
|
@ -36,6 +37,14 @@ export default createReducer(
|
|||
[actions.SET_KEY](state, action) {
|
||||
state.keyFile = action.fileName;
|
||||
state.key = action.key;
|
||||
},
|
||||
|
||||
[actions.SETTINGS_SET](state, { key, value, settings }) {
|
||||
if (settings) {
|
||||
assign(state, settings);
|
||||
} else {
|
||||
state[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -82,3 +91,37 @@ export function setKey(fileName, key) {
|
|||
key: key
|
||||
};
|
||||
}
|
||||
|
||||
export function setSetting(key, value) {
|
||||
return {
|
||||
type: actions.SETTINGS_SET,
|
||||
key,
|
||||
value,
|
||||
socket: {
|
||||
type: 'settings_set',
|
||||
data: {
|
||||
[key]: value
|
||||
},
|
||||
debounce: {
|
||||
delay: 250,
|
||||
key: `settings:${key}`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function setSettings(settings, local = false) {
|
||||
const action = {
|
||||
type: actions.SETTINGS_SET,
|
||||
settings
|
||||
};
|
||||
|
||||
if (!local) {
|
||||
action.socket = {
|
||||
type: 'settings_set',
|
||||
data: settings
|
||||
};
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue