Files
pallectrum/electrum/gui/qt/network_dialog.py
f321x f2aa4404ad qt: fix ServerWidget
The ServerWidget was not working properly, when switching from "Manual
Mode" to "Auto Connect" the change wouldn't get saved as it depended on
having a correct server string entered (which isn't neccessary for Auto
Connect).
Also makes the widget behave more sane by cleaning the server input if
Auto Connect is enabled and switching to Manual Mode if the user
manually selects a server.

Update the ServerWidget every time it is shown (on initialization and
also when the user opens it again or switches between network dialog
tabs).
This will clean it up if the user has entered some invalid server and
closes it, otherwise this server would stay in the input field until the
application is restarted.

The list of servers in the ServerWidget allows the user to right click
and 'Use as server' on the servers in the list, however internally it
was handled differently than what the user would expect when clicking on
'Use as server'. E.g. if the user selects a server in autoconnect mode
it would still stay in autoconnect mode so the server could switch again
to another server any time? Now it will also change the mode to manual
(or stay in single server mode if that was selected before), making it
clear that this server will stay selected.

If the user clicks on "Follow this branch" the connect mode will get changed to
autoconnect as internally we connect to a random interface on this
branch.
2025-10-27 18:46:36 +01:00

643 lines
26 KiB
Python

#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2012 thomasv@gitorious
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from enum import IntEnum
from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QMenu, QGridLayout, QComboBox, QLineEdit, QDialog, QVBoxLayout, QHeaderView,
QCheckBox, QTabWidget, QWidget, QLabel, QPushButton, QHBoxLayout,
QListWidget, QListWidgetItem,
)
from PyQt6.QtGui import QIntValidator
from electrum.i18n import _
from electrum import blockchain
from electrum.interface import ServerAddr, PREFERRED_NETWORK_PROTOCOL
from electrum.network import Network, ProxySettings, is_valid_host, is_valid_port
from electrum.logging import get_logger
from electrum.util import is_valid_websocket_url
from electrum.gui import messages
from .util import (
Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit, PasswordLineEdit, QtEventListener,
qt_event_listener, Spinner, HelpLabel
)
_logger = get_logger(__name__)
protocol_names = ['TCP', 'SSL']
protocol_letters = 'ts'
class NetworkDialog(QDialog, QtEventListener):
def __init__(self, *, network: Network):
QDialog.__init__(self)
self.setWindowTitle(_('Network'))
self.setMinimumSize(500, 500)
self.tabs = tabs = QTabWidget()
self._blockchain_tab = ServerWidget(network)
self._proxy_tab = ProxyWidget(network)
self._nostr_tab = NostrWidget(network)
tabs.addTab(self._blockchain_tab, _('Server'))
tabs.addTab(self._nostr_tab, _('Nostr'))
tabs.addTab(self._proxy_tab, _('Proxy'))
vbox = QVBoxLayout(self)
vbox.addWidget(self.tabs)
vbox.addLayout(Buttons(CloseButton(self)))
def show(self, *, proxy_tab: bool = False):
super().show()
self.tabs.setCurrentWidget(self._proxy_tab if proxy_tab else self._blockchain_tab)
class NodesListWidget(QTreeWidget):
"""List of connected servers."""
SERVER_ADDR_ROLE = Qt.ItemDataRole.UserRole + 100
CHAIN_ID_ROLE = Qt.ItemDataRole.UserRole + 101
ITEMTYPE_ROLE = Qt.ItemDataRole.UserRole + 102
class ItemType(IntEnum):
CHAIN = 0
CONNECTED_SERVER = 1
DISCONNECTED_SERVER = 2
TOPLEVEL = 3
followServer = pyqtSignal([ServerAddr], arguments=['server'])
followChain = pyqtSignal([str], arguments=['chain_id'])
setServer = pyqtSignal([str], arguments=['server'])
def __init__(self, *, network: Network):
QTreeWidget.__init__(self)
self.setHeaderLabels([_('Server'), _('Height')])
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self.create_menu)
self.network = network
def create_menu(self, position):
item = self.currentItem()
if not item:
return
item_type = item.data(0, self.ITEMTYPE_ROLE)
menu = QMenu()
if item_type in [self.ItemType.CONNECTED_SERVER, self.ItemType.DISCONNECTED_SERVER]:
server = item.data(0, self.SERVER_ADDR_ROLE) # type: ServerAddr
if item_type == self.ItemType.CONNECTED_SERVER:
def do_follow_server():
self.followServer.emit(server)
menu.addAction(read_QIcon("chevron-right.png"), _("Use as server"), do_follow_server)
elif item_type == self.ItemType.DISCONNECTED_SERVER:
def do_set_server():
self.setServer.emit(str(server))
menu.addAction(read_QIcon("chevron-right.png"), _("Use as server"), do_set_server)
def set_bookmark(*, add: bool):
self.network.set_server_bookmark(server, add=add)
self.update()
if self.network.is_server_bookmarked(server):
menu.addAction(read_QIcon("bookmark_remove.png"), _("Remove from bookmarks"), lambda: set_bookmark(add=False))
else:
menu.addAction(read_QIcon("bookmark_add.png"), _("Bookmark this server"), lambda: set_bookmark(add=True))
elif item_type == self.ItemType.CHAIN:
chain_id = item.data(0, self.CHAIN_ID_ROLE)
def do_follow_chain():
self.followChain.emit(chain_id)
menu.addAction(_("Follow this branch"), do_follow_chain)
else:
return
menu.exec(self.viewport().mapToGlobal(position))
def keyPressEvent(self, event):
if event.key() in [Qt.Key.Key_F2, Qt.Key.Key_Return, Qt.Key.Key_Enter]:
self.on_activated(self.currentItem(), self.currentColumn())
else:
QTreeWidget.keyPressEvent(self, event)
def on_activated(self, item, column):
# on 'enter' we show the menu
pt = self.visualItemRect(item).bottomLeft()
pt.setX(50)
self.customContextMenuRequested.emit(pt)
def update(self):
self.clear()
network = self.network
servers = self.network.get_servers()
use_tor = bool(network.is_proxy_tor)
# connected servers
connected_servers_item = QTreeWidgetItem([_("Connected nodes"), ''])
connected_servers_item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.TOPLEVEL)
chains = network.get_blockchains()
n_chains = len(chains)
for chain_id, interfaces in chains.items():
b = blockchain.blockchains.get(chain_id)
if b is None:
continue
name = b.get_name()
if n_chains > 1:
x = QTreeWidgetItem([name + '@%d'%b.get_max_forkpoint(), '%d'%b.height()])
x.setData(0, self.ITEMTYPE_ROLE, self.ItemType.CHAIN)
x.setData(0, self.CHAIN_ID_ROLE, b.get_id())
else:
x = connected_servers_item
for i in interfaces:
item = QTreeWidgetItem([f"{i.server.to_friendly_name()}", '%d'%i.tip])
item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.CONNECTED_SERVER)
item.setData(0, self.SERVER_ADDR_ROLE, i.server)
item.setToolTip(0, str(i.server))
if i == network.interface:
item.setIcon(0, read_QIcon("chevron-right.png"))
elif network.is_server_bookmarked(i.server):
item.setIcon(0, read_QIcon("bookmark.png"))
x.addChild(item)
if n_chains > 1:
connected_servers_item.addChild(x)
# disconnected servers
disconnected_servers_item = QTreeWidgetItem([_("Other known servers"), ""])
disconnected_servers_item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.TOPLEVEL)
connected_hosts = set([iface.host for ifaces in chains.values() for iface in ifaces])
protocol = PREFERRED_NETWORK_PROTOCOL
server_addrs = [
ServerAddr(_host, port, protocol=protocol)
for _host, d in servers.items()
if (port := d.get(protocol))]
server_addrs.sort(key=lambda x: (-network.is_server_bookmarked(x), str(x)))
for server in server_addrs:
if server.host in connected_hosts:
continue
if server.host.endswith('.onion') and not use_tor:
continue
item = QTreeWidgetItem([server.net_addr_str(), ""])
item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.DISCONNECTED_SERVER)
item.setData(0, self.SERVER_ADDR_ROLE, server)
if network.is_server_bookmarked(server):
item.setIcon(0, read_QIcon("bookmark.png"))
disconnected_servers_item.addChild(item)
self.addTopLevelItem(connected_servers_item)
self.addTopLevelItem(disconnected_servers_item)
connected_servers_item.setExpanded(True)
for i in range(connected_servers_item.childCount()):
connected_servers_item.child(i).setExpanded(True)
disconnected_servers_item.setExpanded(True)
# headers
h = self.header()
h.setStretchLastSection(False)
h.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
h.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
super().update()
class ProxyWidget(QWidget):
PROXY_MODES = {
'socks4': 'SOCKS4',
'socks5': 'SOCKS5/TOR'
}
torProbeFinished = pyqtSignal([str, int], arguments=['host', 'port'])
def __init__(self, network: Network, parent=None):
super().__init__(parent)
self.network = network
self.config = network.config
fixed_width_port = 6 * char_width_in_lineedit()
# proxy setting.
self.proxy_cb = QCheckBox(_('Use proxy'))
self.proxy_mode = QComboBox()
for k, v in self.PROXY_MODES.items():
self.proxy_mode.addItem(v, k)
self.proxy_mode.setCurrentIndex(1)
self.proxy_host = QLineEdit()
self.proxy_port = QLineEdit()
self.proxy_port.setFixedWidth(fixed_width_port)
self.proxy_port_validator = QIntValidator(1, 65535)
self.proxy_port.setValidator(self.proxy_port_validator)
self.proxy_user = QLineEdit()
self.proxy_user.setPlaceholderText(_("Proxy username"))
self.proxy_password = PasswordLineEdit()
self.proxy_password.setPlaceholderText(_("Proxy password"))
grid = QGridLayout(self)
grid.setSpacing(8)
grid.addWidget(self.proxy_cb, 0, 0, 1, 4)
proxy_helpbutton = HelpButton(
_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.'))
grid.addWidget(proxy_helpbutton, 0, 4, alignment=Qt.AlignmentFlag.AlignRight)
grid.addWidget(self.proxy_mode, 1, 0, 1, 1)
grid.addWidget(self.proxy_host, 1, 1, 1, 3)
grid.addWidget(self.proxy_port, 1, 4, 1, 1)
grid.addWidget(self.proxy_user, 2, 1, 1, 2)
grid.addWidget(self.proxy_password, 2, 3, 1, 2)
detect_l = QHBoxLayout()
self.detect_button = QPushButton(_('Detect Tor proxy'))
self.spinner = Spinner()
self.spinner.setMargin(5)
detect_l.addWidget(self.detect_button)
detect_l.addWidget(self.spinner)
grid.addLayout(detect_l, 3, 0, 1, 5, alignment=Qt.AlignmentFlag.AlignLeft)
spacer = QVBoxLayout()
spacer.addStretch(1)
grid.addLayout(spacer, 4, 0, 1, 5)
self.update_from_config()
self.update()
# connect signal handlers after init from config
self.proxy_cb.stateChanged.connect(self.on_proxy_enable_toggle)
self.proxy_mode.currentIndexChanged.connect(self.on_proxy_settings_changed)
self.proxy_host.editingFinished.connect(self.on_proxy_settings_changed)
self.proxy_port.editingFinished.connect(self.on_proxy_settings_changed)
self.proxy_user.editingFinished.connect(self.on_proxy_settings_changed)
self.proxy_password.editingFinished.connect(self.on_proxy_settings_changed)
self.detect_button.clicked.connect(self.detect_tor)
self.torProbeFinished.connect(self.on_tor_probe_finished)
def update(self):
enabled = self.proxy_cb.isChecked() and self.config.cv.NETWORK_PROXY.is_modifiable()
for item in [
self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password,
self.detect_button
]:
item.setEnabled(enabled)
if not self.proxy_port.hasAcceptableInput() and not is_valid_port(self.proxy_port.text()):
return
if not is_valid_host(self.proxy_host.text()):
return
net_params = self.network.get_parameters()
proxy = self.get_proxy_settings()
net_params = net_params._replace(proxy=proxy)
self.network.run_from_another_thread(self.network.set_parameters(net_params))
def update_from_config(self):
proxy = ProxySettings.from_config(self.config)
self.proxy_cb.setChecked(proxy.enabled)
self.proxy_mode.setCurrentText(self.PROXY_MODES.get(proxy.mode))
self.proxy_host.setText(proxy.host)
self.proxy_port.setText(proxy.port)
self.proxy_user.setText(proxy.user)
self.proxy_password.setText(proxy.password)
if not self.config.cv.NETWORK_PROXY.is_modifiable():
for w in [
self.proxy_cb, self.proxy_mode, self.proxy_host, self.proxy_port,
self.proxy_user, self.proxy_password, self.detect_button
]:
w.setEnabled(False)
def on_proxy_enable_toggle(self):
# probe if enabled and no pre-existing settings
# if self.proxy_cb.isChecked() and (not self.proxy_host.text() or not self.proxy_port.text()):
# self.detect_tor()
self.update()
def on_proxy_settings_changed(self):
self.update()
def get_proxy_settings(self) -> ProxySettings:
proxy = ProxySettings()
proxy.enabled = self.proxy_cb.isChecked()
proxy.mode = self.proxy_mode.currentData()
proxy.host = self.proxy_host.text()
proxy.port = self.proxy_port.text()
proxy.user = self.proxy_user.text()
proxy.password = self.proxy_password.text()
return proxy
def detect_tor(self):
self.detect_button.setEnabled(False)
self.spinner.setVisible(True)
ProxySettings.probe_tor(self.torProbeFinished.emit) # via signal
@pyqtSlot(str, int)
def on_tor_probe_finished(self, host: str, port: int):
self.detect_button.setEnabled(True)
self.spinner.setVisible(False)
if host:
self.proxy_mode.setCurrentIndex(1)
self.proxy_host.setText(host)
self.proxy_port.setText(str(port))
self.update()
class ConnectMode(IntEnum):
AUTOCONNECT = 0
MANUAL = 1
ONESERVER = 2
class ServerWidget(QWidget, QtEventListener):
CONNECT_MODES = {
ConnectMode.AUTOCONNECT: messages.MSG_CONNECTMODE_AUTOCONNECT,
ConnectMode.MANUAL: messages.MSG_CONNECTMODE_MANUAL,
ConnectMode.ONESERVER: messages.MSG_CONNECTMODE_ONESERVER,
}
def __init__(self, network: Network, parent=None):
super().__init__(parent)
self.network = network
self.config = network.config
self.setLayout(QVBoxLayout())
grid = QGridLayout()
self.connect_combo = QComboBox()
for i, v in sorted(self.CONNECT_MODES.items()):
self.connect_combo.addItem(v, i)
self.connect_combo.currentIndexChanged.connect(self.on_server_settings_changed)
grid.addWidget(QLabel(_('Connection mode') + ':'), 0, 0)
msg = (
f"""
{messages.MSG_CONNECTMODE_SERVER_HELP}<br/><br/>
{messages.MSG_CONNECTMODE_NODES_HELP}
<ul>
<li><b>{messages.MSG_CONNECTMODE_AUTOCONNECT}</b>: {messages.MSG_CONNECTMODE_AUTOCONNECT_HELP}</li>
<li><b>{messages.MSG_CONNECTMODE_MANUAL}</b>: {messages.MSG_CONNECTMODE_MANUAL_HELP}</li>
<li><b>{messages.MSG_CONNECTMODE_ONESERVER}</b>: {messages.MSG_CONNECTMODE_ONESERVER_HELP}</li>
</ul>
"""
)
grid.addWidget(HelpButton(msg), 0, 4)
grid.addWidget(self.connect_combo, 0, 1, 1, 3)
self.server_e = QLineEdit()
self.server_e.editingFinished.connect(self.on_server_settings_changed)
grid.addWidget(QLabel(_('Server') + ':'), 1, 0)
grid.addWidget(self.server_e, 1, 1, 1, 3)
grid.addWidget(HelpButton(messages.MSG_CONNECTMODE_SERVER_HELP), 1, 4)
self.status_label_header = QLabel(_('Status') + ':')
self.status_label = QLabel('')
self.status_label_helpbutton = HelpButton(messages.MSG_CONNECTMODE_NODES_HELP)
grid.addWidget(self.status_label_header, 2, 0)
grid.addWidget(self.status_label, 2, 1, 1, 3)
grid.addWidget(self.status_label_helpbutton, 2, 4)
msg = _('This is the height of your local copy of the blockchain.')
self.height_label_header = QLabel(_('Blockchain') + ':')
self.height_label = QLabel('')
self.height_label_helpbutton = HelpButton(msg)
grid.addWidget(self.height_label_header, 3, 0)
grid.addWidget(self.height_label, 3, 1)
grid.addWidget(self.height_label_helpbutton, 3, 4)
self.split_label = QLabel('')
grid.addWidget(self.split_label, 4, 1, 1, 3)
self.layout().addLayout(grid)
self.nodes_list_widget = NodesListWidget(network=self.network)
self.nodes_list_widget.followServer.connect(self.follow_server)
self.nodes_list_widget.followChain.connect(self.follow_branch)
def do_set_server(server):
self.server_e.setText(server)
if self.is_auto_connect():
# switch to manual mode as the user manually selected a server
self.set_connect_mode(ConnectMode.MANUAL, block_signals=True)
self.on_server_settings_changed()
self.nodes_list_widget.setServer.connect(do_set_server)
self.layout().addWidget(self.nodes_list_widget)
self.nodes_list_widget.update()
self.register_callbacks()
self.destroyed.connect(lambda: self.unregister_callbacks())
def showEvent(self, event):
# gets called every time the ServerWidget is shown, when opening it and when
# switching between the tabs.
super().showEvent(event)
_logger.debug(f"showing ServerWidget")
# If the user entered garbage the previous time the ServerWidget was open this will restore
# it back to the current config
self.update_from_config()
self.update()
@qt_event_listener
def on_event_network_updated(self):
self.nodes_list_widget.update() # NOTE: move event handling to widget itself?
self.update()
def is_auto_connect(self):
return self.connect_combo.currentIndex() == ConnectMode.AUTOCONNECT
def is_one_server(self):
return self.connect_combo.currentIndex() == ConnectMode.ONESERVER
def set_connect_mode(self, connect_mode: ConnectMode, *, block_signals = False):
# if block_signals = True the on_server_settings_changed won't get called when changing the index
assert isinstance(connect_mode, ConnectMode), connect_mode
self.connect_combo.blockSignals(block_signals)
self.connect_combo.setCurrentIndex(connect_mode)
self.connect_combo.blockSignals(False)
def on_server_settings_changed(self):
if not self.network._was_started:
self.update()
return
current_net_params = self.network.get_parameters()
new_server = ServerAddr.from_str_with_inference(self.server_e.text().strip())
new_server = new_server or current_net_params.server # keep existing server while input is invalid
settings_changed = False
if new_server != current_net_params.server:
settings_changed = True
if self.is_auto_connect() != current_net_params.auto_connect:
settings_changed = True
if self.is_one_server() != current_net_params.oneserver:
settings_changed = True
if settings_changed:
_logger.debug(
f"ServerWidget.on_server_settings_changed:\n"
f"[server: {current_net_params.server} -> {new_server}]\n"
f"[auto_connect: {current_net_params.auto_connect} -> {self.is_auto_connect()}]\n"
f"[oneserver: {current_net_params.oneserver} -> {self.is_one_server()}]"
)
self.set_server(
new_server,
auto_connect=self.is_auto_connect(),
one_server=self.is_one_server(),
)
self.update()
def update(self):
self.server_e.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable() and not self.is_auto_connect())
if self.is_auto_connect():
self.server_e.clear()
elif not self.server_e.text():
self.server_e.setText(self.config.NETWORK_SERVER or "")
for item in [
self.status_label_header, self.status_label, self.status_label_helpbutton,
self.height_label_header, self.height_label, self.height_label_helpbutton]:
item.setVisible(self.network._was_started)
msg = _('Fork detection disabled') if self.is_one_server() else ''
if self.network._was_started:
# Network was started, so we don't run in initial setup wizard.
# behavior in this case is to apply changes immediately.
# Also, we show block height and potential chain tips
height_str = _('{} blocks').format(self.network.get_local_height())
self.height_label.setText(height_str)
self.status_label.setText(self.network.get_status())
chains = self.network.get_blockchains()
if len(chains) > 1:
chain = self.network.blockchain()
forkpoint = chain.get_max_forkpoint()
name = chain.get_name()
msg = _('Fork detected at block {0}').format(forkpoint) + '\n'
if self.is_auto_connect():
msg += _('You are following branch {}').format(name)
else:
msg += _('Your server is on branch {0} ({1} blocks)').format(name, chain.get_branch_size())
self.split_label.setText(msg)
def update_from_config(self):
auto_connect = self.config.NETWORK_AUTO_CONNECT
one_server = self.config.NETWORK_ONESERVER
v = ConnectMode.AUTOCONNECT if auto_connect else ConnectMode.ONESERVER if one_server else ConnectMode.MANUAL
self.set_connect_mode(v)
server = self.config.NETWORK_SERVER
self.server_e.setText(server)
self.server_e.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable() and not auto_connect)
self.nodes_list_widget.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable())
_logger.debug(f"update from config: done")
def follow_branch(self, chain_id):
self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
# follow_chain_given_id connects to random interface, so set connect_mode back to AUTOCONNECT
self.set_connect_mode(ConnectMode.AUTOCONNECT, block_signals=True)
self.update()
def follow_server(self, server: ServerAddr):
try:
self.network.follow_chain_given_server(server)
except KeyError:
_logger.debug(f"follow_server: cannot follow, not connected to {server.net_addr_str()}.")
return
self.server_e.setText(str(server))
if self.is_auto_connect():
# the user manually selected a server, so the ConnectMode gets set to MANUAL
self.set_connect_mode(ConnectMode.MANUAL, block_signals=True)
self.set_server(
server=server,
auto_connect=False,
one_server=self.is_one_server(),
)
self.update()
def set_server(self, server: ServerAddr, *, auto_connect: bool, one_server: bool):
current_net_params = self.network.get_parameters()
new_net_params = current_net_params._replace(
server=server,
auto_connect=auto_connect,
oneserver=one_server,
)
_logger.debug(f"set_server: {new_net_params=}")
self.network.run_from_another_thread(self.network.set_parameters(new_net_params))
class NostrWidget(QWidget, QtEventListener):
def __init__(self, network: Network, parent=None):
super().__init__(parent)
self.network = network
self.config = network.config
vbox = QVBoxLayout()
self.setLayout(vbox)
grid = QGridLayout()
nostr_relays_label = QLabel(self.config.cv.NOSTR_RELAYS.get_short_desc())
nostr_helpbutton = HelpButton(self.config.cv.NOSTR_RELAYS.get_long_desc())
grid.addWidget(nostr_relays_label, 0, 0)
grid.addWidget(nostr_helpbutton, 0, 1)
vbox.addLayout(grid)
self.relays_list = QListWidget()
self.relay_edit = QLineEdit()
self.relay_edit.textChanged.connect(self.on_relay_edited)
vbox.addWidget(self.relays_list)
vbox.addStretch()
self.add_button = QPushButton(_('Add'))
self.add_button.clicked.connect(self.add_relay)
self.add_button.setEnabled(False)
remove_button = QPushButton(_('Remove'))
remove_button.clicked.connect(self.remove_relay)
reset_button = QPushButton(_('Reset'))
reset_button.clicked.connect(self.reset_relays)
buttons = Buttons(self.relay_edit, self.add_button, remove_button, reset_button)
vbox.addLayout(buttons)
self.update_list()
def on_relay_edited(self, text):
self.add_button.setEnabled(is_valid_websocket_url(text))
def update_list(self):
self.relays_list.clear()
for relay in self.config.get_nostr_relays():
item = QListWidgetItem(relay)
self.relays_list.addItem(item)
def add_relay(self):
relay = self.relay_edit.text()
self.config.add_nostr_relay(relay)
self.update_list()
def remove_relay(self):
item = self.relays_list.currentItem()
if item is None:
return
self.config.remove_nostr_relay(item.text())
self.update_list()
def reset_relays(self):
self.config.NOSTR_RELAYS = None
self.update_list()