Add colored nicks settings option

This commit is contained in:
Ken-Håvard Lieng 2018-10-15 08:56:17 +02:00
parent ec03db4db6
commit 6c6a9e12cf
27 changed files with 577 additions and 109 deletions

File diff suppressed because one or more lines are too long

View File

@ -81,7 +81,7 @@
"test:watch": "jest --watch", "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: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: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" "gen:template": "egon -s -m ../server"
}, },
"jest": { "jest": {

View File

@ -7,6 +7,7 @@
body { body {
font-family: Roboto Mono, monospace; font-family: Roboto Mono, monospace;
background: #f0f0f0; background: #f0f0f0;
color: #222;
} }
h1, h1,
@ -16,11 +17,6 @@ h4,
h5, h5,
h6 { h6 {
font-family: Montserrat, sans-serif; font-family: Montserrat, sans-serif;
font-weight: 400;
}
h1 {
font-weight: 700;
} }
input { input {
@ -28,6 +24,7 @@ input {
border: none; border: none;
outline: none; outline: none;
background: #fff; background: #fff;
color: #222;
} }
input::-ms-clear { input::-ms-clear {
@ -54,26 +51,44 @@ button:active {
background: #6bb758; 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; position: absolute;
left: -99999px; left: -99999px;
opacity: 0; opacity: 0;
} }
input[type='checkbox'] + span { .checkbox span {
width: 20px; width: 20px;
height: 20px; height: 20px;
border: 2px solid #777; border: 2px solid #777;
display: inline-block;
position: relative; position: relative;
} }
input[type='checkbox']:checked + span { .checkbox:not(.top-label) span {
margin-right: 10px;
}
.checkbox input:checked + span {
background: #6bb758; background: #6bb758;
border-color: #6bb758; border-color: #6bb758;
} }
input[type='checkbox']:checked + span:before { .checkbox input:checked + span:before {
content: ''; content: '';
width: 5px; width: 5px;
height: 10px; height: 10px;
@ -456,6 +471,8 @@ input::-webkit-inner-spin-button {
margin-left: 10px; margin-left: 10px;
padding: 0 5px; padding: 0 5px;
font: 24px Montserrat, sans-serif; font: 24px Montserrat, sans-serif;
font-weight: 700;
color: #222;
white-space: nowrap; white-space: nowrap;
line-height: 50px; line-height: 50px;
} }
@ -692,48 +709,86 @@ input.message-input-nick.invalid {
background: #ddd; background: #ddd;
} }
.settings { .settings-container {
text-align: center; display: flex;
justify-content: center;
overflow: auto; overflow: auto;
height: 100%;
} }
.settings p { .settings {
color: #999; 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 { .settings h1 {
text-align: center;
margin: 20px; margin: 20px;
} }
.settings h2 { .settings h2 {
margin: 15px; font-weight: 700;
color: #222;
} }
.settings button { .settings button {
margin: 5px;
width: 200px; width: 200px;
} }
.settings div {
display: inline-block;
}
.settings .error { .settings .error {
margin: 10px; margin-top: 15px;
color: #f6546a; color: #f6546a;
text-align: center;
} }
.input-file { .input-file {
color: #fff; color: #fff;
background: #222 !important; background: #222 !important;
padding: 10px; padding: 10px;
margin: 5px;
width: 200px; width: 200px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; 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 { .ReactVirtualized__List {
box-sizing: content-box !important; box-sizing: content-box !important;
outline: none; outline: none;
@ -813,4 +868,8 @@ input.message-input-nick.invalid {
margin: auto 50px; margin: auto 50px;
max-width: 400px; max-width: 400px;
} }
.settings-section {
margin-left: 50px;
}
} }

View File

@ -50,6 +50,7 @@ export default class Chat extends Component {
render() { render() {
const { const {
channel, channel,
coloredNicks,
currentInputHistoryEntry, currentInputHistoryEntry,
hasMoreMessages, hasMoreMessages,
messages, messages,
@ -69,7 +70,6 @@ export default class Chat extends Component {
toggleSearch, toggleSearch,
toggleUserList toggleUserList
} = this.props; } = this.props;
let chatClass; let chatClass;
if (isChannel(tab)) { if (isChannel(tab)) {
chatClass = 'chat-channel'; chatClass = 'chat-channel';
@ -93,6 +93,7 @@ export default class Chat extends Component {
/> />
<Search search={search} onSearch={this.handleSearch} /> <Search search={search} onSearch={this.handleSearch} />
<MessageBox <MessageBox
coloredNicks={coloredNicks}
hasMoreMessages={hasMoreMessages} hasMoreMessages={hasMoreMessages}
messages={messages} messages={messages}
tab={tab} tab={tab}
@ -111,6 +112,7 @@ export default class Chat extends Component {
{...inputActions} {...inputActions}
/> />
<UserList <UserList
coloredNicks={coloredNicks}
showUserList={showUserList} showUserList={showUserList}
users={users} users={users}
onNickClick={this.handleNickClick} onNickClick={this.handleNickClick}

View File

@ -6,7 +6,7 @@ export default class Message extends PureComponent {
handleNickClick = () => this.props.onNickClick(this.props.message.from); handleNickClick = () => this.props.onNickClick(this.props.message.from);
render() { render() {
const { message } = this.props; const { message, coloredNick } = this.props;
const className = classnames('message', { const className = classnames('message', {
[`message-${message.type}`]: message.type [`message-${message.type}`]: message.type
@ -19,7 +19,7 @@ export default class Message extends PureComponent {
}; };
const senderStyle = {}; const senderStyle = {};
if (message.from) { if (message.from && coloredNick) {
senderStyle.color = stringToRGB(message.from); senderStyle.color = stringToRGB(message.from);
} }

View File

@ -176,13 +176,14 @@ export default class MessageBox extends PureComponent {
return null; return null;
} }
const { messages, onNickClick } = this.props; const { messages, coloredNicks, onNickClick } = this.props;
const message = messages[index - 1]; const message = messages[index - 1];
return ( return (
<Message <Message
key={message.id} key={message.id}
message={message} message={message}
coloredNick={coloredNicks}
style={style} style={style}
onNickClick={onNickClick} onNickClick={onNickClick}
/> />

View File

@ -16,12 +16,13 @@ export default class UserList extends PureComponent {
}; };
renderUser = ({ index, style, key }) => { renderUser = ({ index, style, key }) => {
const { users, onNickClick } = this.props; const { users, coloredNicks, onNickClick } = this.props;
return ( return (
<UserListItem <UserListItem
key={key} key={key}
user={users[index]} user={users[index]}
coloredNick={coloredNicks}
style={style} style={style}
onClick={onNickClick} onClick={onNickClick}
/> />

View File

@ -5,11 +5,15 @@ export default class UserListItem extends PureComponent {
handleClick = () => this.props.onClick(this.props.user.nick); handleClick = () => this.props.onClick(this.props.user.nick);
render() { render() {
const { user } = this.props; const { user, coloredNick } = this.props;
const style = { let { style } = this.props;
if (coloredNick) {
style = {
color: stringToRGB(user.nick), color: stringToRGB(user.nick),
...this.props.style ...style
}; };
}
return ( return (
<p style={style} onClick={this.handleClick}> <p style={style} onClick={this.handleClick}>

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { Form, withFormik } from 'formik'; import { Form, withFormik } from 'formik';
import Navicon from 'containers/Navicon'; 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 TextInput from 'components/ui/TextInput';
import Error from 'components/ui/formik/Error'; import Error from 'components/ui/formik/Error';
import { isValidNick, isValidChannel, isValidUsername, isInt } from 'utils'; import { isValidNick, isValidChannel, isValidUsername, isInt } from 'utils';
@ -77,7 +77,12 @@ class Connect extends Component {
<div className="connect-form-address"> <div className="connect-form-address">
<TextInput name="host" placeholder="Host" /> <TextInput name="host" placeholder="Host" />
<TextInput name="port" type="number" placeholder="Port" /> <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> </div>
<Error name="host" /> <Error name="host" />
<Error name="port" /> <Error name="port" />

View File

@ -1,33 +1,57 @@
import React from 'react'; import React from 'react';
import Navicon from 'containers/Navicon'; import Navicon from 'containers/Navicon';
import Checkbox from 'components/ui/Checkbox';
import FileInput from 'components/ui/FileInput'; 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 status = settings.uploadingCert ? 'Uploading...' : 'Upload';
const error = settings.certError; const error = settings.certError;
return ( return (
<div className="settings-container">
<div className="settings"> <div className="settings">
<Navicon /> <Navicon />
<h1>Settings</h1> <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> <h2>Client Certificate</h2>
<div> <div className="settings-cert">
<div className="settings-file">
<p>Certificate</p> <p>Certificate</p>
<FileInput <FileInput
name={settings.certFile || 'Select Certificate'} name={settings.certFile || 'Select Certificate'}
onChange={onCertChange} onChange={onCertChange}
/> />
</div> </div>
<div> <div className="settings-file">
<p>Private Key</p> <p>Private Key</p>
<FileInput <FileInput
name={settings.keyFile || 'Select Key'} name={settings.keyFile || 'Select Key'}
onChange={onKeyChange} onChange={onKeyChange}
/> />
</div> </div>
<button onClick={uploadCert}>{status}</button> <button className="settings-button" onClick={uploadCert}>
{status}
</button>
{error ? <p className="error">{error}</p> : null} {error ? <p className="error">{error}</p> : null}
</div> </div>
</div>
</div>
</div>
); );
}; };

View File

@ -1,30 +1,18 @@
import React from 'react'; import React from 'react';
import { Field } from 'formik'; import classnames from 'classnames';
const Checkbox = ({ name, label, onChange, ...props }) => ( const Checkbox = ({ name, label, topLabel, ...props }) => (
<Field <label
name={name} className={classnames('checkbox', {
render={({ field, form }) => ( 'top-label': topLabel
<label htmlFor={name}> })}
{label && <div>{label}</div>} htmlFor={name}
<input >
type="checkbox" {topLabel && label}
id={name} <input type="checkbox" id={name} name={name} {...props} />
name={name}
checked={field.value}
onChange={e => {
form.setFieldTouched(name, true);
field.onChange(e);
if (onChange) {
onChange(e);
}
}}
{...props}
/>
<span /> <span />
{!topLabel && label}
</label> </label>
)}
/>
); );
export default Checkbox; export default Checkbox;

View 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;

View File

@ -32,6 +32,7 @@ import {
setNick, setNick,
setServerName setServerName
} from 'state/servers'; } from 'state/servers';
import { getSettings } from 'state/settings';
import { getSelectedTab, select } from 'state/tab'; import { getSelectedTab, select } from 'state/tab';
import { getShowUserList, toggleUserList } from 'state/ui'; import { getShowUserList, toggleUserList } from 'state/ui';
@ -46,7 +47,8 @@ const mapState = createStructuredSelector({
status: getCurrentServerStatus, status: getCurrentServerStatus,
tab: getSelectedTab, tab: getSelectedTab,
title: getSelectedTabTitle, title: getSelectedTabTitle,
users: getSelectedChannelUsers users: getSelectedChannelUsers,
coloredNicks: state => getSettings(state).coloredNicks
}); });
const mapDispatch = dispatch => ({ const mapDispatch = dispatch => ({

View File

@ -1,7 +1,13 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect'; import { createStructuredSelector } from 'reselect';
import Settings from 'components/pages/Settings'; 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({ const mapState = createStructuredSelector({
settings: getSettings settings: getSettings
@ -10,7 +16,8 @@ const mapState = createStructuredSelector({
const mapDispatch = { const mapDispatch = {
onCertChange: setCert, onCertChange: setCert,
onKeyChange: setKey, onKeyChange: setKey,
uploadCert uploadCert,
setSetting
}; };
export default connect( export default connect(

View File

@ -2,6 +2,7 @@ import Cookie from 'js-cookie';
import { socket as socketActions } from 'state/actions'; import { socket as socketActions } from 'state/actions';
import { getWrapWidth, setConnectDefaults, appSet } from 'state/app'; import { getWrapWidth, setConnectDefaults, appSet } from 'state/app';
import { addMessages } from 'state/messages'; import { addMessages } from 'state/messages';
import { setSettings } from 'state/settings';
import { select, updateSelection } from 'state/tab'; import { select, updateSelection } from 'state/tab';
import { find } from 'utils'; import { find } from 'utils';
import { when } from 'utils/observe'; import { when } from 'utils/observe';
@ -12,6 +13,7 @@ export default function initialState({ store }) {
store.dispatch(setConnectDefaults(env.defaults)); store.dispatch(setConnectDefaults(env.defaults));
store.dispatch(appSet('hexIP', env.hexIP)); store.dispatch(appSet('hexIP', env.hexIP));
store.dispatch(setSettings(env.settings, true));
if (env.servers) { if (env.servers) {
store.dispatch({ store.dispatch({

View File

@ -37,6 +37,7 @@ export const SET_CERT = 'SET_CERT';
export const SET_CERT_ERROR = 'SET_CERT_ERROR'; export const SET_CERT_ERROR = 'SET_CERT_ERROR';
export const SET_KEY = 'SET_KEY'; export const SET_KEY = 'SET_KEY';
export const UPLOAD_CERT = 'UPLOAD_CERT'; export const UPLOAD_CERT = 'UPLOAD_CERT';
export const SETTINGS_SET = 'SETTINGS_SET';
export const SELECT_TAB = 'SELECT_TAB'; export const SELECT_TAB = 'SELECT_TAB';

View File

@ -200,8 +200,8 @@ export function setServerName(name, server) {
server server
}, },
debounce: { debounce: {
delay: 1000, delay: 500,
key: server key: `server_name:${server}`
} }
}; };
} }

View File

@ -1,3 +1,4 @@
import assign from 'lodash/assign';
import createReducer from 'utils/createReducer'; import createReducer from 'utils/createReducer';
import * as actions from './actions'; import * as actions from './actions';
@ -36,6 +37,14 @@ export default createReducer(
[actions.SET_KEY](state, action) { [actions.SET_KEY](state, action) {
state.keyFile = action.fileName; state.keyFile = action.fileName;
state.key = action.key; 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 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;
}

View File

@ -27,6 +27,8 @@ type indexData struct {
Channels []*storage.Channel Channels []*storage.Channel
HexIP bool HexIP bool
Settings *storage.ClientSettings
// Users in the selected channel // Users in the selected channel
Users *Userlist Users *Userlist
@ -54,6 +56,8 @@ func getIndexData(r *http.Request, state *State) *indexData {
return &data return &data
} }
data.Settings = state.user.GetClientSettings()
servers, err := state.user.GetServers() servers, err := state.user.GetServers()
if err != nil { if err != nil {
return nil return nil

View File

@ -99,6 +99,16 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchServer(in *jlexer.Lexer, out
} }
case "hexIP": case "hexIP":
out.HexIP = bool(in.Bool()) 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": case "users":
if in.IsNull() { if in.IsNull() {
in.Skip() in.Skip()
@ -199,6 +209,16 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchServer(out *jwriter.Writer, i
} }
out.Bool(bool(in.HexIP)) 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 { if in.Users != nil {
const prefix string = ",\"users\":" const prefix string = ",\"users\":"
if first { if first {
@ -245,6 +265,53 @@ func (v *indexData) UnmarshalJSON(data []byte) error {
func (v *indexData) UnmarshalEasyJSON(l *jlexer.Lexer) { func (v *indexData) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjson7e607aefDecodeGithubComKhliengDispatchServer(l, v) 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) { func easyjson7e607aefDecodeGithubComKhliengDispatchStorage(in *jlexer.Lexer, out *storage.Channel) {
isTopLevel := in.IsStart() isTopLevel := in.IsStart()
if in.IsNull() { if in.IsNull() {

View File

@ -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() { func (h *wsHandler) initHandlers() {
h.handlers = map[string]func([]byte){ h.handlers = map[string]func([]byte){
"connect": h.connect, "connect": h.connect,
@ -293,6 +300,7 @@ func (h *wsHandler) initHandlers() {
"cert": h.cert, "cert": h.cert,
"fetch_messages": h.fetchMessages, "fetch_messages": h.fetchMessages,
"set_server_name": h.setServerName, "set_server_name": h.setServerName,
"settings_set": h.setSettings,
} }
} }

View File

@ -1,9 +1,14 @@
struct User { struct User {
ID uint64 ID uint64
Username string Username string
clientSettings *ClientSettings
lastIP []byte lastIP []byte
} }
struct ClientSettings {
ColoredNicks bool
}
struct Server { struct Server {
Name string Name string
Host string Host string

View File

@ -29,6 +29,15 @@ func (d *User) Size() (s uint64) {
} }
s += l s += l
} }
{
if d.clientSettings != nil {
{
s += (*d.clientSettings).Size()
}
s += 0
}
}
{ {
l := uint64(len(d.lastIP)) l := uint64(len(d.lastIP))
@ -44,7 +53,7 @@ func (d *User) Size() (s uint64) {
} }
s += l s += l
} }
s += 8 s += 9
return return
} }
func (d *User) Marshal(buf []byte) ([]byte, error) { 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) copy(buf[i+8:], d.Username)
i += l 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)) l := uint64(len(d.lastIP))
@ -90,18 +115,18 @@ func (d *User) Marshal(buf []byte) ([]byte, error) {
t := uint64(l) t := uint64(l)
for t >= 0x80 { for t >= 0x80 {
buf[i+8] = byte(t) | 0x80 buf[i+9] = byte(t) | 0x80
t >>= 7 t >>= 7
i++ i++
} }
buf[i+8] = byte(t) buf[i+9] = byte(t)
i++ i++
} }
copy(buf[i+8:], d.lastIP) copy(buf[i+9:], d.lastIP)
i += l i += l
} }
return buf[:i+8], nil return buf[:i+9], nil
} }
func (d *User) Unmarshal(buf []byte) (uint64, error) { 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]) d.Username = string(buf[i+8 : i+8+l])
i += 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) l := uint64(0)
{ {
bs := uint8(7) bs := uint8(7)
t := uint64(buf[i+8] & 0x7F) t := uint64(buf[i+9] & 0x7F)
for buf[i+8]&0x80 == 0x80 { for buf[i+9]&0x80 == 0x80 {
i++ i++
t |= uint64(buf[i+8]&0x7F) << bs t |= uint64(buf[i+9]&0x7F) << bs
bs += 7 bs += 7
} }
i++ i++
@ -154,10 +197,45 @@ func (d *User) Unmarshal(buf []byte) (uint64, error) {
} else { } else {
d.lastIP = make([]byte, l) d.lastIP = make([]byte, l)
} }
copy(d.lastIP, buf[i+8:]) copy(d.lastIP, buf[i+9:])
i += l 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) { func (d *Server) Size() (s uint64) {

View File

@ -15,13 +15,17 @@ type User struct {
store Store store Store
messageLog MessageStore messageLog MessageStore
messageIndex MessageSearchProvider messageIndex MessageSearchProvider
clientSettings *ClientSettings
lastIP []byte lastIP []byte
certificate *tls.Certificate certificate *tls.Certificate
lock sync.Mutex lock sync.Mutex
} }
func NewUser(store Store) (*User, error) { func NewUser(store Store) (*User, error) {
user := &User{store: store} user := &User{
store: store,
clientSettings: DefaultClientSettings(),
}
err := store.SaveUser(user) err := store.SaveUser(user)
if err != nil { if err != nil {
@ -84,6 +88,44 @@ func (u *User) SetLastIP(ip []byte) error {
return u.store.SaveUser(u) 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 { type Server struct {
Name string Name string
Host string Host string

90
storage/user_easyjson.go Normal file
View File

@ -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)
}

View File

@ -80,6 +80,16 @@ func TestUser(t *testing.T) {
channels, err = user.GetChannels() channels, err = user.GetChannels()
assert.Len(t, channels, 0) 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() user.Remove()
_, err = os.Stat(storage.Path.User(user.Username)) _, err = os.Stat(storage.Path.User(user.Username))
assert.True(t, os.IsNotExist(err)) assert.True(t, os.IsNotExist(err))