mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2025-11-16 15:02:53 -05:00
feat: Add cancel function for progress indicator
This commit is contained in:
@ -65,8 +65,7 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
|||||||
setupConnections();
|
setupConnections();
|
||||||
|
|
||||||
m_typingTimer.start();
|
m_typingTimer.start();
|
||||||
|
|
||||||
// Create hover handler for refactoring suggestions
|
|
||||||
m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
|
m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,12 +82,12 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
Client::openDocument(document);
|
Client::openDocument(document);
|
||||||
|
|
||||||
// Register hover handler for this document
|
|
||||||
auto editors = TextEditor::BaseTextEditor::textEditorsForDocument(document);
|
auto editors = TextEditor::BaseTextEditor::textEditorsForDocument(document);
|
||||||
for (auto *editor : editors) {
|
for (auto *editor : editors) {
|
||||||
if (auto *widget = editor->editorWidget()) {
|
if (auto *widget = editor->editorWidget()) {
|
||||||
widget->addHoverHandler(m_refactorHoverHandler);
|
widget->addHoverHandler(m_refactorHoverHandler);
|
||||||
|
widget->installEventFilter(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
connect(
|
connect(
|
||||||
@ -187,11 +186,16 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
|
|||||||
documentVersion(filePath),
|
documentVersion(filePath),
|
||||||
Position(cursor.mainCursor())}};
|
Position(cursor.mainCursor())}};
|
||||||
if (Settings::codeCompletionSettings().showProgressWidget()) {
|
if (Settings::codeCompletionSettings().showProgressWidget()) {
|
||||||
|
// Setup cancel callback before showing progress
|
||||||
|
m_progressHandler.setCancelCallback([this, editor = QPointer<TextEditorWidget>(editor)]() {
|
||||||
|
if (editor) {
|
||||||
|
cancelRunningRequest(editor);
|
||||||
|
}
|
||||||
|
});
|
||||||
m_progressHandler.showProgress(editor);
|
m_progressHandler.showProgress(editor);
|
||||||
}
|
}
|
||||||
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
|
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
|
||||||
const GetCompletionRequest::Response &response) {
|
const GetCompletionRequest::Response &response) {
|
||||||
qDebug() << "setResponseCallback";
|
|
||||||
QTC_ASSERT(editor, return);
|
QTC_ASSERT(editor, return);
|
||||||
handleCompletions(response, editor);
|
handleCompletions(response, editor);
|
||||||
});
|
});
|
||||||
@ -224,6 +228,13 @@ void QodeAssistClient::requestQuickRefactor(
|
|||||||
&QodeAssistClient::handleRefactoringResult);
|
&QodeAssistClient::handleRefactoringResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup cancel callback before showing progress
|
||||||
|
m_progressHandler.setCancelCallback([this, editor = QPointer<TextEditorWidget>(editor)]() {
|
||||||
|
if (editor && m_refactorHandler) {
|
||||||
|
m_refactorHandler->cancelRequest();
|
||||||
|
m_progressHandler.hideProgress();
|
||||||
|
}
|
||||||
|
});
|
||||||
m_progressHandler.showProgress(editor);
|
m_progressHandler.showProgress(editor);
|
||||||
m_refactorHandler->sendRefactorRequest(editor, instructions);
|
m_refactorHandler->sendRefactorRequest(editor, instructions);
|
||||||
}
|
}
|
||||||
@ -260,14 +271,12 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
|||||||
void QodeAssistClient::handleCompletions(
|
void QodeAssistClient::handleCompletions(
|
||||||
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
|
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
|
||||||
{
|
{
|
||||||
qDebug() << "hideProgress";
|
|
||||||
m_progressHandler.hideProgress();
|
m_progressHandler.hideProgress();
|
||||||
|
|
||||||
if (response.error()) {
|
if (response.error()) {
|
||||||
log(*response.error());
|
log(*response.error());
|
||||||
|
|
||||||
QString errorMessage = tr("Code completion failed: %1")
|
QString errorMessage = tr("Code completion failed: %1").arg(response.error()->message());
|
||||||
.arg(response.error()->message());
|
|
||||||
m_errorHandler.showError(editor, errorMessage);
|
m_errorHandler.showError(editor, errorMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -314,7 +323,7 @@ void QodeAssistClient::handleCompletions(
|
|||||||
Text::Position pos{toTextPos(c.position())};
|
Text::Position pos{toTextPos(c.position())};
|
||||||
return TextSuggestion::Data{range, pos, c.text()};
|
return TextSuggestion::Data{range, pos, c.text()};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (completions.isEmpty()) {
|
if (completions.isEmpty()) {
|
||||||
LOG_MESSAGE("No valid completions received");
|
LOG_MESSAGE("No valid completions received");
|
||||||
return;
|
return;
|
||||||
@ -379,11 +388,11 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
|||||||
QString errorMessage = result.errorMessage.isEmpty()
|
QString errorMessage = result.errorMessage.isEmpty()
|
||||||
? tr("Quick refactor failed")
|
? tr("Quick refactor failed")
|
||||||
: tr("Quick refactor failed: %1").arg(result.errorMessage);
|
: tr("Quick refactor failed: %1").arg(result.errorMessage);
|
||||||
|
|
||||||
if (result.editor) {
|
if (result.editor) {
|
||||||
m_errorHandler.showError(result.editor, errorMessage);
|
m_errorHandler.showError(result.editor, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Refactoring failed: %1").arg(result.errorMessage));
|
LOG_MESSAGE(QString("Refactoring failed: %1").arg(result.errorMessage));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -404,14 +413,14 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
|||||||
|
|
||||||
int startPos = range.begin.toPositionInDocument(editorWidget->document());
|
int startPos = range.begin.toPositionInDocument(editorWidget->document());
|
||||||
int endPos = range.end.toPositionInDocument(editorWidget->document());
|
int endPos = range.end.toPositionInDocument(editorWidget->document());
|
||||||
|
|
||||||
if (startPos != endPos) {
|
if (startPos != endPos) {
|
||||||
QTextCursor startCursor(editorWidget->document());
|
QTextCursor startCursor(editorWidget->document());
|
||||||
startCursor.setPosition(startPos);
|
startCursor.setPosition(startPos);
|
||||||
if (startCursor.positionInBlock() > 0) {
|
if (startCursor.positionInBlock() > 0) {
|
||||||
startCursor.movePosition(QTextCursor::StartOfBlock);
|
startCursor.movePosition(QTextCursor::StartOfBlock);
|
||||||
}
|
}
|
||||||
|
|
||||||
QTextCursor endCursor(editorWidget->document());
|
QTextCursor endCursor(editorWidget->document());
|
||||||
endCursor.setPosition(endPos);
|
endCursor.setPosition(endPos);
|
||||||
if (endCursor.positionInBlock() > 0) {
|
if (endCursor.positionInBlock() > 0) {
|
||||||
@ -420,20 +429,19 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
|||||||
endCursor.movePosition(QTextCursor::NextCharacter);
|
endCursor.movePosition(QTextCursor::NextCharacter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils::Text::Position expandedBegin = Utils::Text::Position::fromPositionInDocument(
|
Utils::Text::Position expandedBegin = Utils::Text::Position::fromPositionInDocument(
|
||||||
editorWidget->document(), startCursor.position());
|
editorWidget->document(), startCursor.position());
|
||||||
Utils::Text::Position expandedEnd = Utils::Text::Position::fromPositionInDocument(
|
Utils::Text::Position expandedEnd = Utils::Text::Position::fromPositionInDocument(
|
||||||
editorWidget->document(), endCursor.position());
|
editorWidget->document(), endCursor.position());
|
||||||
|
|
||||||
range = Utils::Text::Range(expandedBegin, expandedEnd);
|
range = Utils::Text::Range(expandedBegin, expandedEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEditor::TextSuggestion::Data suggestionData{
|
TextEditor::TextSuggestion::Data suggestionData{
|
||||||
Utils::Text::Range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)},
|
Utils::Text::Range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)},
|
||||||
pos,
|
pos,
|
||||||
result.newText
|
result.newText};
|
||||||
};
|
|
||||||
editorWidget->insertSuggestion(
|
editorWidget->insertSuggestion(
|
||||||
std::make_unique<RefactorSuggestion>(suggestionData, editorWidget->document()));
|
std::make_unique<RefactorSuggestion>(suggestionData, editorWidget->document()));
|
||||||
|
|
||||||
@ -453,4 +461,36 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
|||||||
LOG_MESSAGE("Displaying refactoring suggestion with hover handler");
|
LOG_MESSAGE("Displaying refactoring suggestion with hover handler");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
|
||||||
|
{
|
||||||
|
if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
|
||||||
|
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
||||||
|
|
||||||
|
if (keyEvent->key() == Qt::Key_Escape) {
|
||||||
|
auto *editor = qobject_cast<TextEditor::TextEditorWidget *>(watched);
|
||||||
|
|
||||||
|
if (editor) {
|
||||||
|
if (m_runningRequests.contains(editor)) {
|
||||||
|
cancelRunningRequest(editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_scheduledRequests.contains(editor)) {
|
||||||
|
auto *timer = m_scheduledRequests.value(editor);
|
||||||
|
if (timer && timer->isActive()) {
|
||||||
|
timer->stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_refactorHandler && m_refactorHandler->isProcessing()) {
|
||||||
|
m_refactorHandler->cancelRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_progressHandler.hideProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return LanguageClient::Client::eventFilter(watched, event);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@ -53,6 +53,9 @@ public:
|
|||||||
void requestQuickRefactor(
|
void requestQuickRefactor(
|
||||||
TextEditor::TextEditorWidget *editor, const QString &instructions = QString());
|
TextEditor::TextEditorWidget *editor, const QString &instructions = QString());
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void scheduleRequest(TextEditor::TextEditorWidget *editor);
|
void scheduleRequest(TextEditor::TextEditorWidget *editor);
|
||||||
void handleCompletions(
|
void handleCompletions(
|
||||||
|
|||||||
@ -52,6 +52,7 @@ public:
|
|||||||
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
|
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
|
||||||
|
|
||||||
void cancelRequest();
|
void cancelRequest();
|
||||||
|
bool isProcessing() const { return m_isRefactoringInProgress; }
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void refactoringCompleted(const QodeAssist::RefactorResult &result);
|
void refactoringCompleted(const QodeAssist::RefactorResult &result);
|
||||||
|
|||||||
@ -60,6 +60,11 @@ void CompletionProgressHandler::hideProgress()
|
|||||||
Utils::ToolTip::hideImmediately();
|
Utils::ToolTip::hideImmediately();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CompletionProgressHandler::setCancelCallback(std::function<void()> callback)
|
||||||
|
{
|
||||||
|
m_cancelCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
void CompletionProgressHandler::identifyMatch(
|
void CompletionProgressHandler::identifyMatch(
|
||||||
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report)
|
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report)
|
||||||
{
|
{
|
||||||
@ -83,6 +88,11 @@ void CompletionProgressHandler::operateTooltip(
|
|||||||
|
|
||||||
m_progressWidget = new ProgressWidget(editorWidget);
|
m_progressWidget = new ProgressWidget(editorWidget);
|
||||||
|
|
||||||
|
// Set cancel callback for the widget
|
||||||
|
if (m_cancelCallback) {
|
||||||
|
m_progressWidget->setCancelCallback(m_cancelCallback);
|
||||||
|
}
|
||||||
|
|
||||||
const QRect cursorRect = editorWidget->cursorRect(editorWidget->textCursor());
|
const QRect cursorRect = editorWidget->cursorRect(editorWidget->textCursor());
|
||||||
QPoint globalPos = editorWidget->viewport()->mapToGlobal(cursorRect.topLeft());
|
QPoint globalPos = editorWidget->viewport()->mapToGlobal(cursorRect.topLeft());
|
||||||
QPoint localPos = editorWidget->mapFromGlobal(globalPos);
|
QPoint localPos = editorWidget->mapFromGlobal(globalPos);
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
#include <texteditor/basehoverhandler.h>
|
#include <texteditor/basehoverhandler.h>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
@ -31,6 +32,8 @@ class CompletionProgressHandler : public TextEditor::BaseHoverHandler
|
|||||||
public:
|
public:
|
||||||
void showProgress(TextEditor::TextEditorWidget *widget);
|
void showProgress(TextEditor::TextEditorWidget *widget);
|
||||||
void hideProgress();
|
void hideProgress();
|
||||||
|
void setCancelCallback(std::function<void()> callback);
|
||||||
|
bool isProgressVisible() const { return !m_progressWidget.isNull(); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void identifyMatch(
|
void identifyMatch(
|
||||||
@ -41,6 +44,7 @@ private:
|
|||||||
QPointer<TextEditor::TextEditorWidget> m_widget;
|
QPointer<TextEditor::TextEditorWidget> m_widget;
|
||||||
QPointer<ProgressWidget> m_progressWidget;
|
QPointer<ProgressWidget> m_progressWidget;
|
||||||
QPoint m_iconPosition;
|
QPoint m_iconPosition;
|
||||||
|
std::function<void()> m_cancelCallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
* Copyright (C) 2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
@ -19,10 +19,15 @@
|
|||||||
|
|
||||||
#include "ProgressWidget.hpp"
|
#include "ProgressWidget.hpp"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QStyle>
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
ProgressWidget::ProgressWidget(QWidget *parent)
|
ProgressWidget::ProgressWidget(QWidget *parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
|
, m_isHovered(false)
|
||||||
{
|
{
|
||||||
m_dotPosition = 0;
|
m_dotPosition = 0;
|
||||||
m_timer.setInterval(300);
|
m_timer.setInterval(300);
|
||||||
@ -64,6 +69,10 @@ ProgressWidget::ProgressWidget(QWidget *parent)
|
|||||||
}
|
}
|
||||||
|
|
||||||
setFixedSize(40, 40);
|
setFixedSize(40, 40);
|
||||||
|
setMouseTracking(true);
|
||||||
|
|
||||||
|
QIcon closeIcon = QApplication::style()->standardIcon(QStyle::SP_DockWidgetCloseButton);
|
||||||
|
m_closePixmap = closeIcon.pixmap(16, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
ProgressWidget::~ProgressWidget()
|
ProgressWidget::~ProgressWidget()
|
||||||
@ -71,6 +80,11 @@ ProgressWidget::~ProgressWidget()
|
|||||||
m_timer.stop();
|
m_timer.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ProgressWidget::setCancelCallback(std::function<void()> callback)
|
||||||
|
{
|
||||||
|
m_cancelCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
void ProgressWidget::paintEvent(QPaintEvent *)
|
void ProgressWidget::paintEvent(QPaintEvent *)
|
||||||
{
|
{
|
||||||
QPainter painter(this);
|
QPainter painter(this);
|
||||||
@ -78,10 +92,21 @@ void ProgressWidget::paintEvent(QPaintEvent *)
|
|||||||
|
|
||||||
painter.fillRect(rect(), m_backgroundColor);
|
painter.fillRect(rect(), m_backgroundColor);
|
||||||
|
|
||||||
if (!m_logoPixmap.isNull()) {
|
if (m_isHovered) {
|
||||||
QRect logoRect(
|
if (!m_closePixmap.isNull()) {
|
||||||
(width() - m_logoPixmap.width()) / 2, 5, m_logoPixmap.width(), m_logoPixmap.height());
|
int x = ((width() - (m_closePixmap.width() / 2)) / 2);
|
||||||
painter.drawPixmap(logoRect, m_logoPixmap);
|
int y = ((height() - (m_closePixmap.height() / 2)) / 2);
|
||||||
|
painter.drawPixmap(x, y, m_closePixmap);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!m_logoPixmap.isNull()) {
|
||||||
|
QRect logoRect(
|
||||||
|
(width() - m_logoPixmap.width()) / 2,
|
||||||
|
5,
|
||||||
|
m_logoPixmap.width(),
|
||||||
|
m_logoPixmap.height());
|
||||||
|
painter.drawPixmap(logoRect, m_logoPixmap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int dotSpacing = 6;
|
int dotSpacing = 6;
|
||||||
@ -111,4 +136,31 @@ void ProgressWidget::paintEvent(QPaintEvent *)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ProgressWidget::enterEvent(QEnterEvent *event)
|
||||||
|
{
|
||||||
|
Q_UNUSED(event);
|
||||||
|
m_isHovered = true;
|
||||||
|
setCursor(Qt::PointingHandCursor);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgressWidget::leaveEvent(QEvent *event)
|
||||||
|
{
|
||||||
|
Q_UNUSED(event);
|
||||||
|
m_isHovered = false;
|
||||||
|
setCursor(Qt::ArrowCursor);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgressWidget::mousePressEvent(QMouseEvent *event)
|
||||||
|
{
|
||||||
|
if (event->button() == Qt::LeftButton && m_isHovered) {
|
||||||
|
emit cancelRequested();
|
||||||
|
if (m_cancelCallback) {
|
||||||
|
m_cancelCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QWidget::mousePressEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
#include <utils/progressindicator.h>
|
#include <utils/progressindicator.h>
|
||||||
#include <utils/theme/theme.h>
|
#include <utils/theme/theme.h>
|
||||||
@ -30,12 +31,21 @@ namespace QodeAssist {
|
|||||||
|
|
||||||
class ProgressWidget : public QWidget
|
class ProgressWidget : public QWidget
|
||||||
{
|
{
|
||||||
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
ProgressWidget(QWidget *parent = nullptr);
|
ProgressWidget(QWidget *parent = nullptr);
|
||||||
~ProgressWidget();
|
~ProgressWidget();
|
||||||
|
|
||||||
|
void setCancelCallback(std::function<void()> callback);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void cancelRequested();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent *) override;
|
void paintEvent(QPaintEvent *) override;
|
||||||
|
void enterEvent(QEnterEvent *event) override;
|
||||||
|
void leaveEvent(QEvent *event) override;
|
||||||
|
void mousePressEvent(QMouseEvent *event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QTimer m_timer;
|
QTimer m_timer;
|
||||||
@ -43,6 +53,9 @@ private:
|
|||||||
QColor m_textColor;
|
QColor m_textColor;
|
||||||
QColor m_backgroundColor;
|
QColor m_backgroundColor;
|
||||||
QPixmap m_logoPixmap;
|
QPixmap m_logoPixmap;
|
||||||
|
QPixmap m_closePixmap;
|
||||||
|
bool m_isHovered;
|
||||||
|
std::function<void()> m_cancelCallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
Reference in New Issue
Block a user