mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-13 04:08:47 -06:00
Swap the command palette modes for the prefix > (#7935)
VsCode uses `>` as its "prefix" for the equivalent of their "action mode". This PR aligns the Terminal with their logic here. We have to be tricky - if we use the `>` in the actual input as the indicator for action mode, we can't display any placeholder text in the input to tell users to type a command. This wasn't an issue for the commandline mode previously, because we'd stick the "prompt" in the "no matches text" space. However, we can't do that for action mode. Instead, we'll stick a floating text block over the input box, and when the user's in action mode, we'll manually place a `>` into that space. When the user backspaces the `>`, we'll remove it from that block, and switch into commandline mode. ## Validation Steps Performed Played with the cmdpal in lots of different modes, this finally feels good Closes #7736
This commit is contained in:
parent
30e363e7ac
commit
bd7cd5512d
@ -213,41 +213,27 @@ namespace winrt::TerminalApp::implementation
|
|||||||
}
|
}
|
||||||
else if (key == VirtualKey::Escape)
|
else if (key == VirtualKey::Escape)
|
||||||
{
|
{
|
||||||
// Action, TabSearch, TabSwitch Mode: Dismiss the palette if the
|
// Dismiss the palette if the text is empty, otherwise clear the
|
||||||
// text is empty, otherwise clear the search string.
|
// search string.
|
||||||
if (_currentMode != CommandPaletteMode::CommandlineMode)
|
if (_searchBox().Text().empty())
|
||||||
{
|
{
|
||||||
if (_searchBox().Text().empty())
|
_dismissPalette();
|
||||||
{
|
|
||||||
_dismissPalette();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_searchBox().Text(L"");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (_currentMode == CommandPaletteMode::CommandlineMode)
|
else
|
||||||
{
|
{
|
||||||
const auto currentInput = _getPostPrefixInput();
|
_searchBox().Text(L"");
|
||||||
if (currentInput.empty())
|
}
|
||||||
{
|
|
||||||
// The user's only input "> " so far. We should just dismiss
|
|
||||||
// the palette. This is like dismissing the Action mode with
|
|
||||||
// empty input.
|
|
||||||
_dismissPalette();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Clear out the current input. We'll leave a ">" in the
|
|
||||||
// input (to stay in commandline mode), and a leading space
|
|
||||||
// (if they currently had one).
|
|
||||||
const bool hasLeadingSpace = (_searchBox().Text().size()) - (currentInput.size()) > 1;
|
|
||||||
_searchBox().Text(hasLeadingSpace ? L"> " : L">");
|
|
||||||
|
|
||||||
// This will conveniently move the cursor to the end of the
|
e.Handled(true);
|
||||||
// text input for us.
|
}
|
||||||
_searchBox().Select(_searchBox().Text().size(), 0);
|
else if (key == VirtualKey::Back)
|
||||||
}
|
{
|
||||||
|
// If the last filter text was empty, and we're backspacing from
|
||||||
|
// that state, then the user "backspaced" the virtual '>' we're
|
||||||
|
// using as the action mode indicator. Switch into commandline mode.
|
||||||
|
if (_searchBox().Text().empty() && _lastFilterTextWasEmpty && _currentMode == CommandPaletteMode::ActionMode)
|
||||||
|
{
|
||||||
|
_switchToMode(CommandPaletteMode::CommandlineMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Handled(true);
|
e.Handled(true);
|
||||||
@ -480,18 +466,13 @@ namespace winrt::TerminalApp::implementation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method Description:
|
// Method Description:
|
||||||
// - Get all the input text in _searchBox that follows the prefix character
|
// - Get all the input text in _searchBox that follows any leading spaces.
|
||||||
// and any whitespace following that prefix character. This can be used in
|
|
||||||
// commandline mode to get all the useful input that the user input after
|
|
||||||
// the leading ">" prefix.
|
|
||||||
// - Note that this will behave unexpectedly in Action Mode.
|
|
||||||
// Arguments:
|
// Arguments:
|
||||||
// - <none>
|
// - <none>
|
||||||
// Return Value:
|
// Return Value:
|
||||||
// - the string of input following the prefix character.
|
// - the string of input following any number of leading spaces
|
||||||
std::wstring CommandPalette::_getPostPrefixInput()
|
std::wstring CommandPalette::_getTrimmedInput()
|
||||||
{
|
{
|
||||||
const std::wstring input{ _searchBox().Text() };
|
const std::wstring input{ _searchBox().Text() };
|
||||||
if (input.empty())
|
if (input.empty())
|
||||||
@ -499,17 +480,15 @@ namespace winrt::TerminalApp::implementation
|
|||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto rawCmdline{ input.substr(1) };
|
|
||||||
|
|
||||||
// Trim leading whitespace
|
// Trim leading whitespace
|
||||||
const auto firstNonSpace = rawCmdline.find_first_not_of(L" ");
|
const auto firstNonSpace = input.find_first_not_of(L" ");
|
||||||
if (firstNonSpace == std::wstring::npos)
|
if (firstNonSpace == std::wstring::npos)
|
||||||
{
|
{
|
||||||
// All the following characters are whitespace.
|
// All the following characters are whitespace.
|
||||||
return L"";
|
return L"";
|
||||||
}
|
}
|
||||||
|
|
||||||
return rawCmdline.substr(firstNonSpace);
|
return input.substr(firstNonSpace);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method Description:
|
// Method Description:
|
||||||
@ -520,12 +499,11 @@ namespace winrt::TerminalApp::implementation
|
|||||||
// - <none>
|
// - <none>
|
||||||
void CommandPalette::_dispatchCommandline()
|
void CommandPalette::_dispatchCommandline()
|
||||||
{
|
{
|
||||||
const auto input = _getPostPrefixInput();
|
auto cmdline{ _getTrimmedInput() };
|
||||||
if (input.empty())
|
if (cmdline.empty())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
winrt::hstring cmdline{ input };
|
|
||||||
|
|
||||||
// Build the ExecuteCommandline action from the values we've parsed on the commandline.
|
// Build the ExecuteCommandline action from the values we've parsed on the commandline.
|
||||||
ExecuteCommandlineArgs args{ cmdline };
|
ExecuteCommandlineArgs args{ cmdline };
|
||||||
@ -574,32 +552,49 @@ namespace winrt::TerminalApp::implementation
|
|||||||
void CommandPalette::_filterTextChanged(IInspectable const& /*sender*/,
|
void CommandPalette::_filterTextChanged(IInspectable const& /*sender*/,
|
||||||
Windows::UI::Xaml::RoutedEventArgs const& /*args*/)
|
Windows::UI::Xaml::RoutedEventArgs const& /*args*/)
|
||||||
{
|
{
|
||||||
if (_currentMode == CommandPaletteMode::CommandlineMode || _currentMode == CommandPaletteMode::ActionMode)
|
if (_currentMode == CommandPaletteMode::CommandlineMode)
|
||||||
{
|
{
|
||||||
_evaluatePrefix();
|
_evaluatePrefix();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We're setting _lastFilterTextWasEmpty here, because if the user tries
|
||||||
|
// to backspace the last character in the input, the Backspace KeyDown
|
||||||
|
// event will fire _before_ _filterTextChanged does. Updating the value
|
||||||
|
// here will ensure that we can check this case appropriately.
|
||||||
|
_lastFilterTextWasEmpty = _searchBox().Text().empty();
|
||||||
|
|
||||||
_updateFilteredActions();
|
_updateFilteredActions();
|
||||||
_filteredActionsView().SelectedIndex(0);
|
_filteredActionsView().SelectedIndex(0);
|
||||||
|
|
||||||
_noMatchesText().Visibility(_filteredActions.Size() > 0 ? Visibility::Collapsed : Visibility::Visible);
|
if (_currentMode == CommandPaletteMode::TabSearchMode || _currentMode == CommandPaletteMode::ActionMode)
|
||||||
|
{
|
||||||
|
_noMatchesText().Visibility(_filteredActions.Size() > 0 ? Visibility::Collapsed : Visibility::Visible);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_noMatchesText().Visibility(Visibility::Collapsed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommandPalette::_evaluatePrefix()
|
void CommandPalette::_evaluatePrefix()
|
||||||
{
|
{
|
||||||
auto newMode = CommandPaletteMode::ActionMode;
|
// This will take you from commandline mode, into action mode. The
|
||||||
|
// backspace handler in _keyDownHandler will handle taking us from
|
||||||
|
// action mode to commandline mode.
|
||||||
|
auto newMode = CommandPaletteMode::CommandlineMode;
|
||||||
|
|
||||||
auto inputText = _searchBox().Text();
|
auto inputText = _getTrimmedInput();
|
||||||
if (inputText.size() > 0)
|
if (inputText.size() > 0)
|
||||||
{
|
{
|
||||||
if (inputText[0] == L'>')
|
if (inputText[0] == L'>')
|
||||||
{
|
{
|
||||||
newMode = CommandPaletteMode::CommandlineMode;
|
newMode = CommandPaletteMode::ActionMode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newMode != _currentMode)
|
if (newMode != _currentMode)
|
||||||
{
|
{
|
||||||
|
//_switchToMode will remove the '>' character from the input.
|
||||||
_switchToMode(newMode);
|
_switchToMode(newMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -644,6 +639,8 @@ namespace winrt::TerminalApp::implementation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_searchBox().Text(L"");
|
||||||
|
_searchBox().Select(_searchBox().Text().size(), 0);
|
||||||
// Leaving this block of code outside the above if-statement
|
// Leaving this block of code outside the above if-statement
|
||||||
// guarantees that the correct text is shown for the mode
|
// guarantees that the correct text is shown for the mode
|
||||||
// whenever _switchToMode is called.
|
// whenever _switchToMode is called.
|
||||||
@ -652,20 +649,24 @@ namespace winrt::TerminalApp::implementation
|
|||||||
case CommandPaletteMode::TabSearchMode:
|
case CommandPaletteMode::TabSearchMode:
|
||||||
case CommandPaletteMode::TabSwitchMode:
|
case CommandPaletteMode::TabSwitchMode:
|
||||||
{
|
{
|
||||||
SearchBoxText(RS_(L"TabSwitcher_SearchBoxText"));
|
SearchBoxPlaceholderText(RS_(L"TabSwitcher_SearchBoxText"));
|
||||||
NoMatchesText(RS_(L"TabSwitcher_NoMatchesText"));
|
NoMatchesText(RS_(L"TabSwitcher_NoMatchesText"));
|
||||||
ControlName(RS_(L"TabSwitcherControlName"));
|
ControlName(RS_(L"TabSwitcherControlName"));
|
||||||
|
PrefixCharacter(L"");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CommandPaletteMode::CommandlineMode:
|
case CommandPaletteMode::CommandlineMode:
|
||||||
NoMatchesText(RS_(L"CmdPalCommandlinePrompt"));
|
SearchBoxPlaceholderText(RS_(L"CmdPalCommandlinePrompt"));
|
||||||
|
NoMatchesText(L"");
|
||||||
ControlName(RS_(L"CommandPaletteControlName"));
|
ControlName(RS_(L"CommandPaletteControlName"));
|
||||||
|
PrefixCharacter(L"");
|
||||||
break;
|
break;
|
||||||
case CommandPaletteMode::ActionMode:
|
case CommandPaletteMode::ActionMode:
|
||||||
default:
|
default:
|
||||||
SearchBoxText(RS_(L"CommandPalette_SearchBox/PlaceholderText"));
|
SearchBoxPlaceholderText(RS_(L"CommandPalette_SearchBox/PlaceholderText"));
|
||||||
NoMatchesText(RS_(L"CommandPalette_NoMatchesText/Text"));
|
NoMatchesText(RS_(L"CommandPalette_NoMatchesText/Text"));
|
||||||
ControlName(RS_(L"CommandPaletteControlName"));
|
ControlName(RS_(L"CommandPaletteControlName"));
|
||||||
|
PrefixCharacter(L">");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -717,7 +718,7 @@ namespace winrt::TerminalApp::implementation
|
|||||||
{
|
{
|
||||||
std::vector<Command> actions;
|
std::vector<Command> actions;
|
||||||
|
|
||||||
auto searchText = _searchBox().Text();
|
winrt::hstring searchText{ _getTrimmedInput() };
|
||||||
const bool addAll = searchText.empty();
|
const bool addAll = searchText.empty();
|
||||||
|
|
||||||
auto commandsToFilter = _commandsToFilter();
|
auto commandsToFilter = _commandsToFilter();
|
||||||
|
|||||||
@ -39,7 +39,8 @@ namespace winrt::TerminalApp::implementation
|
|||||||
|
|
||||||
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
|
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
|
||||||
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, NoMatchesText, _PropertyChangedHandlers);
|
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, NoMatchesText, _PropertyChangedHandlers);
|
||||||
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, SearchBoxText, _PropertyChangedHandlers);
|
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, SearchBoxPlaceholderText, _PropertyChangedHandlers);
|
||||||
|
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, PrefixCharacter, _PropertyChangedHandlers);
|
||||||
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, ControlName, _PropertyChangedHandlers);
|
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, ControlName, _PropertyChangedHandlers);
|
||||||
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, ParentCommandName, _PropertyChangedHandlers);
|
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, ParentCommandName, _PropertyChangedHandlers);
|
||||||
|
|
||||||
@ -55,6 +56,8 @@ namespace winrt::TerminalApp::implementation
|
|||||||
|
|
||||||
Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::Command> _commandsToFilter();
|
Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::Command> _commandsToFilter();
|
||||||
|
|
||||||
|
bool _lastFilterTextWasEmpty{ true };
|
||||||
|
|
||||||
void _filterTextChanged(Windows::Foundation::IInspectable const& sender,
|
void _filterTextChanged(Windows::Foundation::IInspectable const& sender,
|
||||||
Windows::UI::Xaml::RoutedEventArgs const& args);
|
Windows::UI::Xaml::RoutedEventArgs const& args);
|
||||||
void _previewKeyDownHandler(Windows::Foundation::IInspectable const& sender,
|
void _previewKeyDownHandler(Windows::Foundation::IInspectable const& sender,
|
||||||
@ -84,8 +87,8 @@ namespace winrt::TerminalApp::implementation
|
|||||||
CommandPaletteMode _currentMode;
|
CommandPaletteMode _currentMode;
|
||||||
void _switchToMode(CommandPaletteMode mode);
|
void _switchToMode(CommandPaletteMode mode);
|
||||||
|
|
||||||
|
std::wstring _getTrimmedInput();
|
||||||
void _evaluatePrefix();
|
void _evaluatePrefix();
|
||||||
std::wstring _getPostPrefixInput();
|
|
||||||
|
|
||||||
Microsoft::Terminal::TerminalControl::IKeyBindings _bindings;
|
Microsoft::Terminal::TerminalControl::IKeyBindings _bindings;
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,8 @@ namespace TerminalApp
|
|||||||
CommandPalette();
|
CommandPalette();
|
||||||
|
|
||||||
String NoMatchesText { get; };
|
String NoMatchesText { get; };
|
||||||
String SearchBoxText { get; };
|
String SearchBoxPlaceholderText { get; };
|
||||||
|
String PrefixCharacter { get; };
|
||||||
String ControlName { get; };
|
String ControlName { get; };
|
||||||
String ParentCommandName { get; };
|
String ParentCommandName { get; };
|
||||||
|
|
||||||
|
|||||||
@ -8,8 +8,8 @@ the MIT License. See LICENSE in the project root for license information. -->
|
|||||||
xmlns:mux="using:Microsoft.UI.Xaml.Controls"
|
xmlns:mux="using:Microsoft.UI.Xaml.Controls"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:Windows10version1903="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract, 8)"
|
xmlns:Windows10version1903="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract, 8)"
|
||||||
xmlns:SettingsModel="using:Microsoft.Terminal.Settings.Model"
|
xmlns:SettingsModel="using:Microsoft.Terminal.Settings.Model"
|
||||||
TabNavigation="Cycle"
|
TabNavigation="Cycle"
|
||||||
IsTabStop="True"
|
IsTabStop="True"
|
||||||
AllowFocusOnInteraction="True"
|
AllowFocusOnInteraction="True"
|
||||||
@ -135,27 +135,27 @@ the MIT License. See LICENSE in the project root for license information. -->
|
|||||||
to receive clicks _anywhere_ in its bounds. -->
|
to receive clicks _anywhere_ in its bounds. -->
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
x:Name="_shadowBackdrop"
|
x:Name="_shadowBackdrop"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.ColumnSpan="3"
|
Grid.ColumnSpan="3"
|
||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch">
|
VerticalAlignment="Stretch">
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
x:Name="_backdrop"
|
x:Name="_backdrop"
|
||||||
Style="{ThemeResource CommandPaletteBackground}"
|
Style="{ThemeResource CommandPaletteBackground}"
|
||||||
CornerRadius="{ThemeResource ControlCornerRadius}"
|
CornerRadius="{ThemeResource ControlCornerRadius}"
|
||||||
PointerPressed="_backdropPointerPressed"
|
PointerPressed="_backdropPointerPressed"
|
||||||
Margin="8"
|
Margin="8"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Windows10version1903:Shadow="{StaticResource CommandPaletteShadow}"
|
Windows10version1903:Shadow="{StaticResource CommandPaletteShadow}"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Top">
|
VerticalAlignment="Top">
|
||||||
|
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
@ -164,15 +164,28 @@ the MIT License. See LICENSE in the project root for license information. -->
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBox
|
<TextBox
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
x:Name="_searchBox"
|
x:Name="_searchBox"
|
||||||
Margin="8"
|
Margin="8"
|
||||||
IsSpellCheckEnabled="False"
|
Padding="18,8,8,8"
|
||||||
TextChanged="_filterTextChanged"
|
IsSpellCheckEnabled="False"
|
||||||
PlaceholderText="{x:Bind SearchBoxText, Mode=OneWay}"
|
TextChanged="_filterTextChanged"
|
||||||
Text="">
|
PlaceholderText="{x:Bind SearchBoxPlaceholderText, Mode=OneWay}"
|
||||||
|
Text="">
|
||||||
</TextBox>
|
</TextBox>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="0"
|
||||||
|
x:Name="_prefixCharacter"
|
||||||
|
Margin="16,16,0,-8"
|
||||||
|
FontSize="14"
|
||||||
|
Visibility="{x:Bind PrefixCharacter,
|
||||||
|
Mode=OneWay,
|
||||||
|
Converter={StaticResource ParentCommandVisibilityConverter}}"
|
||||||
|
Text="{x:Bind PrefixCharacter, Mode=OneWay}"
|
||||||
|
>
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Padding="16, 0, 16, 4"
|
Padding="16, 0, 16, 4"
|
||||||
x:Name="_parentCommandText"
|
x:Name="_parentCommandText"
|
||||||
@ -194,17 +207,17 @@ the MIT License. See LICENSE in the project root for license information. -->
|
|||||||
</TextBlock>
|
</TextBlock>
|
||||||
|
|
||||||
<ListView
|
<ListView
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
x:Name="_filteredActionsView"
|
x:Name="_filteredActionsView"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
SelectionMode="Single"
|
SelectionMode="Single"
|
||||||
CanReorderItems="False"
|
CanReorderItems="False"
|
||||||
AllowDrop="False"
|
AllowDrop="False"
|
||||||
IsItemClickEnabled="True"
|
IsItemClickEnabled="True"
|
||||||
ItemClick="_listItemClicked"
|
ItemClick="_listItemClicked"
|
||||||
PreviewKeyDown="_keyDownHandler"
|
PreviewKeyDown="_keyDownHandler"
|
||||||
ItemsSource="{x:Bind FilteredActions}">
|
ItemsSource="{x:Bind FilteredActions}">
|
||||||
|
|
||||||
<ItemsControl.ItemTemplate >
|
<ItemsControl.ItemTemplate >
|
||||||
<DataTemplate x:DataType="SettingsModel:Command">
|
<DataTemplate x:DataType="SettingsModel:Command">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user