Add join room page
This commit is contained in:
@@ -1,269 +0,0 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Effect 2.0
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
|
||||
Dialog {
|
||||
property var connection
|
||||
|
||||
property string keyword
|
||||
property string server
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: 480
|
||||
height: Math.min(window.height - 100, 800)
|
||||
|
||||
id: root
|
||||
|
||||
title: "Explore Rooms"
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
AutoTextField {
|
||||
property bool isRoomAlias: text.match(/#(.+):(.+)/g)
|
||||
property var room: isRoomAlias ? connection.roomByAlias(text) : null
|
||||
property bool isJoined: room != null
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
id: identifierField
|
||||
|
||||
placeholderText: "Find a room..."
|
||||
|
||||
onEditingFinished: {
|
||||
keyword = text
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: joinButton
|
||||
|
||||
visible: identifierField.isRoomAlias
|
||||
|
||||
text: identifierField.isJoined ? "View" : "Join"
|
||||
highlighted: true
|
||||
flat: identifierField.isJoined
|
||||
|
||||
onClicked: {
|
||||
if (identifierField.isJoined) {
|
||||
roomListForm.joinRoom(identifierField.room)
|
||||
} else {
|
||||
Controller.joinRoom(connection, identifierField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
Layout.maximumWidth: 120
|
||||
|
||||
id: serverField
|
||||
|
||||
editable: currentIndex == 1
|
||||
|
||||
model: ["Local", "Global", "matrix.org"]
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
if (currentIndex == 0) {
|
||||
server = ""
|
||||
} else if (currentIndex == 2) {
|
||||
server = "matrix.org"
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
if (currentIndex == 1) {
|
||||
server = editText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MenuSeparator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
AutoListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
id: publicRoomsListView
|
||||
|
||||
clip: true
|
||||
|
||||
spacing: 4
|
||||
|
||||
model: PublicRoomListModel {
|
||||
id: publicRoomListModel
|
||||
|
||||
connection: root.connection
|
||||
server: root.server
|
||||
keyword: root.keyword
|
||||
}
|
||||
|
||||
delegate: Control {
|
||||
width: publicRoomsListView.width
|
||||
height: 48
|
||||
|
||||
padding: 8
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 8
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: height
|
||||
Layout.fillHeight: true
|
||||
|
||||
source: model.avatarMediaId ? "image://mxc/" + model.avatarMediaId : ""
|
||||
hint: name
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
spacing: 4
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
text: name
|
||||
color: MPalette.foreground
|
||||
font.pixelSize: 13
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: allowGuests
|
||||
|
||||
text: "GUESTS CAN JOIN"
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 10
|
||||
padding: 4
|
||||
|
||||
background: Rectangle {
|
||||
color: MPalette.banner
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: worldReadable
|
||||
|
||||
text: "WORLD READABLE"
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 10
|
||||
padding: 4
|
||||
|
||||
background: Rectangle {
|
||||
color: MPalette.banner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
visible: text
|
||||
|
||||
text: topic ? topic.replace(/(\r\n\t|\n|\r\t)/gm," ") : ""
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 10
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.preferredWidth: 16
|
||||
Layout.preferredHeight: 16
|
||||
|
||||
icon: "\ue7fc"
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 16
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.preferredWidth: 36
|
||||
|
||||
text: memberCount
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
Control {
|
||||
Layout.preferredWidth: 32
|
||||
Layout.preferredHeight: 32
|
||||
|
||||
visible: isJoined
|
||||
|
||||
contentItem: MaterialIcon {
|
||||
icon: "\ue89e"
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 20
|
||||
}
|
||||
|
||||
background: RippleEffect {
|
||||
circular: true
|
||||
|
||||
onClicked: {
|
||||
roomListForm.joinRoom(connection.room(roomID))
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
Layout.preferredWidth: 32
|
||||
Layout.preferredHeight: 32
|
||||
|
||||
visible: !isJoined
|
||||
|
||||
contentItem: MaterialIcon {
|
||||
icon: "\ue7f0"
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 20
|
||||
}
|
||||
|
||||
background: RippleEffect {
|
||||
circular: true
|
||||
|
||||
onClicked: {
|
||||
Controller.joinRoom(connection, roomID)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
|
||||
onContentYChanged: {
|
||||
if(publicRoomListModel.hasMore && contentHeight - contentY < publicRoomsListView.height + 200)
|
||||
publicRoomListModel.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: destroy()
|
||||
}
|
||||
@@ -4,7 +4,6 @@ UserDetailDialog 2.0 UserDetailDialog.qml
|
||||
MessageSourceDialog 2.0 MessageSourceDialog.qml
|
||||
LoginDialog 2.0 LoginDialog.qml
|
||||
CreateRoomDialog 2.0 CreateRoomDialog.qml
|
||||
JoinRoomDialog 2.0 JoinRoomDialog.qml
|
||||
InviteUserDialog 2.0 InviteUserDialog.qml
|
||||
AcceptInvitationDialog 2.0 AcceptInvitationDialog.qml
|
||||
FontFamilyDialog 2.0 FontFamilyDialog.qml
|
||||
|
||||
173
imports/NeoChat/Page/JoinRoomPage.qml
Normal file
173
imports/NeoChat/Page/JoinRoomPage.qml
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
*
|
||||
* SPDX-LicenseIdentifier: GPL-3.0-only
|
||||
*/
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import NeoChat 2.0
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Effect 2.0
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
id: root
|
||||
property var connection
|
||||
|
||||
property string keyword
|
||||
property string server
|
||||
|
||||
signal joinRoom(string room)
|
||||
|
||||
title: i18n("Explore Rooms")
|
||||
|
||||
header: Control {
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
contentItem: RowLayout {
|
||||
Kirigami.SearchField {
|
||||
property bool isRoomAlias: text.match(/#(.+):(.+)/g)
|
||||
property var room: isRoomAlias ? connection.roomByAlias(text) : null
|
||||
property bool isJoined: room != null
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
id: identifierField
|
||||
|
||||
placeholderText: i18n("Find a room...")
|
||||
|
||||
onEditingFinished: {
|
||||
keyword = text
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: joinButton
|
||||
|
||||
visible: identifierField.isRoomAlias
|
||||
|
||||
text: identifierField.isJoined ? i18n("View") : i18n("Join")
|
||||
highlighted: true
|
||||
|
||||
onClicked: {
|
||||
if (!identifierField.isJoined) {
|
||||
Controller.joinRoom(connection, identifierField.text);
|
||||
}
|
||||
RoomManager.enterRoom(connection.room(identifierField.room));
|
||||
applicationWindow().pageStack.layers.pop();
|
||||
}
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
Layout.maximumWidth: 120
|
||||
|
||||
id: serverField
|
||||
|
||||
editable: currentIndex == 1
|
||||
|
||||
model: [i18n("Local"), i18n("Global"), "matrix.org"]
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
if (currentIndex == 0) {
|
||||
server = ""
|
||||
} else if (currentIndex == 2) {
|
||||
server = "matrix.org"
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
if (currentIndex == 1) {
|
||||
server = editText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: publicRoomsListView
|
||||
clip: true
|
||||
model: PublicRoomListModel {
|
||||
id: publicRoomListModel
|
||||
|
||||
connection: root.connection
|
||||
server: root.server
|
||||
keyword: root.keyword
|
||||
}
|
||||
|
||||
onContentYChanged: {
|
||||
if(publicRoomListModel.hasMore && contentHeight - contentY < publicRoomsListView.height + 200)
|
||||
publicRoomListModel.next();
|
||||
}
|
||||
delegate: Kirigami.AbstractListItem {
|
||||
property bool justJoined: false
|
||||
width: publicRoomsListView.width
|
||||
onClicked: {
|
||||
if (!isJoined) {
|
||||
Controller.joinRoom(connection, roomID)
|
||||
justJoined = true;
|
||||
} else {
|
||||
RoomManager.enterRoom(connection.room(roomID))
|
||||
applicationWindow().pageStack.layers.pop();
|
||||
}
|
||||
}
|
||||
contentItem: RowLayout {
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
|
||||
|
||||
source: model.avatar ? "image://mxc/" + model.avatar : ""
|
||||
name: name
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
level: 4
|
||||
text: name
|
||||
font.bold: true
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
Label {
|
||||
visible: isJoined || justJoined
|
||||
text: i18n("Joined")
|
||||
color: Kirigami.Theme.linkColor
|
||||
}
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
visible: text
|
||||
text: topic ? topic.replace(/(\r\n\t|\n|\r\t)/gm," ") : ""
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Icon {
|
||||
source: "user"
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
Label {
|
||||
text: memberCount + " " + (alias ?? roomID)
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import org.kde.kirigami 2.13 as Kirigami
|
||||
import org.kde.kitemmodels 1.0
|
||||
import org.kde.neochat 0.1
|
||||
|
||||
import NeoChat 2.0
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Menu 2.0
|
||||
|
||||
@@ -121,17 +122,10 @@ Kirigami.ScrollablePage {
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
console.log(mouse.button)
|
||||
if (mouse.button == Qt.RightButton) {
|
||||
roomListContextMenu.createObject(parent, {"room": currentRoom}).popup()
|
||||
} else {
|
||||
if (enteredRoom) {
|
||||
leaveRoom(enteredRoom)
|
||||
}
|
||||
|
||||
enteredRoom = currentRoom
|
||||
|
||||
enterRoom(enteredRoom)
|
||||
RoomManager.enterRoom(currentRoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@ LoadingPage 2.0 LoadingPage.qml
|
||||
LoginPage 2.0 LoginPage.qml
|
||||
RoomListPage 2.0 RoomListPage.qml
|
||||
RoomPage 2.0 RoomPage.qml
|
||||
JoinRoomPage 2.0 JoinRoomPage.qml
|
||||
|
||||
40
imports/NeoChat/RoomManager.qml
Normal file
40
imports/NeoChat/RoomManager.qml
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
*
|
||||
* SPDX-LicenseIdentifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick 2.14
|
||||
import NeoChat.Page 2.0
|
||||
|
||||
/**
|
||||
* Manage opening and close rooms
|
||||
*/
|
||||
Item {
|
||||
id: openRoomAction
|
||||
|
||||
property var currentRoom: null
|
||||
property var pageStack: null
|
||||
|
||||
readonly property bool hasOpenRoom: currentRoom != null
|
||||
|
||||
signal leaveRoom(string room);
|
||||
signal openRoom(string room);
|
||||
|
||||
function enterRoom(room) {
|
||||
if (currentRoom != null) {
|
||||
currentRoom = null;
|
||||
pageStack.removePage(pageStack.lastItem);
|
||||
}
|
||||
pageStack.push(roomPage, {"currentRoom": room});
|
||||
currentRoom = room;
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomPage
|
||||
|
||||
RoomPage {}
|
||||
}
|
||||
}
|
||||
1
imports/NeoChat/qmldir
Normal file
1
imports/NeoChat/qmldir
Normal file
@@ -0,0 +1 @@
|
||||
singleton RoomManager 2.0 RoomManager.qml
|
||||
40
qml/main.qml
40
qml/main.qml
@@ -5,20 +5,25 @@
|
||||
* SPDX-LicenseIdentifier: GPL-3.0-only
|
||||
*/
|
||||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14 as Controls
|
||||
import QtQuick.Controls 2.14 as QQC2
|
||||
import QtQuick.Layouts 1.14
|
||||
|
||||
import org.kde.kirigami 2.12 as Kirigami
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
import NeoChat 2.0
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Panel 2.0
|
||||
import NeoChat.Dialog 2.0
|
||||
import NeoChat.Page 2.0
|
||||
import NeoChat.Page 2.0
|
||||
|
||||
Kirigami.ApplicationWindow {
|
||||
id: root
|
||||
property var currentRoom: null
|
||||
|
||||
Component.onCompleted: RoomManager.pageStack = root.pageStack
|
||||
|
||||
contextDrawer: RoomDrawer {
|
||||
id: contextDrawer
|
||||
enabled: root.currentRoomm !== null
|
||||
@@ -30,16 +35,22 @@ Kirigami.ApplicationWindow {
|
||||
isMenu: true
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18n("About Neochat")
|
||||
iconName: "help-about"
|
||||
onTriggered: pageStack.layers.push(aboutPage)
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("About")
|
||||
text: i18n("Explore rooms")
|
||||
iconName: "compass"
|
||||
onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/JoinRoomPage.qml", {"connection": Controller.connection})
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Explore Rooms")
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Accounts")
|
||||
iconName: "im-user"
|
||||
onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/AccountsPage.qml")
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Accounts")
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("About Neochat")
|
||||
iconName: "help-about"
|
||||
onTriggered: pageStack.layers.push(aboutPage)
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("About")
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -58,17 +69,6 @@ Kirigami.ApplicationWindow {
|
||||
RoomListPage {
|
||||
id: roomList
|
||||
roomListModel: spectralRoomListModel
|
||||
|
||||
onEnterRoom: {
|
||||
applicationWindow().pageStack.push(roomPanelComponent, {"currentRoom": room});
|
||||
root.currentRoom = room;
|
||||
}
|
||||
onLeaveRoom: {
|
||||
var stack = applicationWindow().pageStack;
|
||||
roomList.enteredRoom = null;
|
||||
|
||||
stack.removePage(stack.lastItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,12 +103,4 @@ Kirigami.ApplicationWindow {
|
||||
|
||||
connection: Controller.connection
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomPanelComponent
|
||||
|
||||
RoomPage {
|
||||
currentRoom: root.currentRoom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
res.qrc
4
res.qrc
@@ -3,12 +3,15 @@
|
||||
<file>assets/img/matrix.svg</file>
|
||||
<file>assets/img/icon.png</file>
|
||||
<file>qml/main.qml</file>
|
||||
<file>imports/NeoChat/qmldir</file>
|
||||
<file>imports/NeoChat/RoomManager.qml</file>
|
||||
<file>imports/NeoChat/Page/qmldir</file>
|
||||
<file>imports/NeoChat/Page/LoginPage.qml</file>
|
||||
<file>imports/NeoChat/Page/LoadingPage.qml</file>
|
||||
<file>imports/NeoChat/Page/RoomListPage.qml</file>
|
||||
<file>imports/NeoChat/Page/RoomPage.qml</file>
|
||||
<file>imports/NeoChat/Page/AccountsPage.qml</file>
|
||||
<file>imports/NeoChat/Page/JoinRoomPage.qml</file>
|
||||
<file>imports/NeoChat/Component/qmldir</file>
|
||||
<file>imports/NeoChat/Component/ChatTextInput.qml</file>
|
||||
<file>imports/NeoChat/Component/AutoMouseArea.qml</file>
|
||||
@@ -47,7 +50,6 @@
|
||||
<file>imports/NeoChat/Dialog/UserDetailDialog.qml</file>
|
||||
<file>imports/NeoChat/Dialog/MessageSourceDialog.qml</file>
|
||||
<file>imports/NeoChat/Dialog/CreateRoomDialog.qml</file>
|
||||
<file>imports/NeoChat/Dialog/JoinRoomDialog.qml</file>
|
||||
<file>imports/NeoChat/Dialog/InviteUserDialog.qml</file>
|
||||
<file>imports/NeoChat/Dialog/AcceptInvitationDialog.qml</file>
|
||||
<file>imports/NeoChat/Dialog/StartChatDialog.qml</file>
|
||||
|
||||
@@ -177,6 +177,12 @@ QVariant PublicRoomListModel::data(const QModelIndex &index, int role) const
|
||||
if (role == RoomIDRole) {
|
||||
return room.roomId;
|
||||
}
|
||||
if (role == AliasRole) {
|
||||
if (!room.canonicalAlias.isEmpty()) {
|
||||
return room.canonicalAlias;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (role == MemberCountRole) {
|
||||
return room.numJoinedMembers;
|
||||
}
|
||||
@@ -208,6 +214,7 @@ QHash<int, QByteArray> PublicRoomListModel::roleNames() const
|
||||
roles[AllowGuestsRole] = "allowGuests";
|
||||
roles[WorldReadableRole] = "worldReadable";
|
||||
roles[IsJoinedRole] = "isJoined";
|
||||
roles[AliasRole] = "alias";
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ public:
|
||||
AvatarRole,
|
||||
TopicRole,
|
||||
RoomIDRole,
|
||||
AliasRole,
|
||||
MemberCountRole,
|
||||
AllowGuestsRole,
|
||||
WorldReadableRole,
|
||||
|
||||
Reference in New Issue
Block a user