- Matrix: interactive device verification (SAS emoji flow), recovery-key
backup/restore, membership announcement moderation, member/room avatars,
power-level-aware moderation menu, and an encryption lock indicator on
the composer - verified live with two independent devices.
- Replace the single combined join panel with a dedicated page per
protocol (IrcJoinPanel/MatrixJoinPanel/DiscordJoinPanel), routed by
whichever account the "+" button was clicked for rather than listing
every account in one expandable page.
- Discord: friends list (Online/All, click-to-DM, add-friend, matches
READY's relationships + live PRESENCE_UPDATE), create-a-server, and
join-by-invite alongside the existing DM-by-ID flow.
- Discord: an expired attachment-link fallback ("Open in Discord") for
scrollback media whose signed cdn.discordapp.com URL has lapsed -
Discord's own /attachments/refresh-urls re-sign endpoint hard-rejects
this backend's user-token requests, confirmed live, so this opens the
real message instead of showing a broken embed.
- Discord: detect gateway close code 4004 (revoked token) as a distinct
ConnState::AuthFailed instead of retrying a dead credential forever;
the Accounts pane shows a clear "needs login" state that routes
straight to a fresh QR login, which upserts the same account id in
place (no lost buffers/scrollback).
- Fix the unread-DM pinned-buffer flash never reliably triggering: an
implicit property binding computed via nested closures over two
separate properties wasn't consistently re-evaluated by QML's
dependency tracker; replaced with an explicit recompute function called
at every actual mutation point.
- Fix ChatBarWidget.qml's pill double-padding against BasePill's own
padding.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
222 lines
7.6 KiB
QML
222 lines
7.6 KiB
QML
import QtQuick
|
|
import QtQuick.Layouts
|
|
import qs.Common
|
|
import qs.Widgets
|
|
import "../settings"
|
|
|
|
// Structured by service so adding Matrix/Discord/Slack later means adding
|
|
// another rail entry + its own ProtocolSettingsPanel.qml, not redesigning
|
|
// this file - mirrors DMS's own global Settings pane (a left-hand list of
|
|
// categories that swaps a content pane on the right), just sized for a
|
|
// handful of protocols instead of dozens of categories, so no search or
|
|
// keyboard navigation is needed here.
|
|
//
|
|
// Deliberately anchor-based rather than a RowLayout: a RowLayout's
|
|
// fillWidth allocation for the content Loader turned out to depend on the
|
|
// loaded panel's own (sometimes not-yet-settled) implicit width, which
|
|
// could shove the rail/divider sideways once a wide panel loaded. Fixed
|
|
// widths + anchors for the rail and divider can't move regardless of what
|
|
// the loaded panel does; the content area also clips, so even a
|
|
// misbehaving panel can only get cut off, never push the divider around.
|
|
Item {
|
|
id: root
|
|
|
|
property var pluginService: null
|
|
property string selectedProtocol: "display"
|
|
// Only Sneedchat's settings panel needs these so far (it edits live
|
|
// account config, unlike the other panels' pure local preferences) -
|
|
// threaded through from ChatView.qml the same way AccountsPanel.qml
|
|
// already receives them.
|
|
property var accounts: []
|
|
|
|
signal sockchatRoomsChanged(string accountId, var rooms)
|
|
signal torConfigChanged(string torMode, string proxy)
|
|
signal regenerateTorCircuitRequested()
|
|
signal restartTorFromScratchRequested()
|
|
|
|
readonly property var protocols: [
|
|
{ "id": "display", "label": "Display", "available": true },
|
|
{ "id": "irc", "label": "IRC", "available": true },
|
|
{ "id": "sockchat", "label": "Sneedchat", "available": true },
|
|
{ "id": "tor", "label": "Tor", "available": true },
|
|
{ "id": "matrix", "label": "Matrix", "available": true },
|
|
{ "id": "discord", "label": "Discord", "available": false },
|
|
{ "id": "slack", "label": "Slack", "available": false }
|
|
]
|
|
|
|
implicitHeight: layoutRow.height
|
|
height: implicitHeight
|
|
|
|
Item {
|
|
id: layoutRow
|
|
width: parent.width
|
|
height: Math.max(rail.implicitHeight, panelContainer.height)
|
|
|
|
ColumnLayout {
|
|
id: rail
|
|
anchors.left: parent.left
|
|
anchors.top: parent.top
|
|
width: 130
|
|
spacing: Theme.spacingXXS
|
|
|
|
Repeater {
|
|
model: root.protocols
|
|
|
|
Rectangle {
|
|
id: railRow
|
|
required property var modelData
|
|
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 32
|
|
radius: Theme.cornerRadius
|
|
color: {
|
|
if (root.selectedProtocol === modelData.id)
|
|
return Theme.primaryContainer;
|
|
if (railHover.hovered)
|
|
return Theme.surfaceHover;
|
|
return "transparent";
|
|
}
|
|
|
|
HoverHandler {
|
|
id: railHover
|
|
}
|
|
|
|
StyledText {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingS
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: railRow.modelData.available ? railRow.modelData.label : (railRow.modelData.label + " (soon)")
|
|
color: root.selectedProtocol === railRow.modelData.id ? Theme.primary : (railRow.modelData.available ? Theme.surfaceText : Theme.surfaceVariantText)
|
|
font.weight: root.selectedProtocol === railRow.modelData.id ? Font.Medium : Font.Normal
|
|
wrapMode: Text.NoWrap
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: root.selectedProtocol = railRow.modelData.id
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: divider
|
|
anchors.left: rail.right
|
|
anchors.leftMargin: Theme.spacingM
|
|
anchors.top: parent.top
|
|
width: 1
|
|
height: layoutRow.height
|
|
color: Theme.outlineMedium
|
|
}
|
|
|
|
Item {
|
|
id: panelContainer
|
|
anchors.left: divider.right
|
|
anchors.leftMargin: Theme.spacingM
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
height: panelLoader.item ? panelLoader.item.implicitHeight : 0
|
|
clip: true
|
|
|
|
Loader {
|
|
id: panelLoader
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
sourceComponent: {
|
|
if (root.selectedProtocol === "display")
|
|
return displayPanelComponent;
|
|
if (root.selectedProtocol === "irc")
|
|
return ircPanelComponent;
|
|
if (root.selectedProtocol === "sockchat")
|
|
return sockchatPanelComponent;
|
|
if (root.selectedProtocol === "tor")
|
|
return torPanelComponent;
|
|
if (root.selectedProtocol === "matrix")
|
|
return matrixPanelComponent;
|
|
return comingSoonComponent;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: displayPanelComponent
|
|
|
|
DisplaySettingsPanel {
|
|
width: parent.width
|
|
pluginService: root.pluginService
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: ircPanelComponent
|
|
|
|
IrcSettingsPanel {
|
|
width: parent.width
|
|
pluginService: root.pluginService
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: sockchatPanelComponent
|
|
|
|
SneedchatSettingsPanel {
|
|
width: parent.width
|
|
accounts: root.accounts
|
|
onRoomsChanged: (accountId, rooms) => root.sockchatRoomsChanged(accountId, rooms)
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: torPanelComponent
|
|
|
|
TorSettingsPanel {
|
|
width: parent.width
|
|
accounts: root.accounts
|
|
onTorConfigChanged: (torMode, proxy) => root.torConfigChanged(torMode, proxy)
|
|
onRegenerateCircuitRequested: root.regenerateTorCircuitRequested()
|
|
onRestartFromScratchRequested: root.restartTorFromScratchRequested()
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: matrixPanelComponent
|
|
|
|
MatrixSettingsPanel {
|
|
width: parent.width
|
|
pluginService: root.pluginService
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: comingSoonComponent
|
|
|
|
ColumnLayout {
|
|
width: parent.width
|
|
spacing: Theme.spacingXS
|
|
|
|
StyledText {
|
|
Layout.fillWidth: true
|
|
text: {
|
|
const proto = root.protocols.find(p => p.id === root.selectedProtocol);
|
|
return proto ? proto.label : "";
|
|
}
|
|
font.pixelSize: Theme.fontSizeLarge
|
|
font.weight: Font.Bold
|
|
color: Theme.surfaceVariantText
|
|
}
|
|
|
|
StyledText {
|
|
Layout.fillWidth: true
|
|
text: "Not connected yet - this will get its own settings here once its backend exists, the same way IRC's does."
|
|
wrapMode: Text.WordWrap
|
|
color: Theme.surfaceVariantText
|
|
}
|
|
}
|
|
}
|
|
}
|