Public Access
Merge branch 'claude/trust-connect-popup-56abec'
This commit is contained in:
@@ -17,6 +17,13 @@
|
||||
and the plan names presenting it as dismissible as the one design mistake that matters here. A phone
|
||||
makes this easy to get wrong, because a bottom sheet is swipe-to-dismiss by convention; this is not a
|
||||
sheet for that reason, it is a full-screen panel.
|
||||
|
||||
◆ WHAT IS BEHIND IT IS NO LONGER ALWAYS THE HOST LIST. The shell used to move to HOSTS before letting a
|
||||
handshake ask this, because the desktop head drew the question as a banner on that screen; both heads draw
|
||||
it over the surface now, so this is raised over whatever the user was on — including the connect box on the
|
||||
Connections surface, which is where a machine that is in no keychain at all is dialled from. Nothing here
|
||||
changes for that, and that is the point: this control never knew which screen it was over. See
|
||||
MainWindowViewModel.OnVaultConnectionFailed and HostKeyCard.axaml on the desktop.
|
||||
-->
|
||||
|
||||
<Panel>
|
||||
@@ -107,8 +114,15 @@
|
||||
<!--
|
||||
The only control on the screen, and it goes back rather than forward. Compare the sheet above,
|
||||
where the accent button connects.
|
||||
|
||||
◆ IT SAID "BACK TO HOSTS", AND IT NEITHER WENT THERE NOR CAME BACK. Two things were wrong with
|
||||
it at once. RejectHostKey cleared only the pending key, so pressing it left HasHostKeyMismatch
|
||||
set and this opaque panel up over whatever the user navigated to next — including the host editor
|
||||
the paragraph above sends them to. And the shell no longer moves to Hosts to ask the question, so
|
||||
even fixed it does not land there: it lowers this panel and reveals the screen that was already
|
||||
underneath. "BACK" is what that is. See VaultViewModel.RejectHostKey.
|
||||
-->
|
||||
<Button Classes="secondary" Content="BACK TO HOSTS" Margin="0,24,0,0"
|
||||
<Button Classes="secondary" Content="BACK" Margin="0,24,0,0"
|
||||
Command="{Binding RejectHostKeyCommand}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
@@ -256,6 +256,12 @@
|
||||
◆ The two host-key decisions, over everything. Outside the screen Panel deliberately: file transfer
|
||||
makes its own trust decision on the desktop, and the phone now has that screen — so a sheet nested
|
||||
inside the hosts screen would be one the files screen could not raise.
|
||||
|
||||
◆ And that arrangement is what the shell now relies on rather than merely permits. It used to move to
|
||||
HOSTS before letting a handshake ask the question, because the desktop head drew it as a banner on that
|
||||
screen; it does not any more, so this sheet is what is over the Connections surface when a machine
|
||||
typed into the connect box by hand turns out to be a first contact. Which is the case the navigation
|
||||
was worst for — it took the box away. See MainWindowViewModel.OnVaultConnectionFailed.
|
||||
-->
|
||||
<Panel Grid.Row="1">
|
||||
<views:HostKeySheet DataContext="{Binding Vault}" />
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
||||
x:Class="DodoSSH.Client.App.Views.HostKeyCard"
|
||||
x:DataType="vm:VaultViewModel">
|
||||
|
||||
<!--
|
||||
The two host-key decisions, over whatever the window happens to be showing. The desktop counterpart of
|
||||
the phone's HostKeySheet, and one control for both answers for the same reason: they are one decision
|
||||
point, the view models make them mutually exclusive, and the shell must not be able to show both.
|
||||
|
||||
── IT WAS TWO BANNERS AT THE TOP OF THE HOSTS SCREEN. ───────────────────────────────────────────────
|
||||
Which is why the shell used to navigate there before letting the vault raise the question: a connection
|
||||
can be started from the palette on any screen, and a prompt behind the screen somebody is looking at is a
|
||||
prompt nobody can answer. The navigation covered that, and paid for it everywhere else — it took the user
|
||||
off the screen they were working on, and for the one connection that has no host at all it judged a
|
||||
machine typed in by hand on the list it is deliberately not on. See MainWindowViewModel's own note on
|
||||
OnVaultConnectionFailed.
|
||||
|
||||
Drawn over the surface instead, so nothing has to move and the palette's case needs no special one.
|
||||
|
||||
── THE OCCLUSION RULE, AND THIS IS THE CONTROL THAT MOST NEEDS IT. ──────────────────────────────────
|
||||
A second connection can be made while a first one is open, so this card is routinely raised over a live
|
||||
terminal — and that rectangle is a native child window which composites above everything Avalonia draws.
|
||||
A card layered over it would be sliced at its left edge with TRUST AND CONNECT unreachable, which for the
|
||||
most safety-critical question in the product is the worst place for that class of bug to land. The shell
|
||||
collapses the WebView while this is up; see MainWindowViewModel.IsHostKeyDecisionShowing.
|
||||
|
||||
── ◆ WHAT IS DELIBERATELY DIFFERENT BETWEEN THE TWO HALVES. ─────────────────────────────────────────
|
||||
First contact offers TRUST AND CONNECT, because deciding whether a fingerprint is the one the operator
|
||||
published is a judgement a person is entitled to make and is the only moment they can make it.
|
||||
|
||||
A changed key offers no way forward at all: no continue, no "connect anyway", and nothing that turns
|
||||
dismissing it into a connection. The only way past it is FORGET HOST KEY in the host's own editor, which
|
||||
is a deliberate act performed somewhere else — a button next to this warning would be "continue anyway"
|
||||
with two clicks instead of one. The phone's sheet carries the same note, and calls presenting this as
|
||||
dismissible the one design mistake here that matters.
|
||||
-->
|
||||
|
||||
<!--
|
||||
A wash with no press handler, and the omission is the point: every other overlay in this window closes by
|
||||
clicking away from it, and a question with two named answers must not be answerable by missing. The
|
||||
palette's own backdrop is the control to compare with — see QuickConnect.
|
||||
|
||||
80% of Canvas, written out because a scrim is a brush with an alpha and the palette holds no alpha
|
||||
variant of a surface; the pre-multiplied ones there are accent washes.
|
||||
-->
|
||||
<Border Background="#CC0E1220">
|
||||
<Panel>
|
||||
|
||||
<!-- ============ UNKNOWN HOST KEY ============ -->
|
||||
<Border Classes="card" IsVisible="{Binding HasPendingHostKey}"
|
||||
BorderBrush="{StaticResource WarnSoft}">
|
||||
<StackPanel Spacing="10">
|
||||
|
||||
<TextBlock Classes="label" Text="UNKNOWN HOST KEY" Foreground="{StaticResource Warn}" />
|
||||
|
||||
<TextBlock Foreground="{StaticResource WarnText}" TextWrapping="Wrap">
|
||||
<Run Text="First contact with" />
|
||||
<Run Text="{Binding PendingHostKey.Host}" Foreground="{StaticResource Text}" />
|
||||
<Run Text="·" />
|
||||
<Run Text="{Binding PendingHostKey.Port}" />
|
||||
<Run Text=". Nothing in this keychain has approved this key." />
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock Classes="label" FontSize="10" Text="{Binding PendingHostKey.Algorithm}" />
|
||||
|
||||
<!--
|
||||
In full, wrapping rather than trimmed, and selectable. A fingerprint exists to be compared
|
||||
character by character against one an operator published, and the two things that ruin that are
|
||||
an ellipsis in the middle and a line you cannot copy.
|
||||
-->
|
||||
<Border Background="{StaticResource Field}" BorderBrush="{StaticResource BorderMid}"
|
||||
BorderThickness="1" CornerRadius="4" Padding="10,8">
|
||||
<SelectableTextBlock Classes="mono" FontSize="12" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource Warn}"
|
||||
Text="{Binding PendingHostKey.Fingerprint}" />
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="12"
|
||||
Text="Compare it with the fingerprint the server's operator published before trusting it. Trusting pins it for this keychain, on every machine." />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="accent" Content="TRUST AND CONNECT"
|
||||
Command="{Binding TrustHostKeyCommand}" />
|
||||
<Button Classes="ghost" Content="CANCEL" Command="{Binding RejectHostKeyCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ============ CHANGED HOST KEY ============ -->
|
||||
<Border Classes="card" IsVisible="{Binding HasHostKeyMismatch}"
|
||||
BorderBrush="{StaticResource DangerSoft}">
|
||||
<StackPanel Spacing="10">
|
||||
|
||||
<TextBlock Classes="label" Text="HOST KEY CHANGED" Foreground="{StaticResource Danger}" />
|
||||
|
||||
<TextBlock Foreground="{StaticResource Danger}" FontWeight="SemiBold" TextWrapping="Wrap"
|
||||
Text="The host key changed and the connection was refused." />
|
||||
|
||||
<!--
|
||||
The whole explanation comes from the view model, which already composes it — which host, both
|
||||
fingerprints, and which of them this keychain has pinned. Rewriting it here would be a second
|
||||
copy of the most safety-critical sentence in the product, in a second place to forget.
|
||||
-->
|
||||
<Border Background="{StaticResource Field}" BorderBrush="{StaticResource BorderMid}"
|
||||
BorderThickness="1" CornerRadius="4" Padding="10,8">
|
||||
<SelectableTextBlock Classes="mono" FontSize="12" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource DangerText}"
|
||||
Text="{Binding HostKeyMismatch}" />
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="12" Foreground="{StaticResource WarnText}"
|
||||
Text="If this server was rebuilt on purpose, edit the host and choose FORGET HOST KEY, then connect again. There is deliberately no way to continue from here." />
|
||||
|
||||
<!--
|
||||
One button, and it goes nowhere. It puts the refusal away so the host's editor can be reached; it
|
||||
is not an answer, because this half of the control has none to give. Compare the accent button
|
||||
above, which connects.
|
||||
-->
|
||||
<Button Classes="ghost" Content="CLOSE" HorizontalAlignment="Left"
|
||||
Command="{Binding RejectHostKeyCommand}" />
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,20 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>
|
||||
/// The two host-key decisions, drawn over whatever the window is showing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Its data context is the <c>VaultViewModel</c>, so every binding in the markup is a property of the vault;
|
||||
/// which of the two halves draws is decided there and the two are mutually exclusive. Whether it is on screen
|
||||
/// at all is the shell's business — see <c>MainWindowViewModel.IsHostKeyDecisionShowing</c>, which is also
|
||||
/// what collapses the terminal's WebView underneath it.
|
||||
///
|
||||
/// In its own file rather than in the window, like every other card here, because nothing inside that window
|
||||
/// can be laid out by a test: WebView2's adapter refuses the headless session's thread.
|
||||
/// </remarks>
|
||||
internal sealed partial class HostKeyCard : UserControl
|
||||
{
|
||||
public HostKeyCard() => InitializeComponent();
|
||||
}
|
||||
@@ -89,39 +89,19 @@
|
||||
<StackPanel Grid.Row="2">
|
||||
|
||||
<!--
|
||||
Host key prompts. Unknown and changed look deliberately different: one is a decision, the other is
|
||||
a refusal. Presenting a changed key with a "continue" button is how users are taught to click
|
||||
through the one warning that matters.
|
||||
-->
|
||||
<Border Padding="16,10" Background="{StaticResource WarnWash}"
|
||||
BorderBrush="{StaticResource WarnSoft}" BorderThickness="0,0,0,1"
|
||||
IsVisible="{Binding HasPendingHostKey}">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="This host has not been seen before. Check the fingerprint against what the server's operator published."
|
||||
Foreground="{StaticResource WarnText}" TextWrapping="Wrap" />
|
||||
<SelectableTextBlock Classes="mono" Text="{Binding PendingHostKey.Fingerprint}"
|
||||
Foreground="{StaticResource Warn}" TextWrapping="Wrap" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="accent" Content="TRUST AND CONNECT"
|
||||
Command="{Binding TrustHostKeyCommand}" />
|
||||
<Button Classes="ghost" Content="CANCEL"
|
||||
Command="{Binding RejectHostKeyCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
◆ THE TWO HOST KEY PROMPTS WERE HERE, and they are now drawn over the window instead.
|
||||
|
||||
<Border Padding="16,10" Background="{StaticResource DangerWash}"
|
||||
BorderBrush="{StaticResource DangerSoft}" BorderThickness="0,0,0,1"
|
||||
IsVisible="{Binding HasHostKeyMismatch}">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="The host key changed and the connection was refused."
|
||||
Foreground="{StaticResource Danger}" FontWeight="SemiBold" />
|
||||
<SelectableTextBlock Text="{Binding HostKeyMismatch}"
|
||||
Foreground="{StaticResource Danger}" TextWrapping="Wrap" />
|
||||
<TextBlock Text="If the server was legitimately rebuilt, edit the host and choose "Forget host key" first. There is deliberately no way to continue from here."
|
||||
Foreground="{StaticResource WarnText}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
They were banners on this screen, so the shell navigated to this screen before letting the vault
|
||||
raise one — because a connection can be started from the palette on any screen, and a question
|
||||
behind whatever somebody is looking at is a question nobody can answer. That covered the palette
|
||||
and charged everybody else for it: it took the user off the screen they were working on, and for a
|
||||
machine typed into the phone's connect box by hand it asked about it on the list it is deliberately
|
||||
not on. The decision is over the surface now, both heads, and nothing has to move. See
|
||||
HostKeyCard.axaml and MainWindowViewModel.OnVaultConnectionFailed.
|
||||
|
||||
Not copied into both places, deliberately. One prompt in two markups is two copies of the most
|
||||
safety-critical wording in the product, and the second is the one that goes stale.
|
||||
-->
|
||||
|
||||
<!--
|
||||
The conflict log. The merge is only allowed to pick a winner because the value it overrode is kept
|
||||
|
||||
@@ -348,6 +348,22 @@
|
||||
-->
|
||||
<views:QuickConnect x:Name="Palette" IsVisible="{Binding IsSearching}" />
|
||||
|
||||
<!--
|
||||
◆ Later still, and the order between these two is a ranking rather than an accident: the palette is
|
||||
something the user opened and can close, and a host-key decision is a connection waiting on them. It
|
||||
used to be two banners at the top of the hosts screen, which is why the shell navigated there before
|
||||
the question could be asked — see HostKeyCard.axaml, and MainWindowViewModel.OnVaultConnectionFailed
|
||||
for what that cost every other screen.
|
||||
|
||||
Wrapped, like every other child here whose data context is the vault: the visibility is the shell's
|
||||
business — it is the flag that also collapses the WebView — and IsHostKeyDecisionShowing does not
|
||||
exist on a VaultViewModel, which with compiled bindings is a build error rather than a card that
|
||||
silently never appears.
|
||||
-->
|
||||
<Panel IsVisible="{Binding IsHostKeyDecisionShowing, FallbackValue=False}">
|
||||
<views:HostKeyCard x:Name="HostKeyPane" DataContext="{Binding Vault}" />
|
||||
</Panel>
|
||||
|
||||
</Panel>
|
||||
|
||||
<!--
|
||||
|
||||
@@ -947,7 +947,14 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
/// same rectangle is drawn underneath it and its buttons cannot be clicked. Anything that covers the
|
||||
/// terminal's area has to collapse the terminal instead, and that is every one of the conditions here: a
|
||||
/// locked vault (the unlock card), the page area (every screen uses the full width), the quick-connect
|
||||
/// palette, and the phone's connect sheet.
|
||||
/// palette, the phone's connect sheet, and the host-key decision.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The host-key decision is the newest of them, and the one that made it a rule rather than a list.</b>
|
||||
/// It is drawn over whatever is on screen at the moment a handshake asks the question — including an open
|
||||
/// terminal, because a second connection can be being made while a first one is being typed into. Without
|
||||
/// this condition the two buttons that answer the most safety-critical question in the product would be
|
||||
/// sliced at the WebView's left edge and take no clicks. See <see cref="IsHostKeyDecisionShowing"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The sheet is here rather than in <see cref="IsTerminalSurface"/>, and the palette is not.</b> The
|
||||
@@ -984,7 +991,36 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal bool IsTerminalShowing =>
|
||||
IsTerminalSurface && !IsConnectSheetOpen && SelectedTab is { HasSession: true };
|
||||
IsTerminalSurface
|
||||
&& !IsConnectSheetOpen
|
||||
&& !IsHostKeyDecisionShowing
|
||||
&& SelectedTab is { HasSession: true };
|
||||
|
||||
/// <summary>
|
||||
/// Whether a host key is waiting to be judged, over whatever the user is looking at.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>It is asked of the shell rather than of a screen because the answer decides an occlusion, and
|
||||
/// because the question is no longer tied to a screen.</b> The prompt used to be a banner at the top of
|
||||
/// the hosts screen, and the shell navigated there before letting the vault raise it — which is how a
|
||||
/// machine typed into the phone's own connect box came to be judged on a list it is deliberately not on.
|
||||
/// Both heads now draw the decision over whichever surface was showing when the handshake stopped, so
|
||||
/// there is nowhere it can be asked from that it cannot be answered on, and nothing has to move.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Two states, one flag, and the views keep them apart: an unknown key is a decision with two buttons and
|
||||
/// a changed one is a refusal with no way forward. What they share is that both cover the rectangle the
|
||||
/// terminal would be in. See <c>HostKeyCard.axaml</c> on the desktop and <c>HostKeySheet.axaml</c> on the
|
||||
/// phone.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The transfers screen's own copy of this question is not here, and that is not an omission: file
|
||||
/// transfer's prompt is drawn inside that screen, which is a page, so it occludes nothing.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal bool IsHostKeyDecisionShowing =>
|
||||
IsUnlocked && Vault is { HasPendingHostKey: true } or { HasHostKeyMismatch: true };
|
||||
|
||||
/// <summary>
|
||||
/// Whether the terminal half of the window is the half being shown, pane or no pane.
|
||||
@@ -1623,13 +1659,15 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
|
||||
CloseSearch();
|
||||
|
||||
// The hosts page, because that is where this connection's questions get asked. An unknown or changed
|
||||
// host key is answered by a prompt drawn on that page and the palette opens from any screen, so
|
||||
// connecting from the files screen without this would leave the question behind the screen that asked
|
||||
// it. The surface does not stay here — the tab that appears for the attempt takes it — and it does not
|
||||
// need to: a refusal that needs an answer puts the page back, which is where this leaves the screen.
|
||||
Screen = ShellScreen.Hosts;
|
||||
Surface = ShellSurface.Page;
|
||||
// ◆ IT USED TO GO TO THE HOSTS PAGE FIRST, and the reason it did no longer exists. An unknown or
|
||||
// changed host key was answered by a prompt drawn on that page, and the palette opens from any screen,
|
||||
// so connecting from the files screen without the jump would have left the question behind the screen
|
||||
// that asked it. Both heads draw the decision over the surface now — see IsHostKeyDecisionShowing —
|
||||
// and the other thing this connection can say before it dials, a refusal on Vault.Status, is in the
|
||||
// status bar, which is a row of the window rather than part of a screen.
|
||||
//
|
||||
// So the palette leaves the user where they were, which is the whole point of a palette: Ctrl+K over a
|
||||
// transfer that is still running should not cost the transfer's screen.
|
||||
vault.SelectedHost = vault.Hosts.FirstOrDefault(host => host.EntityId == row.EntityId);
|
||||
|
||||
// Null, not the token. A [RelayCommand] over a method whose only parameter is a CancellationToken
|
||||
@@ -2815,12 +2853,27 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
OnPropertyChanged(nameof(HasRecentConnections));
|
||||
|
||||
RaiseSyncState();
|
||||
|
||||
// The host-key decision is a property of the vault, so swapping the vault out is one of the ways it
|
||||
// stops being on screen — locking with a question still up is the case, and it is reachable: a
|
||||
// handshake that stopped on an unknown key does not hold the window, so the lock button is live
|
||||
// behind the card asking about it.
|
||||
RaiseTerminalState();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// One property is watched rather than all of them: the titlebar's sync state is the vault's outbox
|
||||
/// depth, which lives on the vault, and re-raising the shell's two derived properties on every
|
||||
/// notification a busy vault produces would repaint the titlebar on every keystroke in an editor.
|
||||
/// <para>
|
||||
/// Named properties are watched rather than all of them: the titlebar's sync state is the vault's outbox
|
||||
/// depth, which lives on the vault, and re-raising the shell's derived properties on every notification a
|
||||
/// busy vault produces would repaint the titlebar on every keystroke in an editor.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The two host-key flags are here because the decision the vault raises collapses this shell's WebView —
|
||||
/// see <see cref="IsHostKeyDecisionShowing"/>. It is a property rather than an event because it is a
|
||||
/// state the vault is in and not a moment: it is entered by a refused handshake, left by either answer,
|
||||
/// and cleared outright by forgetting a pin from a host's editor, which is a third caller that has
|
||||
/// nothing to do with connecting.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void OnVaultPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
@@ -2829,6 +2882,14 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
{
|
||||
RaiseSyncState();
|
||||
}
|
||||
|
||||
if (string.Equals(
|
||||
e.PropertyName, nameof(VaultViewModel.HasPendingHostKey), StringComparison.Ordinal)
|
||||
|| string.Equals(
|
||||
e.PropertyName, nameof(VaultViewModel.HasHostKeyMismatch), StringComparison.Ordinal))
|
||||
{
|
||||
RaiseTerminalState();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnVaultHostsChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
|
||||
@@ -2913,11 +2974,27 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
/// Answers a connection that did not become a session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Two outcomes, because there are two kinds of not-connecting. A refusal stays in the strip as a tab
|
||||
/// carrying its reason — connecting no longer holds the window, so the user may be three screens away by
|
||||
/// now, and the status line they are not looking at is not where a failure should end. A host key
|
||||
/// question is not a refusal: it is a prompt on the hosts screen, so the tab goes and the window is put
|
||||
/// back where the question is being asked.
|
||||
/// now, and the status line they are not looking at is not where a failure should end. A host key question
|
||||
/// is not a refusal: nothing has gone wrong and there is nothing to report, so the tab goes and the
|
||||
/// decision takes its place until it is answered.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>It used to move the window, and that was the wrong half of the problem to solve.</b> The prompt was
|
||||
/// a banner on the hosts screen, so this navigated there — <c>Screen = Hosts</c>, <c>Surface = Page</c> —
|
||||
/// on the reasoning that a connection can be started from the palette on any screen and a question behind
|
||||
/// the screen somebody is looking at is a question nobody can answer. True, and answered the wrong way
|
||||
/// round: what it did to the one connection that has no host at all was to judge a machine typed into the
|
||||
/// phone's connect box on a list it is deliberately not on, after taking the box away.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Both heads now draw the decision over whatever is showing, so nothing has to move and the palette's
|
||||
/// case is covered without a special one. What is left here is the tab, and the surface it was on stays as
|
||||
/// it is — for the phone that surface <em>is</em> the connect box, and it is what the user comes back to
|
||||
/// whichever way they answer.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void OnVaultConnectionFailed(object? sender, ConnectionFailedEventArgs e)
|
||||
{
|
||||
@@ -2945,11 +3022,11 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
: Tabs[Math.Clamp(index - 1, 0, Tabs.Count - 1)];
|
||||
}
|
||||
|
||||
// The screen the question is drawn on, and the page rather than a terminal. A connection can be
|
||||
// started from the palette on any screen, so without this the prompt would be behind whatever the
|
||||
// user was looking at, with the connection waiting on an answer they cannot reach.
|
||||
Screen = ShellScreen.Hosts;
|
||||
Surface = ShellSurface.Page;
|
||||
// Nothing else. The decision is drawn over the surface the user is on rather than on a screen they
|
||||
// have to be taken to — see the remark — and the neighbour selected above is what that surface shows
|
||||
// once the question is answered. Its WebView is collapsed while the card is up, which is the one
|
||||
// thing a card in that rectangle cannot do for itself; see IsHostKeyDecisionShowing.
|
||||
RaiseTerminalState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -3344,6 +3421,7 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
OnPropertyChanged(nameof(IsTerminalSurface));
|
||||
OnPropertyChanged(nameof(IsTerminalShowing));
|
||||
OnPropertyChanged(nameof(IsConnectingShowing));
|
||||
OnPropertyChanged(nameof(IsHostKeyDecisionShowing));
|
||||
|
||||
// The tabs themselves, and not only the window's own flags. A tab that stayed lit after the user
|
||||
// navigated to preferences would be a second "you are here" mark pointing at a terminal that is not
|
||||
|
||||
@@ -9960,12 +9960,37 @@ internal sealed partial class VaultViewModel(
|
||||
await AutoSyncAsync(cancellationToken).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
/// <summary>Dismisses the trust prompt without pinning anything.</summary>
|
||||
/// <summary>
|
||||
/// Dismisses whichever host-key decision is showing, without pinning or forgetting anything.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// One command for both, because both are dismissals and the two states are mutually exclusive — which is
|
||||
/// what <c>TransfersViewModel.RejectHostKey</c> has always done for the same pair of cards. The mismatch
|
||||
/// has nothing else it may offer: withdrawing a pin is a deliberate act performed in the host's editor,
|
||||
/// away from the moment of connecting, and a button here would be "continue anyway" with two clicks
|
||||
/// instead of one.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>It used to clear only the pending key, which left the mismatch with no working exit.</b> That was
|
||||
/// invisible for as long as the refusal was a banner at the top of a screen — nothing was drawn over, so
|
||||
/// nothing was trapped, and the next connection attempt cleared it. It is not invisible now: both heads
|
||||
/// draw the refusal over the surface, and the phone's is an opaque full-screen panel whose one button runs
|
||||
/// this command. Leaving <see cref="HostKeyMismatch"/> set would have kept that panel up over every screen
|
||||
/// the user went to afterwards, including the host editor they were being sent to.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
private void RejectHostKey()
|
||||
{
|
||||
var wasOffered = PendingHostKey is not null;
|
||||
|
||||
PendingHostKey = null;
|
||||
Status = "The host key was not trusted, so nothing was connected.";
|
||||
HostKeyMismatch = null;
|
||||
|
||||
Status = wasOffered
|
||||
? "The host key was not trusted, so nothing was connected."
|
||||
: "Nothing was connected.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user