-
Certificate
-
+
+
+
+
Settings
+
+
Visuals
+ setSetting('coloredNicks', e.target.checked)}
+ />
+
+
+
Client Certificate
+
+
+
+
+ {error ?
{error}
: null}
+
+
-
-
- {error ?
{error}
: null}
);
};
diff --git a/client/src/js/components/ui/Checkbox.js b/client/src/js/components/ui/Checkbox.js
index a9933d7c..b975e4af 100644
--- a/client/src/js/components/ui/Checkbox.js
+++ b/client/src/js/components/ui/Checkbox.js
@@ -1,30 +1,18 @@
import React from 'react';
-import { Field } from 'formik';
+import classnames from 'classnames';
-const Checkbox = ({ name, label, onChange, ...props }) => (
-
(
-
- )}
- />
+const Checkbox = ({ name, label, topLabel, ...props }) => (
+
);
export default Checkbox;
diff --git a/client/src/js/components/ui/formik/Checkbox.js b/client/src/js/components/ui/formik/Checkbox.js
new file mode 100644
index 00000000..ca1f14e1
--- /dev/null
+++ b/client/src/js/components/ui/formik/Checkbox.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import { Field } from 'formik';
+import Checkbox from 'components/ui/Checkbox';
+
+const FormikCheckbox = ({ name, onChange, ...props }) => (
+ (
+ {
+ form.setFieldTouched(name, true);
+ field.onChange(e);
+ if (onChange) {
+ onChange(e);
+ }
+ }}
+ {...props}
+ />
+ )}
+ />
+);
+
+export default FormikCheckbox;
diff --git a/client/src/js/components/ui/formik/TextInput.js b/client/src/js/components/ui/formik/TextInput.js
new file mode 100644
index 00000000..e69de29b
diff --git a/client/src/js/containers/Chat.js b/client/src/js/containers/Chat.js
index 54435be6..514aa512 100644
--- a/client/src/js/containers/Chat.js
+++ b/client/src/js/containers/Chat.js
@@ -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 => ({
diff --git a/client/src/js/containers/Settings.js b/client/src/js/containers/Settings.js
index 92cc592f..dbd1b635 100644
--- a/client/src/js/containers/Settings.js
+++ b/client/src/js/containers/Settings.js
@@ -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(
diff --git a/client/src/js/modules/initialState.js b/client/src/js/modules/initialState.js
index 35affc0f..1ba8cc99 100644
--- a/client/src/js/modules/initialState.js
+++ b/client/src/js/modules/initialState.js
@@ -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({
diff --git a/client/src/js/state/actions.js b/client/src/js/state/actions.js
index 4b68fce1..6a9eb94b 100644
--- a/client/src/js/state/actions.js
+++ b/client/src/js/state/actions.js
@@ -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';
diff --git a/client/src/js/state/servers.js b/client/src/js/state/servers.js
index c98fc3c2..7aa00111 100644
--- a/client/src/js/state/servers.js
+++ b/client/src/js/state/servers.js
@@ -200,8 +200,8 @@ export function setServerName(name, server) {
server
},
debounce: {
- delay: 1000,
- key: server
+ delay: 500,
+ key: `server_name:${server}`
}
};
}
diff --git a/client/src/js/state/settings.js b/client/src/js/state/settings.js
index f881fcb8..21a58a24 100644
--- a/client/src/js/state/settings.js
+++ b/client/src/js/state/settings.js
@@ -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;
+}
diff --git a/server/index_data.go b/server/index_data.go
index 4137758e..976d5bf8 100644
--- a/server/index_data.go
+++ b/server/index_data.go
@@ -27,6 +27,8 @@ type indexData struct {
Channels []*storage.Channel
HexIP bool
+ Settings *storage.ClientSettings
+
// Users in the selected channel
Users *Userlist
@@ -54,6 +56,8 @@ func getIndexData(r *http.Request, state *State) *indexData {
return &data
}
+ data.Settings = state.user.GetClientSettings()
+
servers, err := state.user.GetServers()
if err != nil {
return nil
diff --git a/server/index_data_easyjson.go b/server/index_data_easyjson.go
index 9b006d12..49adaf2f 100644
--- a/server/index_data_easyjson.go
+++ b/server/index_data_easyjson.go
@@ -99,6 +99,16 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchServer(in *jlexer.Lexer, out
}
case "hexIP":
out.HexIP = bool(in.Bool())
+ case "settings":
+ if in.IsNull() {
+ in.Skip()
+ out.Settings = nil
+ } else {
+ if out.Settings == nil {
+ out.Settings = new(storage.ClientSettings)
+ }
+ easyjson7e607aefDecodeGithubComKhliengDispatchStorage1(in, &*out.Settings)
+ }
case "users":
if in.IsNull() {
in.Skip()
@@ -199,6 +209,16 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchServer(out *jwriter.Writer, i
}
out.Bool(bool(in.HexIP))
}
+ if in.Settings != nil {
+ const prefix string = ",\"settings\":"
+ if first {
+ first = false
+ out.RawString(prefix[1:])
+ } else {
+ out.RawString(prefix)
+ }
+ easyjson7e607aefEncodeGithubComKhliengDispatchStorage1(out, *in.Settings)
+ }
if in.Users != nil {
const prefix string = ",\"users\":"
if first {
@@ -245,6 +265,53 @@ func (v *indexData) UnmarshalJSON(data []byte) error {
func (v *indexData) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjson7e607aefDecodeGithubComKhliengDispatchServer(l, v)
}
+func easyjson7e607aefDecodeGithubComKhliengDispatchStorage1(in *jlexer.Lexer, out *storage.ClientSettings) {
+ isTopLevel := in.IsStart()
+ if in.IsNull() {
+ if isTopLevel {
+ in.Consumed()
+ }
+ in.Skip()
+ return
+ }
+ in.Delim('{')
+ for !in.IsDelim('}') {
+ key := in.UnsafeString()
+ in.WantColon()
+ if in.IsNull() {
+ in.Skip()
+ in.WantComma()
+ continue
+ }
+ switch key {
+ case "coloredNicks":
+ out.ColoredNicks = bool(in.Bool())
+ default:
+ in.SkipRecursive()
+ }
+ in.WantComma()
+ }
+ in.Delim('}')
+ if isTopLevel {
+ in.Consumed()
+ }
+}
+func easyjson7e607aefEncodeGithubComKhliengDispatchStorage1(out *jwriter.Writer, in storage.ClientSettings) {
+ out.RawByte('{')
+ first := true
+ _ = first
+ if in.ColoredNicks {
+ const prefix string = ",\"coloredNicks\":"
+ if first {
+ first = false
+ out.RawString(prefix[1:])
+ } else {
+ out.RawString(prefix)
+ }
+ out.Bool(bool(in.ColoredNicks))
+ }
+ out.RawByte('}')
+}
func easyjson7e607aefDecodeGithubComKhliengDispatchStorage(in *jlexer.Lexer, out *storage.Channel) {
isTopLevel := in.IsStart()
if in.IsNull() {
diff --git a/server/websocket_handler.go b/server/websocket_handler.go
index 858daffd..996ef383 100644
--- a/server/websocket_handler.go
+++ b/server/websocket_handler.go
@@ -274,6 +274,13 @@ func (h *wsHandler) setServerName(b []byte) {
}
}
+func (h *wsHandler) setSettings(b []byte) {
+ err := h.state.user.UnmarshalClientSettingsJSON(b)
+ if err != nil {
+ log.Println(err)
+ }
+}
+
func (h *wsHandler) initHandlers() {
h.handlers = map[string]func([]byte){
"connect": h.connect,
@@ -293,6 +300,7 @@ func (h *wsHandler) initHandlers() {
"cert": h.cert,
"fetch_messages": h.fetchMessages,
"set_server_name": h.setServerName,
+ "settings_set": h.setSettings,
}
}
diff --git a/storage/storage.schema b/storage/storage.schema
index c6d597af..ca429c17 100644
--- a/storage/storage.schema
+++ b/storage/storage.schema
@@ -1,7 +1,12 @@
struct User {
- ID uint64
- Username string
- lastIP []byte
+ ID uint64
+ Username string
+ clientSettings *ClientSettings
+ lastIP []byte
+}
+
+struct ClientSettings {
+ ColoredNicks bool
}
struct Server {
diff --git a/storage/storage.schema.gen.go b/storage/storage.schema.gen.go
index a5571f11..52eb1f47 100644
--- a/storage/storage.schema.gen.go
+++ b/storage/storage.schema.gen.go
@@ -29,6 +29,15 @@ func (d *User) Size() (s uint64) {
}
s += l
}
+ {
+ if d.clientSettings != nil {
+
+ {
+ s += (*d.clientSettings).Size()
+ }
+ s += 0
+ }
+ }
{
l := uint64(len(d.lastIP))
@@ -44,7 +53,7 @@ func (d *User) Size() (s uint64) {
}
s += l
}
- s += 8
+ s += 9
return
}
func (d *User) Marshal(buf []byte) ([]byte, error) {
@@ -82,6 +91,22 @@ func (d *User) Marshal(buf []byte) ([]byte, error) {
copy(buf[i+8:], d.Username)
i += l
}
+ {
+ if d.clientSettings == nil {
+ buf[i+8] = 0
+ } else {
+ buf[i+8] = 1
+
+ {
+ nbuf, err := (*d.clientSettings).Marshal(buf[i+9:])
+ if err != nil {
+ return nil, err
+ }
+ i += uint64(len(nbuf))
+ }
+ i += 0
+ }
+ }
{
l := uint64(len(d.lastIP))
@@ -90,18 +115,18 @@ func (d *User) Marshal(buf []byte) ([]byte, error) {
t := uint64(l)
for t >= 0x80 {
- buf[i+8] = byte(t) | 0x80
+ buf[i+9] = byte(t) | 0x80
t >>= 7
i++
}
- buf[i+8] = byte(t)
+ buf[i+9] = byte(t)
i++
}
- copy(buf[i+8:], d.lastIP)
+ copy(buf[i+9:], d.lastIP)
i += l
}
- return buf[:i+8], nil
+ return buf[:i+9], nil
}
func (d *User) Unmarshal(buf []byte) (uint64, error) {
@@ -132,16 +157,34 @@ func (d *User) Unmarshal(buf []byte) (uint64, error) {
d.Username = string(buf[i+8 : i+8+l])
i += l
}
+ {
+ if buf[i+8] == 1 {
+ if d.clientSettings == nil {
+ d.clientSettings = new(ClientSettings)
+ }
+
+ {
+ ni, err := (*d.clientSettings).Unmarshal(buf[i+9:])
+ if err != nil {
+ return 0, err
+ }
+ i += ni
+ }
+ i += 0
+ } else {
+ d.clientSettings = nil
+ }
+ }
{
l := uint64(0)
{
bs := uint8(7)
- t := uint64(buf[i+8] & 0x7F)
- for buf[i+8]&0x80 == 0x80 {
+ t := uint64(buf[i+9] & 0x7F)
+ for buf[i+9]&0x80 == 0x80 {
i++
- t |= uint64(buf[i+8]&0x7F) << bs
+ t |= uint64(buf[i+9]&0x7F) << bs
bs += 7
}
i++
@@ -154,10 +197,45 @@ func (d *User) Unmarshal(buf []byte) (uint64, error) {
} else {
d.lastIP = make([]byte, l)
}
- copy(d.lastIP, buf[i+8:])
+ copy(d.lastIP, buf[i+9:])
i += l
}
- return i + 8, nil
+ return i + 9, nil
+}
+
+func (d *ClientSettings) Size() (s uint64) {
+
+ s += 1
+ return
+}
+func (d *ClientSettings) Marshal(buf []byte) ([]byte, error) {
+ size := d.Size()
+ {
+ if uint64(cap(buf)) >= size {
+ buf = buf[:size]
+ } else {
+ buf = make([]byte, size)
+ }
+ }
+ i := uint64(0)
+
+ {
+ if d.ColoredNicks {
+ buf[0] = 1
+ } else {
+ buf[0] = 0
+ }
+ }
+ return buf[:i+1], nil
+}
+
+func (d *ClientSettings) Unmarshal(buf []byte) (uint64, error) {
+ i := uint64(0)
+
+ {
+ d.ColoredNicks = buf[0] == 1
+ }
+ return i + 1, nil
}
func (d *Server) Size() (s uint64) {
diff --git a/storage/user.go b/storage/user.go
index 48df720a..30757422 100644
--- a/storage/user.go
+++ b/storage/user.go
@@ -12,16 +12,20 @@ type User struct {
IDBytes []byte
Username string
- store Store
- messageLog MessageStore
- messageIndex MessageSearchProvider
- lastIP []byte
- certificate *tls.Certificate
- lock sync.Mutex
+ store Store
+ messageLog MessageStore
+ messageIndex MessageSearchProvider
+ clientSettings *ClientSettings
+ lastIP []byte
+ certificate *tls.Certificate
+ lock sync.Mutex
}
func NewUser(store Store) (*User, error) {
- user := &User{store: store}
+ user := &User{
+ store: store,
+ clientSettings: DefaultClientSettings(),
+ }
err := store.SaveUser(user)
if err != nil {
@@ -84,6 +88,44 @@ func (u *User) SetLastIP(ip []byte) error {
return u.store.SaveUser(u)
}
+//easyjson:json
+type ClientSettings struct {
+ ColoredNicks bool
+}
+
+func DefaultClientSettings() *ClientSettings {
+ return &ClientSettings{
+ ColoredNicks: true,
+ }
+}
+
+func (u *User) GetClientSettings() *ClientSettings {
+ u.lock.Lock()
+ settings := *u.clientSettings
+ u.lock.Unlock()
+ return &settings
+}
+
+func (u *User) SetClientSettings(settings *ClientSettings) error {
+ u.lock.Lock()
+ u.clientSettings = settings
+ u.lock.Unlock()
+
+ return u.store.SaveUser(u)
+}
+
+func (u *User) UnmarshalClientSettingsJSON(b []byte) error {
+ u.lock.Lock()
+ err := u.clientSettings.UnmarshalJSON(b)
+ u.lock.Unlock()
+
+ if err != nil {
+ return err
+ }
+
+ return u.store.SaveUser(u)
+}
+
type Server struct {
Name string
Host string
diff --git a/storage/user_easyjson.go b/storage/user_easyjson.go
new file mode 100644
index 00000000..ee31dd22
--- /dev/null
+++ b/storage/user_easyjson.go
@@ -0,0 +1,90 @@
+// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
+
+package storage
+
+import (
+ json "encoding/json"
+ easyjson "github.com/mailru/easyjson"
+ jlexer "github.com/mailru/easyjson/jlexer"
+ jwriter "github.com/mailru/easyjson/jwriter"
+)
+
+// suppress unused package warning
+var (
+ _ *json.RawMessage
+ _ *jlexer.Lexer
+ _ *jwriter.Writer
+ _ easyjson.Marshaler
+)
+
+func easyjson9e1087fdDecodeGithubComKhliengDispatchStorage(in *jlexer.Lexer, out *ClientSettings) {
+ isTopLevel := in.IsStart()
+ if in.IsNull() {
+ if isTopLevel {
+ in.Consumed()
+ }
+ in.Skip()
+ return
+ }
+ in.Delim('{')
+ for !in.IsDelim('}') {
+ key := in.UnsafeString()
+ in.WantColon()
+ if in.IsNull() {
+ in.Skip()
+ in.WantComma()
+ continue
+ }
+ switch key {
+ case "coloredNicks":
+ out.ColoredNicks = bool(in.Bool())
+ default:
+ in.SkipRecursive()
+ }
+ in.WantComma()
+ }
+ in.Delim('}')
+ if isTopLevel {
+ in.Consumed()
+ }
+}
+func easyjson9e1087fdEncodeGithubComKhliengDispatchStorage(out *jwriter.Writer, in ClientSettings) {
+ out.RawByte('{')
+ first := true
+ _ = first
+ if in.ColoredNicks {
+ const prefix string = ",\"coloredNicks\":"
+ if first {
+ first = false
+ out.RawString(prefix[1:])
+ } else {
+ out.RawString(prefix)
+ }
+ out.Bool(bool(in.ColoredNicks))
+ }
+ out.RawByte('}')
+}
+
+// MarshalJSON supports json.Marshaler interface
+func (v ClientSettings) MarshalJSON() ([]byte, error) {
+ w := jwriter.Writer{}
+ easyjson9e1087fdEncodeGithubComKhliengDispatchStorage(&w, v)
+ return w.Buffer.BuildBytes(), w.Error
+}
+
+// MarshalEasyJSON supports easyjson.Marshaler interface
+func (v ClientSettings) MarshalEasyJSON(w *jwriter.Writer) {
+ easyjson9e1087fdEncodeGithubComKhliengDispatchStorage(w, v)
+}
+
+// UnmarshalJSON supports json.Unmarshaler interface
+func (v *ClientSettings) UnmarshalJSON(data []byte) error {
+ r := jlexer.Lexer{Data: data}
+ easyjson9e1087fdDecodeGithubComKhliengDispatchStorage(&r, v)
+ return r.Error()
+}
+
+// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
+func (v *ClientSettings) UnmarshalEasyJSON(l *jlexer.Lexer) {
+ easyjson9e1087fdDecodeGithubComKhliengDispatchStorage(l, v)
+}
diff --git a/storage/user_test.go b/storage/user_test.go
index 7b04885a..3ebcd6bd 100644
--- a/storage/user_test.go
+++ b/storage/user_test.go
@@ -80,6 +80,16 @@ func TestUser(t *testing.T) {
channels, err = user.GetChannels()
assert.Len(t, channels, 0)
+ settings := user.GetClientSettings()
+ assert.NotNil(t, settings)
+ assert.Equal(t, storage.DefaultClientSettings(), settings)
+
+ settings.ColoredNicks = !settings.ColoredNicks
+ err = user.SetClientSettings(settings)
+ assert.Nil(t, err)
+ assert.Equal(t, settings, user.GetClientSettings())
+ assert.NotEqual(t, settings, storage.DefaultClientSettings())
+
user.Remove()
_, err = os.Stat(storage.Path.User(user.Username))
assert.True(t, os.IsNotExist(err))