mirror of
https://github.com/QuasarApp/Hanoi-Towers.git
synced 2025-04-27 10:14:31 +00:00
added an image provider and the tble of users avatars
This commit is contained in:
parent
6c6676d752
commit
0faeb4fdbd
@ -23,6 +23,14 @@ void ProfileData::setRecord(int rec) {
|
||||
emit recordChanged(rec);
|
||||
}
|
||||
|
||||
void ProfileData::setAvatarHash(int avatarHash) {
|
||||
if (_avatarHash == avatarHash)
|
||||
return;
|
||||
|
||||
_avatarHash = avatarHash;
|
||||
emit avatarHashChanged(_avatarHash);
|
||||
}
|
||||
|
||||
ProfileData::ProfileData(const QByteArray &userID):
|
||||
QObject(nullptr) {
|
||||
_userId = userID;
|
||||
@ -93,3 +101,7 @@ QString ProfileData::userId() const{
|
||||
QByteArray ProfileData::userIdRaw() const {
|
||||
return _userId;
|
||||
}
|
||||
|
||||
int ProfileData::avatarHash() const {
|
||||
return _avatarHash;
|
||||
}
|
||||
|
@ -16,8 +16,9 @@ class HANOITOWERSPROTOCOL_EXPORT ProfileData : public QObject, public QH::Stream
|
||||
|
||||
Q_PROPERTY(QObject* gameState READ gameState NOTIFY gameStateChanged)
|
||||
Q_PROPERTY(QString userId READ userId NOTIFY userIdChanged)
|
||||
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
|
||||
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
|
||||
Q_PROPERTY(int record READ record WRITE setRecord NOTIFY recordChanged)
|
||||
Q_PROPERTY(int avatarHash READ avatarHash WRITE setAvatarHash NOTIFY avatarHashChanged)
|
||||
Q_PROPERTY(bool onlineUser READ isOnline WRITE setOnline NOTIFY onlineChanged)
|
||||
|
||||
|
||||
@ -44,10 +45,14 @@ public:
|
||||
QString userId() const;
|
||||
QByteArray userIdRaw() const;
|
||||
|
||||
int avatarHash() const;
|
||||
|
||||
public slots:
|
||||
void setOnline(bool onlineUser);
|
||||
void setRecord(int record);
|
||||
|
||||
void setAvatarHash(int avatarHash);
|
||||
|
||||
signals:
|
||||
void gameStateChanged(QObject* gameState);
|
||||
void onlineChanged(bool onlineUser);
|
||||
@ -57,12 +62,15 @@ signals:
|
||||
|
||||
void userIdChanged(QString userId);
|
||||
|
||||
void avatarHashChanged(int avatarHash);
|
||||
|
||||
private:
|
||||
GameState _state;
|
||||
QString _name;
|
||||
int _record = 0;
|
||||
bool _online = false;
|
||||
QByteArray _userId;
|
||||
int _avatarHash;
|
||||
};
|
||||
|
||||
|
||||
|
64
HanoiTowers/Protockol/src/useravatar.cpp
Normal file
64
HanoiTowers/Protockol/src/useravatar.cpp
Normal file
@ -0,0 +1,64 @@
|
||||
#include "useravatar.h"
|
||||
|
||||
UserAvatar::UserAvatar(): QH::PKG::DBObject("Avatars") {
|
||||
|
||||
}
|
||||
|
||||
bool UserAvatar::copyFrom(const QH::PKG::AbstractData *other) {
|
||||
if (!DBObject::copyFrom(other))
|
||||
return false;
|
||||
|
||||
auto otherObject = dynamic_cast<const UserAvatar*>(other);
|
||||
if (!otherObject)
|
||||
return false;
|
||||
|
||||
_image = otherObject->_image;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QH::PKG::DBObject *UserAvatar::createDBObject() const {
|
||||
return create<UserAvatar>();
|
||||
}
|
||||
|
||||
bool UserAvatar::fromSqlRecord(const QSqlRecord &q) {
|
||||
if (!DBObject::fromSqlRecord(q)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_image = q.value("data").toByteArray();
|
||||
|
||||
return isValid();
|
||||
}
|
||||
|
||||
bool UserAvatar::isValid() const {
|
||||
return DBObject::isValid();
|
||||
}
|
||||
|
||||
QDataStream &UserAvatar::fromStream(QDataStream &stream) {
|
||||
DBObject::toStream(stream);
|
||||
stream >> _image;
|
||||
return stream;
|
||||
}
|
||||
|
||||
QDataStream &UserAvatar::toStream(QDataStream &stream) const {
|
||||
DBObject::toStream(stream);
|
||||
stream << _image;
|
||||
return stream;
|
||||
}
|
||||
|
||||
QH::BaseId UserAvatar::generateId() const {
|
||||
return getId();
|
||||
}
|
||||
|
||||
QH::PKG::DBVariantMap UserAvatar::variantMap() const {
|
||||
return {{"data", {_image, QH::PKG::MemberType::InsertUpdate}}};
|
||||
}
|
||||
|
||||
QByteArray UserAvatar::image() const {
|
||||
return _image;
|
||||
}
|
||||
|
||||
void UserAvatar::setImage(const QByteArray &image) {
|
||||
_image = image;
|
||||
}
|
30
HanoiTowers/Protockol/src/useravatar.h
Normal file
30
HanoiTowers/Protockol/src/useravatar.h
Normal file
@ -0,0 +1,30 @@
|
||||
#ifndef USERAVATAR_H
|
||||
#define USERAVATAR_H
|
||||
|
||||
#include <dbobject.h>
|
||||
|
||||
class UserAvatar: public QH::PKG::DBObject
|
||||
{
|
||||
public:
|
||||
UserAvatar();
|
||||
bool copyFrom(const QH::PKG::AbstractData *other) override;
|
||||
QH::PKG::DBObject *createDBObject() const override;
|
||||
bool fromSqlRecord(const QSqlRecord &q) override;
|
||||
bool isValid() const override;
|
||||
|
||||
// StreamBase interface
|
||||
QByteArray image() const;
|
||||
void setImage(const QByteArray &image);
|
||||
|
||||
protected:
|
||||
QDataStream &fromStream(QDataStream &stream) override;
|
||||
QDataStream &toStream(QDataStream &stream) const override;
|
||||
|
||||
QH::BaseId generateId() const override;
|
||||
QH::PKG::DBVariantMap variantMap() const override;
|
||||
|
||||
private:
|
||||
QByteArray _image;
|
||||
};
|
||||
|
||||
#endif // USERAVATAR_H
|
@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS UsersData (
|
||||
points INTEGER default 0,
|
||||
userdata BLOB default NULL,
|
||||
updateTime INTEGER default 0,
|
||||
userAvatar INTEGER default 0,
|
||||
|
||||
FOREIGN KEY(id) REFERENCES NetworkMembers(id)
|
||||
ON UPDATE CASCADE
|
||||
@ -11,10 +12,11 @@ CREATE TABLE IF NOT EXISTS UsersData (
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS Avatars (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
id INTEGER PRIMARY KEY NOT NULL
|
||||
user_id VARCHAR(64) NOT NULL,
|
||||
data BLOB default NULL,
|
||||
|
||||
FOREIGN KEY(id) REFERENCES NetworkMembers(id)
|
||||
FOREIGN KEY(user_id) REFERENCES NetworkMembers(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
@ -7,7 +7,7 @@ Item {
|
||||
property real spread: 0.1
|
||||
readonly property int duration: 10000
|
||||
|
||||
visible: backEnd.fog
|
||||
visible: backEnd && backEnd.fog
|
||||
|
||||
|
||||
Image {
|
||||
@ -71,7 +71,7 @@ Item {
|
||||
id: tim
|
||||
|
||||
repeat: true;
|
||||
running: backEnd.fogAnimation
|
||||
running: backEnd && backEnd.fogAnimation
|
||||
interval: root.duration
|
||||
onTriggered: {
|
||||
|
||||
|
@ -11,10 +11,12 @@
|
||||
#include <QDir>
|
||||
#include <qmlnotifyservice.h>
|
||||
#include "gamestate.h"
|
||||
#include "hanoiimageprovider.h"
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <lvmainmodel.h>
|
||||
#include <recordlistmodel.h>
|
||||
#include <execution>
|
||||
#include <QQmlContext>
|
||||
|
||||
#define DEFAULT_USER_ID QByteArray("DefaultUser")
|
||||
#define DEFAULT_USER_NAME "User"
|
||||
@ -48,6 +50,9 @@ BackEnd::BackEnd(QQmlApplicationEngine *engine):
|
||||
_recordsTable = new RecordListModel(this);
|
||||
_recordsTable->setSource(_client.localUsersPreview());
|
||||
|
||||
_imageProvider = new HanoiImageProvider(&_client);
|
||||
engine->addImageProvider("HanoiImages", _imageProvider);
|
||||
|
||||
connect(_loginModel , &LoginView::LVMainModel::sigLoginRequest,
|
||||
this, &BackEnd::handleOnlineRequest);
|
||||
|
||||
@ -69,6 +74,9 @@ ProfileData* BackEnd::initProfile(const QByteArray& userId, const QString &userN
|
||||
connect(_profile, &ProfileData::onlineRequest,
|
||||
this, &BackEnd::handleOnlineRequestfromProfile);
|
||||
|
||||
connect(_profile, &ProfileData::nameChanged,
|
||||
this, &BackEnd::handleChangeName);
|
||||
|
||||
if (!_client.login(userId)) {
|
||||
_profile->setName(userName);
|
||||
|
||||
@ -149,6 +157,10 @@ void BackEnd::handleOnlineRequestfromProfile(const QString &name) {
|
||||
emit showOnlinePage();
|
||||
}
|
||||
|
||||
void BackEnd::handleChangeName(const QString &) {
|
||||
_client.updateProfile(*_profile);
|
||||
}
|
||||
|
||||
void BackEnd::handleOnlineRequest(const LoginView::UserData & user) {
|
||||
|
||||
if (!_client.login(user.nickname().toLatin1(), user.rawPassword().toLatin1())) {
|
||||
@ -210,6 +222,16 @@ void BackEnd::setShowHelp(bool state) {
|
||||
_settings->setValue(FIRST_RUN_KEY, state);
|
||||
}
|
||||
|
||||
void BackEnd::setNewAvatar(const QString &pathToAvatar) {
|
||||
QImage image;
|
||||
if (pathToAvatar.contains("file://")) {
|
||||
image = QImage(pathToAvatar.right(pathToAvatar.size() - 7));
|
||||
} else {
|
||||
image = QImage(pathToAvatar);
|
||||
}
|
||||
_client.setNewAvatar(_profile->userId().toLocal8Bit(), image);
|
||||
}
|
||||
|
||||
bool BackEnd::fog() const {
|
||||
return _settings->getValue(FOG, true).toBool();
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ class UserData;
|
||||
}
|
||||
class QQmlApplicationEngine;
|
||||
class RecordListModel;
|
||||
class HanoiImageProvider;
|
||||
|
||||
class BackEnd: public QObject
|
||||
{
|
||||
@ -66,6 +67,7 @@ public:
|
||||
* @param state - a new state of show help message
|
||||
*/
|
||||
Q_INVOKABLE void setShowHelp(bool state);
|
||||
Q_INVOKABLE void setNewAvatar(const QString& pathToAvatar);
|
||||
|
||||
bool fog() const;
|
||||
|
||||
@ -151,6 +153,8 @@ signals:
|
||||
|
||||
private slots:
|
||||
void handleOnlineRequestfromProfile(const QString&);
|
||||
void handleChangeName(const QString&);
|
||||
|
||||
void handleOnlineRequest(const LoginView::UserData&);
|
||||
void handleOnlineRequestError(const QString&Errr);
|
||||
|
||||
@ -167,6 +171,7 @@ private:
|
||||
HanoiClient _client;
|
||||
|
||||
SettingsData _settingsData;
|
||||
HanoiImageProvider *_imageProvider = nullptr;
|
||||
|
||||
};
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
#include <authrequest.h>
|
||||
#include <userdatarequest.h>
|
||||
#include <sqldbwriter.h>
|
||||
#include <useravatar.h>
|
||||
#include "hanoierrorcodes.h"
|
||||
#include "localrecordstable.h"
|
||||
|
||||
@ -141,11 +142,47 @@ void HanoiClient::setStatus(const Status &status) {
|
||||
}
|
||||
}
|
||||
|
||||
bool HanoiClient::setNewAvatar(const QByteArray &userId, const QImage &image) {
|
||||
UserAvatar avatarData;
|
||||
avatarData.setId(userId);
|
||||
|
||||
QByteArray array;
|
||||
QDataStream stram(&array, QIODevice::WriteOnly);
|
||||
|
||||
stram << image;
|
||||
avatarData.setImage(array);
|
||||
|
||||
if (!db()->saveObject(&avatarData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto profile = currentProfile();
|
||||
|
||||
if(profile && profile->isOnline()) {
|
||||
return sendData(&avatarData, _serverAddress);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QImage HanoiClient::userAvatar(const QByteArray &userId) const {
|
||||
UserAvatar avatarData;
|
||||
avatarData.setId(userId);
|
||||
|
||||
auto result = db()->getObject(avatarData);
|
||||
|
||||
if (result) {
|
||||
return QImage::fromData(result->image());
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray HanoiClient::currentUserId() const {
|
||||
return _currentUserId;
|
||||
}
|
||||
|
||||
const ProfileData* HanoiClient::currentProfile() {
|
||||
const ProfileData* HanoiClient::currentProfile() const {
|
||||
|
||||
auto userData = getLocalUser(_currentUserId);
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
#define REMOTE_HOST "127.0.0.1"
|
||||
#endif
|
||||
#define REMOTE_PORT 7770
|
||||
#include <QImage>
|
||||
#include <databasenode.h>
|
||||
#include <profiledata.h>
|
||||
#include <userpreview.h>
|
||||
@ -46,7 +47,7 @@ public:
|
||||
|
||||
QByteArray currentUserId() const;
|
||||
|
||||
const ProfileData *currentProfile();
|
||||
const ProfileData *currentProfile() const;
|
||||
bool updateProfile(const ProfileData& profile);
|
||||
bool addProfile(const ProfileData& profile);
|
||||
|
||||
@ -64,6 +65,9 @@ public:
|
||||
Status getStatus() const;
|
||||
void setStatus(const Status &status);
|
||||
|
||||
bool setNewAvatar(const QByteArray &userId, const QImage& image);
|
||||
QImage userAvatar(const QByteArray &userId) const;
|
||||
|
||||
protected:
|
||||
void nodeConfirmend(const QH::HostAddress &node) override;
|
||||
void nodeConnected(const QH::HostAddress &node) override;
|
||||
|
48
HanoiTowers/client/hanoiimageprovider.cpp
Normal file
48
HanoiTowers/client/hanoiimageprovider.cpp
Normal file
@ -0,0 +1,48 @@
|
||||
#include "hanoiclient.h"
|
||||
#include "hanoiimageprovider.h"
|
||||
|
||||
#include <QThread>
|
||||
#include <QThreadPool>
|
||||
|
||||
HanoiImageProvider::HanoiImageProvider(const HanoiClient *client) {
|
||||
_pool = new QThreadPool();
|
||||
_client = client;
|
||||
}
|
||||
|
||||
HanoiImageProvider::~HanoiImageProvider() {
|
||||
_pool->deleteLater();
|
||||
}
|
||||
|
||||
QQuickImageResponse *HanoiImageProvider::requestImageResponse(const QString &id,
|
||||
const QSize &requestedSize) {
|
||||
|
||||
AsyncImageResponse *response = new AsyncImageResponse(id, requestedSize, _client);
|
||||
_pool->start(response);
|
||||
return response;
|
||||
|
||||
}
|
||||
|
||||
AsyncImageResponse::AsyncImageResponse(const QString &id, const QSize &requestedSize,
|
||||
const HanoiClient *client)
|
||||
: m_id(id), m_requestedSize(requestedSize), m_texture(0), _client(client) {
|
||||
setAutoDelete(false);
|
||||
}
|
||||
|
||||
QQuickTextureFactory *AsyncImageResponse::textureFactory() const {
|
||||
return m_texture;
|
||||
}
|
||||
|
||||
void AsyncImageResponse::run() {
|
||||
|
||||
QImage image = _client->userAvatar(m_id.toLocal8Bit());;
|
||||
|
||||
if (image.isNull()) {
|
||||
image = QImage(":/img/DefaultAvatar");
|
||||
}
|
||||
|
||||
if (m_requestedSize.isValid())
|
||||
image = image.scaled(m_requestedSize);
|
||||
|
||||
m_texture = QQuickTextureFactory::textureFactoryForImage(image);
|
||||
emit finished();
|
||||
}
|
41
HanoiTowers/client/hanoiimageprovider.h
Normal file
41
HanoiTowers/client/hanoiimageprovider.h
Normal file
@ -0,0 +1,41 @@
|
||||
#ifndef HANOIIMAGEPROVIDER_H
|
||||
#define HANOIIMAGEPROVIDER_H
|
||||
|
||||
#include <QQuickAsyncImageProvider>
|
||||
#include <QRunnable>
|
||||
|
||||
class QThreadPool;
|
||||
class HanoiClient;
|
||||
|
||||
class AsyncImageResponse : public QQuickImageResponse, public QRunnable
|
||||
{
|
||||
public:
|
||||
AsyncImageResponse(const QString &id, const QSize &requestedSize, const HanoiClient *client);
|
||||
|
||||
QQuickTextureFactory *textureFactory() const override;
|
||||
|
||||
void run() override;
|
||||
|
||||
QString m_id;
|
||||
QSize m_requestedSize;
|
||||
QQuickTextureFactory *m_texture;
|
||||
|
||||
private:
|
||||
const HanoiClient *_client = nullptr;
|
||||
};
|
||||
|
||||
|
||||
class HanoiImageProvider: public QQuickAsyncImageProvider
|
||||
{
|
||||
public:
|
||||
HanoiImageProvider(const HanoiClient* client);
|
||||
~HanoiImageProvider() override;
|
||||
|
||||
QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
|
||||
|
||||
private:
|
||||
QThreadPool *_pool = nullptr;
|
||||
const HanoiClient *_client = nullptr;
|
||||
};
|
||||
|
||||
#endif // HANOIIMAGEPROVIDER_H
|
@ -3,6 +3,7 @@ import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Window 2.15
|
||||
import QtQuick.Dialogs 1.0
|
||||
|
||||
GridLayout {
|
||||
id: mainLayout
|
||||
@ -10,16 +11,37 @@ GridLayout {
|
||||
columns: 2
|
||||
clip: true
|
||||
|
||||
Image {
|
||||
id: userAvatar
|
||||
source: "qrc:/qtquickplugin/images/template_image.png"
|
||||
property var userModel: null
|
||||
|
||||
signal newAvatar(var path);
|
||||
|
||||
Button {
|
||||
Layout.preferredHeight: 50 * Screen.pixelDensity
|
||||
Layout.preferredWidth: 50 * Screen.pixelDensity
|
||||
Layout.rowSpan: 1
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
Button {
|
||||
Image {
|
||||
id: userAvatar
|
||||
source: "image://HanoiImages/" + ((userModel)? userModel.userId: "")
|
||||
anchors.fill: parent
|
||||
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
opacity: 0.1
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
fileDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
title: qsTr("Please choose a new Avatar")
|
||||
folder: shortcuts.home
|
||||
onAccepted: {
|
||||
if (fileDialog.fileUrls.length) {
|
||||
newAvatar(fileDialog.fileUrls[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,21 +82,27 @@ GridLayout {
|
||||
id: eid
|
||||
|
||||
readOnly: true
|
||||
text: ""
|
||||
text: (userModel)? userModel.userId: ""
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: ename
|
||||
|
||||
text: qsTr("")
|
||||
text: (userModel)? userModel.name: ""
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
maximumLength: 64
|
||||
|
||||
onEditingFinished: {
|
||||
if (userModel)
|
||||
userModel.name = text
|
||||
}
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: erecord
|
||||
|
||||
text: "0"
|
||||
text: (userModel)? userModel.record: ""
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
readOnly: true
|
||||
@ -83,7 +111,13 @@ GridLayout {
|
||||
|
||||
Switch {
|
||||
id: eonline
|
||||
text: qsTr("")
|
||||
checked: (userModel)? userModel.onlineUser: false
|
||||
|
||||
onCheckedChanged: {
|
||||
if (userModel)
|
||||
userModel.onlineUser = checked
|
||||
}
|
||||
text: ""
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,11 @@ Item {
|
||||
|
||||
UserView {
|
||||
Layout.rowSpan: 3
|
||||
userModel: (backEnd)? backEnd.profileObject: null
|
||||
|
||||
onNewAvatar: {
|
||||
backEnd.setNewAvatar(path);
|
||||
}
|
||||
}
|
||||
|
||||
Base.BaseButton {
|
||||
|
@ -31,6 +31,7 @@
|
||||
</qresource>
|
||||
<qresource prefix="/img">
|
||||
<file alias="Help">res/help.png</file>
|
||||
<file alias="DefaultAvatar">res/DefaultAvatar.png</file>
|
||||
</qresource>
|
||||
<qresource prefix="/languages">
|
||||
<file alias="ru">languages/ru.qm</file>
|
||||
|
BIN
HanoiTowers/client/res/DefaultAvatar.png
Normal file
BIN
HanoiTowers/client/res/DefaultAvatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 82 KiB |
@ -4,13 +4,16 @@ CREATE TABLE IF NOT EXISTS Users (
|
||||
token BLOB default NULL,
|
||||
userdata BLOB default NULL,
|
||||
updateTime INTEGER default 0
|
||||
userAvatar INTEGER default 0,
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS Avatars (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
id INTEGER PRIMARY KEY NOT NULL
|
||||
user_id VARCHAR(64) NOT NULL,
|
||||
data BLOB default NULL,
|
||||
|
||||
FOREIGN KEY(id) REFERENCES Users(id)
|
||||
FOREIGN KEY(user_id) REFERENCES Users(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
2
Heart
2
Heart
@ -1 +1 @@
|
||||
Subproject commit 894fa6efcfcbf69ed8e2b39b2ba90b16b3fbfe93
|
||||
Subproject commit acd1276f2a7dd8a8646e00737433146f2beb5ea6
|
Loading…
x
Reference in New Issue
Block a user