Enable overriding connect defaults with query params, closes #49
This commit is contained in:
parent
dcbf3397c1
commit
eab788a782
File diff suppressed because one or more lines are too long
@ -14,6 +14,26 @@ const getSortedDefaultChannels = createSelector(
|
||||
channels => channels.split(',').sort()
|
||||
);
|
||||
|
||||
const transformChannels = channels => {
|
||||
const comma = channels[channels.length - 1] === ',';
|
||||
|
||||
channels = channels
|
||||
.split(',')
|
||||
.map(channel => {
|
||||
channel = channel.trim();
|
||||
if (channel) {
|
||||
if (isValidChannel(channel, false) && channel[0] !== '#') {
|
||||
channel = `#${channel}`;
|
||||
}
|
||||
}
|
||||
return channel;
|
||||
})
|
||||
.filter(s => s)
|
||||
.join(',');
|
||||
|
||||
return comma ? `${channels},` : channels;
|
||||
};
|
||||
|
||||
class Connect extends Component {
|
||||
state = {
|
||||
showOptionals: false
|
||||
@ -51,26 +71,6 @@ class Connect extends Component {
|
||||
return port;
|
||||
};
|
||||
|
||||
transformChannels = channels => {
|
||||
const comma = channels[channels.length - 1] === ',';
|
||||
|
||||
channels = channels
|
||||
.split(',')
|
||||
.map(channel => {
|
||||
channel = channel.trim();
|
||||
if (channel) {
|
||||
if (isValidChannel(channel, false) && channel[0] !== '#') {
|
||||
channel = `#${channel}`;
|
||||
}
|
||||
}
|
||||
return channel;
|
||||
})
|
||||
.filter(s => s)
|
||||
.join(',');
|
||||
|
||||
return comma ? `${channels},` : channels;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { defaults, values } = this.props;
|
||||
const { readOnly, showDetails } = defaults;
|
||||
@ -117,7 +117,7 @@ class Connect extends Component {
|
||||
<Error name="host" />
|
||||
<Error name="port" />
|
||||
<TextInput name="nick" />
|
||||
<TextInput name="channels" transform={this.transformChannels} />
|
||||
<TextInput name="channels" transform={transformChannels} />
|
||||
{this.state.showOptionals && this.renderOptionals()}
|
||||
<Button
|
||||
className="connect-form-button-optionals"
|
||||
@ -140,24 +140,38 @@ class Connect extends Component {
|
||||
|
||||
export default withFormik({
|
||||
enableReinitialize: true,
|
||||
mapPropsToValues: ({ defaults }) => {
|
||||
let port = 6667;
|
||||
if (defaults.port) {
|
||||
({ port } = defaults);
|
||||
mapPropsToValues: ({ defaults, query }) => {
|
||||
let port = '6667';
|
||||
if (query.port || defaults.port) {
|
||||
port = query.port || defaults.port;
|
||||
} else if (defaults.ssl) {
|
||||
port = 6697;
|
||||
port = '6697';
|
||||
}
|
||||
|
||||
let { channels } = query;
|
||||
if (channels) {
|
||||
channels = transformChannels(channels);
|
||||
}
|
||||
|
||||
let ssl;
|
||||
if (query.ssl === 'true') {
|
||||
ssl = true;
|
||||
} else if (query.ssl === 'false') {
|
||||
ssl = false;
|
||||
} else {
|
||||
ssl = defaults.ssl || false;
|
||||
}
|
||||
|
||||
return {
|
||||
name: defaults.name,
|
||||
host: defaults.host,
|
||||
name: query.name || defaults.name,
|
||||
host: query.host || defaults.host,
|
||||
port,
|
||||
nick: '',
|
||||
channels: defaults.channels.join(','),
|
||||
username: '',
|
||||
nick: query.nick || '',
|
||||
channels: channels || defaults.channels.join(','),
|
||||
username: query.username || '',
|
||||
password: defaults.password ? ' ' : '',
|
||||
realname: '',
|
||||
tls: defaults.ssl || false
|
||||
realname: query.realname || '',
|
||||
tls: ssl
|
||||
};
|
||||
},
|
||||
validate: values => {
|
||||
@ -203,6 +217,8 @@ export default withFormik({
|
||||
const channels = values.channels ? values.channels.split(',') : [];
|
||||
delete values.channels;
|
||||
|
||||
values.password = values.password.trim();
|
||||
|
||||
values.port = `${values.port}`;
|
||||
connect(values);
|
||||
select(values.host);
|
||||
|
@ -8,7 +8,8 @@ import connect from 'utils/connect';
|
||||
|
||||
const mapState = createStructuredSelector({
|
||||
defaults: getConnectDefaults,
|
||||
hexIP: state => getApp(state).hexIP
|
||||
hexIP: state => getApp(state).hexIP,
|
||||
query: state => state.router.query
|
||||
});
|
||||
|
||||
const mapDispatch = {
|
||||
|
@ -85,7 +85,7 @@ describe('tab reducer', () => {
|
||||
it('clears the tab when navigating to a non-tab page', () => {
|
||||
let state = reducer(undefined, setSelectedTab('srv', '#chan'));
|
||||
|
||||
state = reducer(state, locationChanged('settings'));
|
||||
state = reducer(state, locationChanged('settings', {}, {}));
|
||||
|
||||
expect(state).toEqual({
|
||||
selected: {},
|
||||
@ -96,10 +96,14 @@ describe('tab reducer', () => {
|
||||
it('selects the tab and adds it to history when navigating to a tab', () => {
|
||||
const state = reducer(
|
||||
undefined,
|
||||
locationChanged('chat', {
|
||||
server: 'srv',
|
||||
name: '#chan'
|
||||
})
|
||||
locationChanged(
|
||||
'chat',
|
||||
{
|
||||
server: 'srv',
|
||||
name: '#chan'
|
||||
},
|
||||
{}
|
||||
)
|
||||
);
|
||||
|
||||
expect(state).toEqual({
|
||||
|
@ -15,7 +15,8 @@ const initialState = {
|
||||
windowWidth: 0,
|
||||
connectDefaults: {
|
||||
name: '',
|
||||
address: '',
|
||||
host: '',
|
||||
port: '',
|
||||
channels: [],
|
||||
ssl: false,
|
||||
password: false,
|
||||
|
@ -6,11 +6,21 @@ export const PUSH = 'ROUTER_PUSH';
|
||||
export const REPLACE = 'ROUTER_REPLACE';
|
||||
|
||||
export function locationChanged(route, params, location) {
|
||||
Object.keys(params).forEach(key => {
|
||||
params[key] = decodeURIComponent(params[key]);
|
||||
});
|
||||
|
||||
const query = {};
|
||||
new URLSearchParams(location.search).forEach((value, key) => {
|
||||
query[key] = value;
|
||||
});
|
||||
|
||||
return {
|
||||
type: LOCATION_CHANGED,
|
||||
route,
|
||||
params,
|
||||
location
|
||||
query,
|
||||
path: decodeURIComponent(location.pathname)
|
||||
};
|
||||
}
|
||||
|
||||
@ -28,13 +38,9 @@ export function replace(path) {
|
||||
};
|
||||
}
|
||||
|
||||
export function routeReducer(state = {}, action) {
|
||||
if (action.type === LOCATION_CHANGED) {
|
||||
return {
|
||||
route: action.route,
|
||||
params: action.params,
|
||||
location: action.location
|
||||
};
|
||||
export function routeReducer(state = {}, { type, ...action }) {
|
||||
if (type === LOCATION_CHANGED) {
|
||||
return action;
|
||||
}
|
||||
|
||||
return state;
|
||||
@ -44,7 +50,7 @@ export function routeMiddleware() {
|
||||
return next => action => {
|
||||
switch (action.type) {
|
||||
case PUSH:
|
||||
history.push(action.path);
|
||||
history.push(`${action.path}`);
|
||||
break;
|
||||
case REPLACE:
|
||||
history.replace(action.path);
|
||||
@ -55,24 +61,13 @@ export function routeMiddleware() {
|
||||
};
|
||||
}
|
||||
|
||||
function decode(location) {
|
||||
location.pathname = decodeURIComponent(location.pathname);
|
||||
return location;
|
||||
}
|
||||
|
||||
function match(routes, location) {
|
||||
let params;
|
||||
for (let i = 0; i < routes.length; i++) {
|
||||
params = routes[i].pattern.match(location.pathname);
|
||||
const params = routes[i].pattern.match(location.pathname);
|
||||
if (params !== null) {
|
||||
const keys = Object.keys(params);
|
||||
for (let j = 0; j < keys.length; j++) {
|
||||
params[keys[j]] = decodeURIComponent(params[keys[j]]);
|
||||
}
|
||||
return locationChanged(routes[i].name, params, decode({ ...location }));
|
||||
return locationChanged(routes[i].name, params, location);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function initRouter(routes, store) {
|
||||
@ -91,16 +86,11 @@ export default function initRouter(routes, store) {
|
||||
let matched = match(patterns, history.location);
|
||||
if (matched) {
|
||||
store.dispatch(matched);
|
||||
} else {
|
||||
matched = { location: {} };
|
||||
}
|
||||
|
||||
history.listen(({ location }) => {
|
||||
const nextMatch = match(patterns, location);
|
||||
if (
|
||||
nextMatch &&
|
||||
nextMatch.location.pathname !== matched.location.pathname
|
||||
) {
|
||||
if (nextMatch && nextMatch.path !== matched?.path) {
|
||||
matched = nextMatch;
|
||||
store.dispatch(matched);
|
||||
}
|
||||
|
@ -26,6 +26,7 @@
|
||||
"babel-loader": "^8.1.0",
|
||||
"brotli": "^1.3.1",
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"cross-env": "^7.0.2",
|
||||
"css-loader": "^3.5.3",
|
||||
"cssnano": "^4.1.10",
|
||||
"del": "^5.1.0",
|
||||
@ -92,9 +93,9 @@
|
||||
"test": "jest",
|
||||
"test:verbose": "jest --verbose",
|
||||
"test:watch": "jest --watch",
|
||||
"gen:install": "GO111MODULE=off go get -u github.com/andyleap/gencode github.com/mailru/easyjson/...",
|
||||
"gen:install": "cross-env GO111MODULE=off go get -u github.com/andyleap/gencode github.com/mailru/easyjson/...",
|
||||
"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 && easyjson -lower_camel_case -omit_empty ../storage/user.go"
|
||||
"gen:json": "cross-env GO111MODULE=off easyjson -all -lower_camel_case -omit_empty ../server/json.go ../server/index_data.go && cross-env GO111MODULE=off easyjson -lower_camel_case -omit_empty ../storage/user.go"
|
||||
},
|
||||
"jest": {
|
||||
"moduleNameMapper": {
|
||||
|
@ -2544,6 +2544,12 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
|
||||
safe-buffer "^5.0.1"
|
||||
sha.js "^2.4.8"
|
||||
|
||||
cross-env@^7.0.2:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.2.tgz#bd5ed31339a93a3418ac4f3ca9ca3403082ae5f9"
|
||||
dependencies:
|
||||
cross-spawn "^7.0.1"
|
||||
|
||||
cross-spawn@^6.0.0, cross-spawn@^6.0.5:
|
||||
version "6.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
|
||||
@ -2554,7 +2560,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
|
||||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
cross-spawn@^7.0.0:
|
||||
cross-spawn@^7.0.0, cross-spawn@^7.0.1:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.2.tgz#d0d7dcfa74e89115c7619f4f721a94e1fdb716d6"
|
||||
dependencies:
|
||||
|
@ -11,14 +11,8 @@ import (
|
||||
)
|
||||
|
||||
type connectDefaults struct {
|
||||
Name string
|
||||
Host string
|
||||
Port int
|
||||
Channels []string
|
||||
Password bool
|
||||
SSL bool
|
||||
ReadOnly bool
|
||||
ShowDetails bool
|
||||
*config.Defaults
|
||||
Password bool
|
||||
}
|
||||
|
||||
type dispatchVersion struct {
|
||||
@ -28,7 +22,7 @@ type dispatchVersion struct {
|
||||
}
|
||||
|
||||
type indexData struct {
|
||||
Defaults *config.Defaults
|
||||
Defaults connectDefaults
|
||||
Servers []Server
|
||||
Channels []*storage.Channel
|
||||
OpenDMs []storage.Tab
|
||||
@ -48,7 +42,7 @@ func (d *Dispatch) getIndexData(r *http.Request, state *State) *indexData {
|
||||
cfg := d.Config()
|
||||
|
||||
data := indexData{
|
||||
Defaults: &cfg.Defaults,
|
||||
Defaults: connectDefaults{Defaults: &cfg.Defaults},
|
||||
HexIP: cfg.HexIP,
|
||||
Version: dispatchVersion{
|
||||
Tag: version.Tag,
|
||||
@ -57,9 +51,7 @@ func (d *Dispatch) getIndexData(r *http.Request, state *State) *indexData {
|
||||
},
|
||||
}
|
||||
|
||||
if data.Defaults.Password != "" {
|
||||
data.Defaults.Password = "******"
|
||||
}
|
||||
data.Defaults.Password = cfg.Defaults.Password != ""
|
||||
|
||||
if state == nil {
|
||||
data.Settings = storage.DefaultClientSettings()
|
||||
|
@ -39,14 +39,8 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchServer(in *jlexer.Lexer, out
|
||||
}
|
||||
switch key {
|
||||
case "defaults":
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
out.Defaults = nil
|
||||
} else {
|
||||
if out.Defaults == nil {
|
||||
out.Defaults = new(config.Defaults)
|
||||
}
|
||||
easyjson7e607aefDecodeGithubComKhliengDispatchConfig(in, out.Defaults)
|
||||
if data := in.Raw(); in.Ok() {
|
||||
in.AddError((out.Defaults).UnmarshalJSON(data))
|
||||
}
|
||||
case "servers":
|
||||
if in.IsNull() {
|
||||
@ -183,11 +177,11 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchServer(out *jwriter.Writer, i
|
||||
out.RawByte('{')
|
||||
first := true
|
||||
_ = first
|
||||
if in.Defaults != nil {
|
||||
if true {
|
||||
const prefix string = ",\"defaults\":"
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
easyjson7e607aefEncodeGithubComKhliengDispatchConfig(out, *in.Defaults)
|
||||
out.Raw((in.Defaults).MarshalJSON())
|
||||
}
|
||||
if len(in.Servers) != 0 {
|
||||
const prefix string = ",\"servers\":"
|
||||
@ -448,163 +442,6 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchStorage(out *jwriter.Writer,
|
||||
}
|
||||
out.RawByte('}')
|
||||
}
|
||||
func easyjson7e607aefDecodeGithubComKhliengDispatchConfig(in *jlexer.Lexer, out *config.Defaults) {
|
||||
isTopLevel := in.IsStart()
|
||||
if in.IsNull() {
|
||||
if isTopLevel {
|
||||
in.Consumed()
|
||||
}
|
||||
in.Skip()
|
||||
return
|
||||
}
|
||||
in.Delim('{')
|
||||
for !in.IsDelim('}') {
|
||||
key := in.UnsafeFieldName(false)
|
||||
in.WantColon()
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
in.WantComma()
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case "name":
|
||||
out.Name = string(in.String())
|
||||
case "host":
|
||||
out.Host = string(in.String())
|
||||
case "port":
|
||||
out.Port = int(in.Int())
|
||||
case "channels":
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
out.Channels = nil
|
||||
} else {
|
||||
in.Delim('[')
|
||||
if out.Channels == nil {
|
||||
if !in.IsDelim(']') {
|
||||
out.Channels = make([]string, 0, 4)
|
||||
} else {
|
||||
out.Channels = []string{}
|
||||
}
|
||||
} else {
|
||||
out.Channels = (out.Channels)[:0]
|
||||
}
|
||||
for !in.IsDelim(']') {
|
||||
var v10 string
|
||||
v10 = string(in.String())
|
||||
out.Channels = append(out.Channels, v10)
|
||||
in.WantComma()
|
||||
}
|
||||
in.Delim(']')
|
||||
}
|
||||
case "password":
|
||||
out.Password = string(in.String())
|
||||
case "ssl":
|
||||
out.SSL = bool(in.Bool())
|
||||
case "readOnly":
|
||||
out.ReadOnly = bool(in.Bool())
|
||||
case "showDetails":
|
||||
out.ShowDetails = bool(in.Bool())
|
||||
default:
|
||||
in.SkipRecursive()
|
||||
}
|
||||
in.WantComma()
|
||||
}
|
||||
in.Delim('}')
|
||||
if isTopLevel {
|
||||
in.Consumed()
|
||||
}
|
||||
}
|
||||
func easyjson7e607aefEncodeGithubComKhliengDispatchConfig(out *jwriter.Writer, in config.Defaults) {
|
||||
out.RawByte('{')
|
||||
first := true
|
||||
_ = first
|
||||
if in.Name != "" {
|
||||
const prefix string = ",\"name\":"
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
out.String(string(in.Name))
|
||||
}
|
||||
if in.Host != "" {
|
||||
const prefix string = ",\"host\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
out.String(string(in.Host))
|
||||
}
|
||||
if in.Port != 0 {
|
||||
const prefix string = ",\"port\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
out.Int(int(in.Port))
|
||||
}
|
||||
if len(in.Channels) != 0 {
|
||||
const prefix string = ",\"channels\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
{
|
||||
out.RawByte('[')
|
||||
for v11, v12 := range in.Channels {
|
||||
if v11 > 0 {
|
||||
out.RawByte(',')
|
||||
}
|
||||
out.String(string(v12))
|
||||
}
|
||||
out.RawByte(']')
|
||||
}
|
||||
}
|
||||
if in.Password != "" {
|
||||
const prefix string = ",\"password\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
out.String(string(in.Password))
|
||||
}
|
||||
if in.SSL {
|
||||
const prefix string = ",\"ssl\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
out.Bool(bool(in.SSL))
|
||||
}
|
||||
if in.ReadOnly {
|
||||
const prefix string = ",\"readOnly\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
out.Bool(bool(in.ReadOnly))
|
||||
}
|
||||
if in.ShowDetails {
|
||||
const prefix string = ",\"showDetails\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
out.Bool(bool(in.ShowDetails))
|
||||
}
|
||||
out.RawByte('}')
|
||||
}
|
||||
func easyjson7e607aefDecodeGithubComKhliengDispatchServer1(in *jlexer.Lexer, out *dispatchVersion) {
|
||||
isTopLevel := in.IsStart()
|
||||
if in.IsNull() {
|
||||
@ -705,6 +542,7 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchServer2(in *jlexer.Lexer, out
|
||||
in.Skip()
|
||||
return
|
||||
}
|
||||
out.Defaults = new(config.Defaults)
|
||||
in.Delim('{')
|
||||
for !in.IsDelim('}') {
|
||||
key := in.UnsafeFieldName(false)
|
||||
@ -715,6 +553,8 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchServer2(in *jlexer.Lexer, out
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case "password":
|
||||
out.Password = bool(in.Bool())
|
||||
case "name":
|
||||
out.Name = string(in.String())
|
||||
case "host":
|
||||
@ -737,15 +577,13 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchServer2(in *jlexer.Lexer, out
|
||||
out.Channels = (out.Channels)[:0]
|
||||
}
|
||||
for !in.IsDelim(']') {
|
||||
var v13 string
|
||||
v13 = string(in.String())
|
||||
out.Channels = append(out.Channels, v13)
|
||||
var v10 string
|
||||
v10 = string(in.String())
|
||||
out.Channels = append(out.Channels, v10)
|
||||
in.WantComma()
|
||||
}
|
||||
in.Delim(']')
|
||||
}
|
||||
case "password":
|
||||
out.Password = bool(in.Bool())
|
||||
case "ssl":
|
||||
out.SSL = bool(in.Bool())
|
||||
case "readOnly":
|
||||
@ -766,10 +604,20 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchServer2(out *jwriter.Writer,
|
||||
out.RawByte('{')
|
||||
first := true
|
||||
_ = first
|
||||
if in.Name != "" {
|
||||
const prefix string = ",\"name\":"
|
||||
if in.Password {
|
||||
const prefix string = ",\"password\":"
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
out.Bool(bool(in.Password))
|
||||
}
|
||||
if in.Name != "" {
|
||||
const prefix string = ",\"name\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
out.String(string(in.Name))
|
||||
}
|
||||
if in.Host != "" {
|
||||
@ -802,25 +650,15 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchServer2(out *jwriter.Writer,
|
||||
}
|
||||
{
|
||||
out.RawByte('[')
|
||||
for v14, v15 := range in.Channels {
|
||||
if v14 > 0 {
|
||||
for v11, v12 := range in.Channels {
|
||||
if v11 > 0 {
|
||||
out.RawByte(',')
|
||||
}
|
||||
out.String(string(v15))
|
||||
out.String(string(v12))
|
||||
}
|
||||
out.RawByte(']')
|
||||
}
|
||||
}
|
||||
if in.Password {
|
||||
const prefix string = ",\"password\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
out.Bool(bool(in.Password))
|
||||
}
|
||||
if in.SSL {
|
||||
const prefix string = ",\"ssl\":"
|
||||
if first {
|
||||
|
Loading…
Reference in New Issue
Block a user