Files
dms-chat/views/ChatSettings.qml
SalastilandClaude Sonnet 5 20389e7396 Add Matrix verification/backup, per-protocol join pages, Discord friends, and expired-media/auth-failure recovery
- 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]>
2026-08-21 12:30:46 -04:00

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