1
1
import * as React from 'react' ;
2
2
import { sprintf } from 'sprintf-js' ;
3
- import { BridgeState , RelayProtocol , TunnelProtocol } from '../../shared/daemon-rpc-types' ;
3
+ import { colors } from '../../config.json' ;
4
+ import {
5
+ BridgeState ,
6
+ IDnsOptions ,
7
+ RelayProtocol ,
8
+ TunnelProtocol ,
9
+ } from '../../shared/daemon-rpc-types' ;
4
10
import { messages } from '../../shared/gettext' ;
11
+ import consumePromise from '../../shared/promise' ;
5
12
import { WgKeyState } from '../redux/settings/reducers' ;
6
13
import {
7
- StyledBottomCellGroup ,
14
+ StyledButtonCellGroup ,
8
15
StyledContainer ,
9
16
StyledInputFrame ,
10
17
StyledNavigationScrollbars ,
@@ -13,10 +20,15 @@ import {
13
20
StyledSelectorContainer ,
14
21
StyledTunnelProtocolSelector ,
15
22
StyledTunnelProtocolContainer ,
23
+ StyledCustomDnsSwitchContainer ,
24
+ StyledCustomDnsFotter ,
25
+ StyledAddCustomDnsLabel ,
26
+ StyledAddCustomDnsButton ,
16
27
} from './AdvancedSettingsStyles' ;
17
28
import * as AppButton from './AppButton' ;
18
29
import { AriaDescription , AriaInput , AriaInputGroup , AriaLabel } from './AriaGroup' ;
19
30
import * as Cell from './cell' ;
31
+ import CellList , { ICellListItem } from './cell/List' ;
20
32
import { Layout } from './Layout' ;
21
33
import { ModalAlert , ModalAlertType , ModalContainer , ModalMessage } from './Modal' ;
22
34
import {
@@ -28,6 +40,7 @@ import {
28
40
} from './NavigationBar' ;
29
41
import Selector , { ISelectorItem } from './cell/Selector' ;
30
42
import SettingsHeader , { HeaderTitle } from './SettingsHeader' ;
43
+ import Accordion from './Accordion' ;
31
44
32
45
const MIN_MSSFIX_VALUE = 1000 ;
33
46
const MAX_MSSFIX_VALUE = 1450 ;
@@ -59,6 +72,7 @@ interface IProps {
59
72
mssfix ?: number ;
60
73
wireguardMtu ?: number ;
61
74
bridgeState : BridgeState ;
75
+ dns : IDnsOptions ;
62
76
setBridgeState : ( value : BridgeState ) => void ;
63
77
setEnableIpv6 : ( value : boolean ) => void ;
64
78
setBlockWhenDisconnected : ( value : boolean ) => void ;
@@ -67,20 +81,29 @@ interface IProps {
67
81
setWireguardMtu : ( value : number | undefined ) => void ;
68
82
setOpenVpnRelayProtocolAndPort : ( protocol ?: RelayProtocol , port ?: number ) => void ;
69
83
setWireguardRelayPort : ( port ?: number ) => void ;
84
+ setDnsOptions : ( dns : IDnsOptions ) => Promise < void > ;
70
85
onViewWireguardKeys : ( ) => void ;
71
86
onViewLinuxSplitTunneling : ( ) => void ;
72
87
onClose : ( ) => void ;
73
88
}
74
89
75
90
interface IState {
76
91
showConfirmBlockWhenDisconnectedAlert : boolean ;
92
+ showAddCustomDns : boolean ;
93
+ invalidDnsIp : boolean ;
77
94
}
78
95
79
96
export default class AdvancedSettings extends React . Component < IProps , IState > {
80
97
public state = {
81
98
showConfirmBlockWhenDisconnectedAlert : false ,
99
+ showAddCustomDns : false ,
100
+ invalidDnsIp : false ,
82
101
} ;
83
102
103
+ private customDnsSwitchRef = React . createRef < HTMLDivElement > ( ) ;
104
+ private customDnsAddButtonRef = React . createRef < HTMLButtonElement > ( ) ;
105
+ private customDnsInputContainerRef = React . createRef < HTMLDivElement > ( ) ;
106
+
84
107
private portItems : { [ key in RelayProtocol ] : Array < ISelectorItem < OptionalPort > > } ;
85
108
private protocolItems : Array < ISelectorItem < OptionalRelayProtocol > > ;
86
109
private bridgeStateItems : Array < ISelectorItem < BridgeState > > ;
@@ -395,7 +418,7 @@ export default class AdvancedSettings extends React.Component<IProps, IState> {
395
418
</ Cell . Footer >
396
419
</ AriaInputGroup >
397
420
398
- < StyledBottomCellGroup >
421
+ < StyledButtonCellGroup >
399
422
< Cell . CellButton onClick = { this . props . onViewWireguardKeys } >
400
423
< Cell . Label >
401
424
{ messages . pgettext ( 'advanced-settings-view' , 'WireGuard key' ) }
@@ -411,7 +434,61 @@ export default class AdvancedSettings extends React.Component<IProps, IState> {
411
434
< Cell . Icon height = { 12 } width = { 7 } source = "icon-chevron" />
412
435
</ Cell . CellButton >
413
436
) }
414
- </ StyledBottomCellGroup >
437
+ </ StyledButtonCellGroup >
438
+
439
+ < StyledCustomDnsSwitchContainer >
440
+ < Cell . InputLabel >
441
+ { messages . pgettext ( 'advanced-settings-view' , 'Use custom DNS server' ) }
442
+ </ Cell . InputLabel >
443
+ < Cell . Switch
444
+ ref = { this . customDnsSwitchRef }
445
+ isOn = { this . props . dns . custom }
446
+ onChange = { this . setCustomDnsEnabled }
447
+ />
448
+ </ StyledCustomDnsSwitchContainer >
449
+ < Accordion expanded = { this . props . dns . custom } >
450
+ < CellList items = { this . customDnsItems ( ) } onRemove = { this . removeDnsAddress } />
451
+
452
+ { this . state . showAddCustomDns && (
453
+ < div ref = { this . customDnsInputContainerRef } >
454
+ < Cell . RowInput
455
+ onSubmit = { this . addDnsAddress }
456
+ onChange = { this . addDnsInputChange }
457
+ invalid = { this . state . invalidDnsIp }
458
+ paddingLeft = { 32 }
459
+ onBlur = { this . customDnsInputBlur }
460
+ autofocus
461
+ />
462
+ </ div >
463
+ ) }
464
+
465
+ < StyledAddCustomDnsButton
466
+ ref = { this . customDnsAddButtonRef }
467
+ onClick = { this . showAddCustomDnsRow }
468
+ disabled = { this . state . showAddCustomDns }
469
+ tabIndex = { - 1 } >
470
+ < StyledAddCustomDnsLabel tabIndex = { - 1 } >
471
+ { messages . pgettext ( 'advanced-settings-view' , 'Add a server' ) }
472
+ </ StyledAddCustomDnsLabel >
473
+ < Cell . UntintedIcon
474
+ source = "icon-add"
475
+ width = { 22 }
476
+ height = { 22 }
477
+ tintColor = { colors . white60 }
478
+ tintHoverColor = { colors . white80 }
479
+ tabIndex = { - 1 }
480
+ />
481
+ </ StyledAddCustomDnsButton >
482
+ </ Accordion >
483
+
484
+ < StyledCustomDnsFotter >
485
+ < Cell . FooterText >
486
+ { messages . pgettext (
487
+ 'advanced-settings-view' ,
488
+ 'Enable to add at least one DNS server.' ,
489
+ ) }
490
+ </ Cell . FooterText >
491
+ </ StyledCustomDnsFotter >
415
492
</ StyledNavigationScrollbars >
416
493
</ NavigationContainer >
417
494
</ StyledContainer >
@@ -423,6 +500,81 @@ export default class AdvancedSettings extends React.Component<IProps, IState> {
423
500
) ;
424
501
}
425
502
503
+ private setCustomDnsEnabled = async ( enabled : boolean ) => {
504
+ await this . props . setDnsOptions ( {
505
+ custom : enabled ,
506
+ addresses : this . props . dns . addresses ,
507
+ } ) ;
508
+
509
+ if ( enabled && this . props . dns . addresses . length === 0 ) {
510
+ this . showAddCustomDnsRow ( ) ;
511
+ }
512
+
513
+ if ( ! enabled ) {
514
+ this . setState ( { showAddCustomDns : false } ) ;
515
+ }
516
+ } ;
517
+
518
+ private customDnsItems ( ) : ICellListItem < string > [ ] {
519
+ return this . props . dns . addresses . map ( ( address ) => ( {
520
+ label : address ,
521
+ value : address ,
522
+ } ) ) ;
523
+ }
524
+
525
+ private showAddCustomDnsRow = ( ) => {
526
+ this . setState ( { showAddCustomDns : true } ) ;
527
+ } ;
528
+
529
+ // The input field should be hidden when it loses focus unless something on the same row or the
530
+ // add-button is the new focused element.
531
+ private customDnsInputBlur = ( event ?: React . FocusEvent < HTMLTextAreaElement > ) => {
532
+ const relatedTarget = event ?. relatedTarget as Node | undefined ;
533
+ if (
534
+ relatedTarget &&
535
+ ( this . customDnsSwitchRef . current ?. contains ( relatedTarget ) ||
536
+ this . customDnsAddButtonRef . current ?. contains ( relatedTarget ) ||
537
+ this . customDnsInputContainerRef . current ?. contains ( relatedTarget ) )
538
+ ) {
539
+ event ?. target . focus ( ) ;
540
+ } else {
541
+ this . hideAddCustomDnsRow ( false ) ;
542
+ }
543
+ } ;
544
+
545
+ private hideAddCustomDnsRow ( justAdded : boolean ) {
546
+ this . setState ( { showAddCustomDns : false } ) ;
547
+ if ( ! justAdded && this . props . dns . addresses . length === 0 ) {
548
+ consumePromise ( this . setCustomDnsEnabled ( false ) ) ;
549
+ }
550
+ }
551
+
552
+ private addDnsInputChange = ( _value : string ) => {
553
+ this . setState ( { invalidDnsIp : false } ) ;
554
+ } ;
555
+
556
+ private addDnsAddress = async ( address : string ) => {
557
+ try {
558
+ await this . props . setDnsOptions ( {
559
+ custom : this . props . dns . custom ,
560
+ addresses : [ ...this . props . dns . addresses , address ] ,
561
+ } ) ;
562
+ this . hideAddCustomDnsRow ( true ) ;
563
+ } catch ( _e ) {
564
+ this . setState ( { invalidDnsIp : true } ) ;
565
+ }
566
+ } ;
567
+
568
+ private removeDnsAddress = ( address : string ) => {
569
+ const addresses = this . props . dns . addresses . filter ( ( item ) => item !== address ) ;
570
+ consumePromise (
571
+ this . props . setDnsOptions ( {
572
+ custom : addresses . length > 0 && this . props . dns . custom ,
573
+ addresses,
574
+ } ) ,
575
+ ) ;
576
+ } ;
577
+
426
578
private tunnelProtocolItems = (
427
579
hasWireguardKey : boolean ,
428
580
) : Array < ISelectorItem < OptionalTunnelProtocol > > => {
0 commit comments