Public Access
376 lines
23 KiB
XML
376 lines
23 KiB
XML
<UserControl xmlns="https://github.com/avaloniaui"
|
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
|
xmlns:views="using:DodoSSH.Client.Android.Views"
|
|
xmlns:contracts="using:DodoSSH.Contracts"
|
|
x:Class="DodoSSH.Client.Android.Views.VaultsScreen"
|
|
x:DataType="vm:VaultsViewModel"
|
|
Background="{StaticResource Canvas}">
|
|
|
|
<!--
|
|
VAULTS, under MORE — and the one screen behind that hub the v2 phone design never drew.
|
|
|
|
It is the reverse of every other entry in docs/design-import-gaps.md: a shipped screen the design had
|
|
no slot for, rather than a drawn screen with nothing behind it. It is on the phone because a vault
|
|
arrives without being asked for — somebody wraps its key to you from their machine — and the person it
|
|
arrives for is at least as likely to be holding a phone as sitting at a desktop. A vault the server has
|
|
just put somebody into, visible only on a head they may never have installed, is a membership they
|
|
cannot see.
|
|
|
|
── IT LISTED TEAMS UNTIL THE SCREEN STOPPED BEING ABOUT THEM. ───────────────────────────────────────
|
|
The rows are vaults now, and the members under one are the people that vault is shared with. Nothing
|
|
about the server changed — it still authorises through a team — and what went is the step where
|
|
somebody had to make one before they could share anything. See VaultsViewModel.
|
|
|
|
So there is no mock-up to depart from. What this departs from instead is the desktop screen over the
|
|
same view model, and every difference below is a phone difference rather than a second opinion.
|
|
|
|
**The desktop's two columns are one.** A 268-pixel vault list beside a members table does not exist at
|
|
360dp, so the lists stack in one scrolling column with the vaults at the top. That is the same thing
|
|
HOSTS does with the desktop's sidebar and its connect column, and for the same reason.
|
|
|
|
**Nothing scrolls inside anything.** The desktop caps its members list at 240 pixels so several can sit
|
|
above each other in one pane. Here every list is sized to its content and the screen's own ScrollViewer
|
|
does all of the scrolling: a list that scrolls inside a page is a region a thumb has to find the edges
|
|
of, and three of them on one screen is three ways to get stuck.
|
|
|
|
◆ **SHARE KEY is drawn and nothing that takes something away is.** That is a decision rather than a
|
|
subset. Wrapping a vault key is the one act on this screen a server cannot perform at all — it needs a
|
|
machine that already holds the key, and this phone is one — so a vaults screen that could only be read
|
|
would leave the product's central claim undemonstrated on the head most people carry. REMOVE and
|
|
WITHDRAW KEY are the other half of that, and both act on the first press: the view model's
|
|
armed-confirmation state covers handing a vault over and nothing else. The desktop guards
|
|
them with a tooltip instead, which is a control a touch screen has no way to show. An irreversible
|
|
revocation under a thumb with its explanation missing is the wrong trade, so both stay on the
|
|
desktop — where the sentence beside them is visible. Handing a vault over is not drawn either, for a
|
|
plainer reason: it decides who controls the vault, which is not a thing to do while walking. Nor is
|
|
renaming, which is a keyboard on a screen that is otherwise all reading.
|
|
|
|
◆ **ADD is drawn, where it was not.** The argument against it was that it is an address typed into a
|
|
box, a directory lookup, a role picker and a paragraph saying what adding somebody did *not* do — five
|
|
controls for an act a colleague at a desktop is already performing. That was a cost argument and it was
|
|
wrong about who is holding what: the person who needs to let a colleague into a vault is often the one
|
|
away from their desk, and telling them to go and find one is the same answer this head exists to stop
|
|
giving. Making a vault was already here on exactly that reasoning.
|
|
|
|
What it costs is four rows under the members list rather than five controls, because three of them are
|
|
the role and are chips rather than a picker — the same chips the host editor wears for tags, so the
|
|
choice is visible without being opened. It is gated on being able to administer the vault, so on a
|
|
membership where the answer would be a refusal there is nothing to press.
|
|
|
|
**The paragraph comes with it and is not the optional part.** Adding somebody is a change to what the
|
|
server will serve and nothing else; the key still has to be wrapped from a machine that holds it, which
|
|
on this head is the bar below. A screen that offered the first and stayed quiet about the second would
|
|
be implying the server can hand out access, which is the one claim this product is built to refuse.
|
|
|
|
**The key-holder list is not drawn.** It is a third list, and what the phone can answer about a vault
|
|
is the more useful half of the same question and is on the vault's own row: whether *this* machine can
|
|
open it.
|
|
|
|
**↻ and `+` both, because this screen has more reason to re-read than any other.** Who is in a vault is
|
|
not cached — it is read from the server on arrival and again at the end of every command — so the one
|
|
thing a member cannot otherwise see is a change somebody else just made: a vault key wrapped to them
|
|
from a colleague's desktop, or a vault they have this moment been added to. On the desktop the
|
|
re-read is leaving the rail and coming back, which is one click. Here it is a trip out to MORE and
|
|
back, so the button earns its place. It binds to a real command rather than to ShowScreen(Vaults),
|
|
which would set Screen to the value it already holds, raise nothing and reload nothing.
|
|
-->
|
|
|
|
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto">
|
|
|
|
<!-- ============ header ============ -->
|
|
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto,Auto" Height="56" Margin="8,0">
|
|
<!--
|
|
◆ Gone on a wide surface, where the rail is how you leave and the hub it points at is not drawn at
|
|
all. An arrow to a screen nothing else can reach would be the one control on this header that leads
|
|
somewhere the layout has removed. See PhoneShell.IsWide.
|
|
-->
|
|
<Button Grid.Column="0" Classes="icon" Content="←"
|
|
IsVisible="{Binding !$parent[views:PhoneShell].IsWide}"
|
|
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).ShowScreenCommand}"
|
|
CommandParameter="{x:Static vm:ShellScreen.More}" />
|
|
<TextBlock Grid.Column="1" Classes="heading" Text="Vaults" Margin="4,0" />
|
|
<Button Grid.Column="2" Classes="icon" Content="↻" Command="{Binding RefreshCommand}"
|
|
IsEnabled="{Binding !IsBusy}" />
|
|
<Button Grid.Column="3" Classes="icon accent" Content="+" Command="{Binding NewVaultCommand}"
|
|
IsEnabled="{Binding !IsBusy}" />
|
|
</Grid>
|
|
|
|
<!-- ============ a new vault ============ -->
|
|
<!--
|
|
Above the list rather than in place of it, which is the opposite of what the host and snippet
|
|
editors do — and the difference is what the form is about. Those two edit a row that is on screen,
|
|
so a card stacked over the list hides the thing being changed. This one is about a vault that does
|
|
not exist yet, and the vaults that do are exactly the useful thing to be able to see while naming it.
|
|
-->
|
|
<Border Grid.Row="1" Classes="card" Margin="12,0,12,8" IsVisible="{Binding IsCreatingVault}">
|
|
<StackPanel Spacing="10">
|
|
<TextBlock Classes="label" Text="NEW VAULT" />
|
|
|
|
<TextBox Classes="field" Text="{Binding NewVaultName}" PlaceholderText="name" />
|
|
|
|
<TextBlock Classes="body"
|
|
Text="Its key is made on this phone and nobody else has it. Add people to it once it exists, then share the key with them." />
|
|
|
|
<Grid ColumnDefinitions="*,8,*">
|
|
<Button Grid.Column="0" Classes="primary" Height="44" Content="CREATE"
|
|
Command="{Binding CreateVaultCommand}" IsEnabled="{Binding !IsBusy}" />
|
|
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
|
Command="{Binding CancelNewVaultCommand}" />
|
|
</Grid>
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
Status, and it is the empty state as well: the view model writes "locked" and every command's
|
|
outcome into the same property. A literal here would be a second voice saying the same thing
|
|
slightly differently.
|
|
-->
|
|
<TextBlock Grid.Row="2" Classes="detail" Margin="18,2,18,6" TextWrapping="Wrap"
|
|
Text="{Binding Status}"
|
|
IsVisible="{Binding Status, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
|
|
|
<!-- ============ the column ============ -->
|
|
<ScrollViewer Grid.Row="3">
|
|
<StackPanel Margin="0,0,0,18">
|
|
|
|
<TextBlock Classes="section" Text="VAULTS" Margin="18,4,18,4" />
|
|
|
|
<!--
|
|
Rows as cards, filled when chosen, which is what HOSTS settled on in v2 and what the radius
|
|
ladder calls a card: one item, one rule, one thing you act on. The fill is on the item rather
|
|
than on a Border inside it so the rounding the theme draws for selection is the row's own.
|
|
-->
|
|
<ListBox ItemsSource="{Binding Vaults}" SelectedItem="{Binding SelectedVault}"
|
|
IsVisible="{Binding HasVaults}" Background="Transparent" BorderThickness="0">
|
|
<ListBox.Styles>
|
|
<Style Selector="ListBoxItem">
|
|
<Setter Property="Padding" Value="0" />
|
|
<Setter Property="MinHeight" Value="0" />
|
|
<Setter Property="Margin" Value="10,1" />
|
|
<Setter Property="CornerRadius" Value="12" />
|
|
</Style>
|
|
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
|
<Setter Property="Background" Value="{StaticResource Active}" />
|
|
<Setter Property="CornerRadius" Value="12" />
|
|
</Style>
|
|
</ListBox.Styles>
|
|
<ListBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:VaultRowViewModel">
|
|
<Grid ColumnDefinitions="*,Auto" MinHeight="54" Margin="14,11">
|
|
<StackPanel Grid.Column="0" Spacing="3" VerticalAlignment="Center">
|
|
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="{Binding Name}"
|
|
TextTrimming="CharacterEllipsis" />
|
|
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Detail}" />
|
|
|
|
<!--
|
|
Whether *this* phone can open it, and whether a rekey is owed. Coloured because both
|
|
states need somebody to act; drawn at all only when there is one, so an ordinary vault
|
|
carries no line rather than a reassurance nobody reads.
|
|
-->
|
|
<TextBlock Classes="detail" FontSize="10" TextWrapping="Wrap"
|
|
Foreground="{StaticResource WarnText}" Text="{Binding State}"
|
|
IsVisible="{Binding HasState}" />
|
|
</StackPanel>
|
|
|
|
<!-- The caller's own role here, which is what says why some of it is read-only. -->
|
|
<Border Grid.Column="1" Classes="tag outline" Margin="8,0,0,0">
|
|
<TextBlock Text="{Binding RoleLabel}" />
|
|
</Border>
|
|
</Grid>
|
|
</DataTemplate>
|
|
</ListBox.ItemTemplate>
|
|
</ListBox>
|
|
|
|
<!-- ============ the chosen vault ============ -->
|
|
<StackPanel IsVisible="{Binding HasSelection}">
|
|
|
|
<!--
|
|
The personal vault has no members and never will, so the members heading is gated on the vault
|
|
being one that can have them rather than left standing over an empty list.
|
|
-->
|
|
<TextBlock Classes="body" Margin="18,18,18,0" IsVisible="{Binding SelectedIsPersonal}"
|
|
Text="Nobody can be added to your personal vault, and the server refuses a key grant on one outright. Make a vault for the things you want to share, and put them in it." />
|
|
|
|
<StackPanel IsVisible="{Binding SelectedIsShared}">
|
|
|
|
<TextBlock Classes="section" Text="MEMBERS" Margin="18,18,18,4" />
|
|
|
|
<TextBlock Classes="body" Margin="18,0,18,4" IsVisible="{Binding HasSharedMembershipWarning}"
|
|
Text="{Binding SharedMembershipWarning}" />
|
|
|
|
<ListBox ItemsSource="{Binding Members}" SelectedItem="{Binding SelectedMember}"
|
|
Background="Transparent" BorderThickness="0">
|
|
<ListBox.Styles>
|
|
<Style Selector="ListBoxItem">
|
|
<Setter Property="Padding" Value="0" />
|
|
<Setter Property="MinHeight" Value="0" />
|
|
<Setter Property="Margin" Value="10,1" />
|
|
<Setter Property="CornerRadius" Value="12" />
|
|
</Style>
|
|
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
|
<Setter Property="Background" Value="{StaticResource Active}" />
|
|
<Setter Property="CornerRadius" Value="12" />
|
|
</Style>
|
|
</ListBox.Styles>
|
|
<ListBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:VaultMemberRowViewModel">
|
|
<Grid ColumnDefinitions="*,Auto" MinHeight="54" Margin="14,11">
|
|
<StackPanel Grid.Column="0" Spacing="3" VerticalAlignment="Center">
|
|
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="{Binding Name}"
|
|
TextTrimming="CharacterEllipsis" />
|
|
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Email}"
|
|
TextTrimming="CharacterEllipsis" />
|
|
|
|
<!--
|
|
◆ The one fact on this row that decides whether the button at the foot of the screen
|
|
can do anything: an account with no published identity key has nothing for a vault
|
|
key to be wrapped to. One sentence, from the view model, painted twice rather than
|
|
written twice — the warning colour is the whole of the difference, and a converter
|
|
for it would hide that the two are the same string.
|
|
|
|
The published case is quiet rather than green. Green on this head means a shell is
|
|
open right now, and a published key is a durable fact about an account — borrowing
|
|
the status colour for it would be the second meaning that makes the first
|
|
unreadable. Only the missing key is coloured, because only it needs answering.
|
|
-->
|
|
<TextBlock Classes="detail" FontSize="10" TextWrapping="Wrap"
|
|
Foreground="{StaticResource TextDim}" Text="{Binding KeyState}"
|
|
IsVisible="{Binding Member.IsEnrolled}" />
|
|
<TextBlock Classes="detail" FontSize="10" TextWrapping="Wrap"
|
|
Foreground="{StaticResource WarnText}" Text="{Binding KeyState}"
|
|
IsVisible="{Binding !Member.IsEnrolled}" />
|
|
|
|
<!--
|
|
A date to the day, or that they have never been here at all. The view model writes
|
|
both, and neither is a guess: the server records the account's last authenticated
|
|
request at most once an hour, which is what makes a day the honest unit.
|
|
-->
|
|
<TextBlock Classes="detail" FontSize="9.5" Foreground="{StaticResource TextFaint}"
|
|
Text="{Binding LastActive}" />
|
|
</StackPanel>
|
|
|
|
<Border Grid.Column="1" Classes="tag outline" Margin="8,0,0,0">
|
|
<TextBlock Text="{Binding Role}" />
|
|
</Border>
|
|
</Grid>
|
|
</DataTemplate>
|
|
</ListBox.ItemTemplate>
|
|
</ListBox>
|
|
|
|
<TextBlock Classes="body" Margin="18,10,18,0"
|
|
Text="Being in a vault is what lets the server hand somebody its rows. It is not what lets them read one: a vault key can only be wrapped by a machine that already holds it, which is what sharing below does." />
|
|
|
|
<!-- ============ ◆ adding somebody ============ -->
|
|
<!--
|
|
In the scrolling column rather than in the bar at the foot, and that is deliberate. The bar is
|
|
raised by choosing a *member* and is about that member; this is about the vault and is
|
|
available with nobody chosen at all, so putting it there would mean selecting somebody in
|
|
order to add somebody else.
|
|
|
|
Only for an admin or the owner. The server refuses anybody else and says so, but a box and a
|
|
button that exist to produce that refusal are worse than an absence — see
|
|
VaultsViewModel.CanAdministerSelected, which is the same gate the desktop uses.
|
|
|
|
The role is three chips rather than a ComboBox, matching the tag chips in the host editor: the
|
|
answer is one of three short words, and a picker would hide two of them behind a tap. Member
|
|
is the default because it is what adding a colleague almost always means; viewer would be
|
|
safer and would be the wrong default, since an interface whose default is wrong teaches people
|
|
to change it without reading it.
|
|
-->
|
|
<StackPanel IsVisible="{Binding CanAdministerSelected}" Spacing="8" Margin="18,18,18,0">
|
|
|
|
<TextBlock Classes="section" Text="ADD SOMEBODY" />
|
|
|
|
<TextBox Classes="field" Text="{Binding NewMemberEmail}"
|
|
PlaceholderText="colleague@example.com" />
|
|
|
|
<TextBlock Classes="label" Text="THEY ARRIVE AS" Margin="0,2,0,0" />
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="chiptoggle" Classes.worn="{Binding AddsAsViewer}"
|
|
Command="{Binding ChooseNewMemberRoleCommand}"
|
|
CommandParameter="{x:Static contracts:TeamMemberRole.Viewer}">
|
|
<TextBlock Classes="mono" FontSize="11.5" Text="VIEWER" />
|
|
</Button>
|
|
<Button Classes="chiptoggle" Classes.worn="{Binding AddsAsMember}"
|
|
Command="{Binding ChooseNewMemberRoleCommand}"
|
|
CommandParameter="{x:Static contracts:TeamMemberRole.Member}">
|
|
<TextBlock Classes="mono" FontSize="11.5" Text="MEMBER" />
|
|
</Button>
|
|
<Button Classes="chiptoggle" Classes.worn="{Binding AddsAsAdmin}"
|
|
Command="{Binding ChooseNewMemberRoleCommand}"
|
|
CommandParameter="{x:Static contracts:TeamMemberRole.Admin}">
|
|
<TextBlock Classes="mono" FontSize="11.5" Text="ADMIN" />
|
|
</Button>
|
|
</StackPanel>
|
|
|
|
<Button Classes="primary" Height="44" Content="ADD"
|
|
Command="{Binding AddMemberCommand}" IsEnabled="{Binding !IsBusy}" />
|
|
|
|
<!--
|
|
The half of the act the server cannot perform, said before it happens rather than discovered
|
|
afterwards. Adding somebody from this phone wraps every vault key this phone can open to
|
|
them as part of the same command — so on an unlocked handset it is one press, and on a
|
|
locked one it is a membership and an owed key, which the status line then says.
|
|
-->
|
|
<TextBlock Classes="body"
|
|
Text="Adding somebody lets the server serve them this vault. It does not let them read it: a vault key is wrapped by a machine that already holds one, so this phone does that too if it can open the vault. An account that has never signed in to this server cannot be added — ask them to sign in once first." />
|
|
|
|
</StackPanel>
|
|
|
|
</StackPanel>
|
|
|
|
</StackPanel>
|
|
|
|
</StackPanel>
|
|
</ScrollViewer>
|
|
|
|
<!-- ============ ◆ giving somebody the key ============ -->
|
|
<!--
|
|
Raised over the column when somebody has been chosen, as HOSTS raises its connect bar and SNIPPETS
|
|
its insert bar, and for the reason written there: there is no second column to put it in, so it names
|
|
what it will do rather than relying on a selection being visible beside the button.
|
|
|
|
One condition where there used to be two. Sharing needs a member and a vault, and the vault is now
|
|
the thing the whole screen is about — a member can only be selected under one, so choosing who is the
|
|
only half left to make.
|
|
-->
|
|
<!-- DeepChrome rather than Chrome, since v5 — the same raised-bar decision SnippetsScreen and
|
|
FilesScreen make for their own selection bars; see the remark on PhoneShell's vault header. -->
|
|
<Border Grid.Row="4"
|
|
IsVisible="{Binding SelectedMember, Converter={x:Static ObjectConverters.IsNotNull}}"
|
|
Background="{StaticResource DeepChrome}" BorderBrush="{StaticResource Border}"
|
|
BorderThickness="0,1,0,0" Padding="14,12">
|
|
<StackPanel Spacing="9">
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
|
<TextBlock Classes="label" Text="THE KEY TO" />
|
|
<TextBlock Classes="mono" FontSize="11" Text="{Binding SelectedVault.Name}"
|
|
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
|
</StackPanel>
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
|
<TextBlock Classes="label" Text="FOR" />
|
|
<TextBlock Classes="mono" FontSize="11" Text="{Binding SelectedMember.Name}"
|
|
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
|
</StackPanel>
|
|
|
|
<Button Classes="primary" Content="SHARE KEY" Command="{Binding ShareVaultCommand}"
|
|
IsEnabled="{Binding !IsBusy}" />
|
|
|
|
<!--
|
|
The sentence the desktop hangs off a tooltip, which a phone cannot show — so it is body text
|
|
under the button, where it is read before the tap rather than after it. It is not decoration:
|
|
the key-log check proves this server has been consistent with itself and nothing more.
|
|
-->
|
|
<TextBlock Classes="body"
|
|
Text="Their published key is checked against the server's append-only key log first, and nothing is wrapped if it does not appear there unchanged. That proves the server has been consistent with itself — not that the key is the right person's. Compare the fingerprint with them over a channel this server does not carry before sharing anything that matters." />
|
|
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
</Grid>
|
|
|
|
</UserControl>
|