Page MenuHomePhorge

No OneTemporary

Size
25 KB
Referenced Files
None
Subscribers
None
diff --git a/src/contents/ui/UserPage.qml b/src/contents/ui/UserPage.qml
index 7a6d1ca..1718d3a 100644
--- a/src/contents/ui/UserPage.qml
+++ b/src/contents/ui/UserPage.qml
@@ -1,333 +1,364 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import QtQuick 2.2
-import QtQuick.Layouts 1.15
-import QtQuick.Controls 2.15
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
-import org.kde.kirigami 2.20 as Kirigami
-import moe.kazv.mxc.kazv 0.0 as MK
+import org.kde.kirigami as Kirigami
+import moe.kazv.mxc.kazv as MK
import '.' as Kazv
import 'device-mgmt' as KazvDM
import 'matrix-helpers.js' as Helpers
Kazv.ClosableScrollablePage {
id: userPage
property string userId: ''
property var user: ({})
property var nameProvider: Kazv.UserNameProvider {
user: userPage.user
}
property var room
property var roomNameProvider: Kazv.RoomNameProvider {
room: userPage.room
}
property var roomName: roomNameProvider.name
property var editingPowerLevel: false
property var submittingPowerLevel: false
property var powerLevelsLoaded: false
property var userPowerLevel: room.userPowerLevel(userId)
property var kickingUser: false
property var canKickUser: !(user.membership === 'ban' || user.membership === 'leave')
property var banningUser: false
property var unbanningUser: false
Connections {
target: room
function onPowerLevelsChanged() {
userPage.userPowerLevel = room.userPowerLevel(userPage.userId);
}
}
title: nameProvider.name
property var ensureMemberEvent: Kazv.AsyncHandler {
trigger: () => room.ensureStateEvent('m.room.member', userId)
}
property var ensurePowerLevels: Kazv.AsyncHandler {
trigger: () => room.ensureStateEvent('m.room.power_levels', '')
onResolved: (success, data) => {
if (success) {
userPage.powerLevelsLoaded = true;
}
}
}
property var updatingNameOverride: false
property var updateNameOverride: Kazv.AsyncHandler {
trigger: () => {
userPage.updatingNameOverride = true;
return sdkVars.userGivenNicknameMap.setAndUpload(
userPage.userId, nameOverrideInput.text || null);
}
onResolved: (success, data) => {
userPage.updatingNameOverride = false;
if (!success) {
showPassiveNotification(l10n.get('user-page-update-name-override-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
Component.onCompleted: {
userPage.ensureMemberEvent.call();
userPage.ensurePowerLevels.call();
}
property var setPowerLevel: Kazv.AsyncHandler {
trigger: () => {
userPage.submittingPowerLevel = true;
return room.setUserPowerLevel(userPage.userId, parseInt(newPowerLevel.text));
}
onResolved: (success, data) => {
if (!success) {
showPassiveNotification(l10n.get('user-page-set-power-level-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
} else {
userPage.editingPowerLevel = false;
}
userPage.submittingPowerLevel = false;
}
}
property var kickUser: Kazv.AsyncHandler {
trigger: () => {
userPage.kickingUser = true;
return room.kickUser(userPage.userId, kickUserReasonInput.text);
}
onResolved: {
if (!success) {
showPassiveNotification(l10n.get('user-page-kick-user-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
} else {
kickUserReasonInput.text = '';
}
userPage.kickingUser = false;
}
}
property var banUser: Kazv.AsyncHandler {
trigger: () => {
userPage.banningUser = true;
return room.banUser(userPage.userId, banUserReasonInput.text);
}
onResolved: {
if (!success) {
showPassiveNotification(l10n.get('user-page-ban-user-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
} else {
banUserReasonInput.text = '';
}
userPage.banningUser = false;
}
}
property var unbanUser: Kazv.AsyncHandler {
trigger: () => {
userPage.unbanningUser = true;
return room.unbanUser(userPage.userId);
}
onResolved: {
if (!success) {
showPassiveNotification(l10n.get('user-page-unban-user-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
}
userPage.unbanningUser = false;
}
}
ColumnLayout {
Kazv.AvatarAdapter {
id: avatar
objectName: 'avatar'
Layout.alignment: Qt.AlignHCenter
Layout.preferredHeight: Kirigami.Units.iconSizes.enormous
Layout.preferredWidth: Kirigami.Units.iconSizes.enormous
source: userPage.user.avatarMxcUri ? matrixSdk.mxcUriToHttp(userPage.user.avatarMxcUri) : ''
sourceSize.width: Kirigami.Units.iconSizes.enormous
sourceSize.height: Kirigami.Units.iconSizes.enormous
name: nameProvider.name
}
+ ColumnLayout {
+ Layout.preferredWidth: this.parent.width
+ Label {
+ objectName: 'userNameLabel'
+ Layout.alignment: Qt.AlignHCenter
+ font.pixelSize: Kirigami.Units.gridUnit
+ text: !!userPage.user.name ? userPage.user.name : userPage.userId
+ }
+ Label {
+ objectName: 'userIdLabel'
+ Layout.alignment: Qt.AlignHCenter
+ font.pixelSize: Kirigami.Units.gridUnit * 0.8
+ visible: !!userPage.user.name
+ text: userPage.userId
+ }
+ }
+
RowLayout {
// Do not allow user to set a name override for themselves
visible: userPage.userId !== matrixSdk.userId
TextField {
id: nameOverrideInput
objectName: 'nameOverrideInput'
placeholderText: l10n.get('user-page-overrided-name-placeholder')
Layout.fillWidth: true
enabled: !userPage.updatingNameOverride
text: nameProvider.overridedName
}
Button {
objectName: 'saveNameOverrideButton'
text: l10n.get('user-page-save-name-override-action')
enabled: !userPage.updatingNameOverride
onClicked: updateNameOverride.call()
}
}
RowLayout {
visible: !userPage.editingPowerLevel
Label {
objectName: 'powerLevelLabel'
Layout.fillWidth: true
text: l10n.get('user-page-power-level', { powerLevel: userPage.userPowerLevel })
}
Button {
objectName: 'editPowerLevelButton'
text: l10n.get('user-page-edit-power-level-action')
enabled: userPage.powerLevelsLoaded
onClicked: {
userPage.editingPowerLevel = true;
}
}
}
RowLayout {
visible: userPage.editingPowerLevel
TextField {
objectName: 'newPowerLevelInput'
id: newPowerLevel
Layout.fillWidth: true
text: `${userPage.userPowerLevel}`
readOnly: userPage.submittingPowerLevel
}
Button {
objectName: 'savePowerLevelButton'
text: l10n.get('user-page-save-power-level-action')
onClicked: {
setPowerLevel.call();
}
enabled: !userPage.submittingPowerLevel
}
Button {
objectName: 'discardPowerLevelButton'
text: l10n.get('user-page-discard-power-level-action')
enabled: !userPage.submittingPowerLevel
onClicked: {
userPage.editingPowerLevel = false;
newPowerLevel.text = `${userPage.userPowerLevel}`;
}
}
}
ColumnLayout {
Kirigami.PromptDialog {
id: kickUserReasonDialog
objectName: 'kickUserReasonDialog'
title: l10n.get('user-page-kick-user-confirm-dialog-title')
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
onAccepted: {
userPage.kickUser.call()
}
ColumnLayout {
Label {
Layout.fillWidth: true
text: l10n.get('user-page-kick-user-confirm-dialog-content', {
userId: userPage.user.userId,
name: nameProvider.name,
roomName: userPage.roomName,
})
wrapMode: Text.Wrap
}
- Kirigami.FormLayout {
- Layout.fillWidth: true
- TextField {
+ ColumnLayout {
+ Label {
Layout.fillWidth: true
- id: kickUserReasonInput
- objectName: 'kickUserReasonInput'
- readOnly: userPage.kickingUser
- Kirigami.FormData.label: l10n.get('user-page-kick-user-reason-prompt')
+ text: l10n.get('user-page-kick-user-confirm-dialog-content', {
+ userId: userPage.user.userId,
+ name: userPage.user.name,
+ roomName: userPage.roomName,
+ })
+ wrapMode: Text.Wrap
+ }
+
+ Kirigami.FormLayout {
+ Layout.fillWidth: true
+ TextField {
+ Layout.fillWidth: true
+ id: kickUserReasonInput
+ objectName: 'kickUserReasonInput'
+ readOnly: userPage.kickingUser
+ Kirigami.FormData.label: l10n.get('user-page-kick-user-reason-prompt')
+ }
}
}
}
}
-
Button {
objectName: 'kickUserButton'
+ icon.name: 'im-kick-user'
Layout.fillWidth: true
enabled: !userPage.kickingUser
text: l10n.get('user-page-kick-user-action')
visible: userPage.canKickUser
onClicked: {
kickUserReasonDialog.open()
}
}
}
RowLayout {
Kirigami.PromptDialog {
id: banUserReasonDialog
objectName: 'banUserReasonDialog'
title: l10n.get('user-page-ban-user-confirm-dialog-title')
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
onAccepted: {
userPage.banUser.call()
}
ColumnLayout {
Label {
Layout.fillWidth: true
text: l10n.get('user-page-ban-user-confirm-dialog-content', {
userId: userPage.user.userId,
name: nameProvider.name,
roomName: userPage.roomName,
})
wrapMode: Text.Wrap
}
Kirigami.FormLayout {
Layout.fillWidth: true
TextField {
Layout.fillWidth: true
id: banUserReasonInput
objectName: 'banUserReasonInput'
readOnly: userPage.banningUser
Kirigami.FormData.label: l10n.get('user-page-ban-user-reason-prompt')
}
}
}
}
Button {
objectName: 'banUserButton'
Layout.fillWidth: true
+ icon.name: 'im-ban-kick-user'
enabled: !userPage.banningUser
text: l10n.get('user-page-ban-user-action')
visible: !unbanUserButton.visible
onClicked: {
banUserReasonDialog.open()
}
}
Button {
id: unbanUserButton
objectName: 'unbanUserButton'
Layout.fillWidth: true
+ icon.name: 'im-user-online'
enabled: !userPage.unbanningUser
text: l10n.get('user-page-unban-user-action')
visible: user.membership === "ban"
onClicked: {
userPage.unbanUser.call()
}
}
}
KazvDM.DeviceList {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: childrenRect.height
userId: userPage.userId
devices: matrixSdk.devicesOfUser(userId)
}
}
}
diff --git a/src/tests/quick-tests/tst_UserPage.qml b/src/tests/quick-tests/tst_UserPage.qml
index f9b4cfd..62ce23b 100644
--- a/src/tests/quick-tests/tst_UserPage.qml
+++ b/src/tests/quick-tests/tst_UserPage.qml
@@ -1,342 +1,366 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtTest 1.0
import '../../contents/ui' as Kazv
import './test-helpers' as TestHelpers
import 'test-helpers.js' as JsHelpers
import moe.kazv.mxc.kazv 0.0 as MK
Item {
id: upper
width: 800
height: 600
property var l10n: JsHelpers.fluentMock
property var matrixSdk: TestHelpers.MatrixSdkMock {
property var userId: '@foo:example.com'
}
property var mockHelper: TestHelpers.MockHelper {}
property var sdkVars: QtObject {
property var userGivenNicknameMap: QtObject {
id: gnMap
property var map: ({})
property var setAndUpload: mockHelper.promise()
}
}
property var showPassiveNotification: mockHelper.noop()
property var powerLevelMapping: ({
'@mew:example.com': 100,
})
property var room: QtObject {
signal onPowerLevelsChanged()
function userPowerLevel(userId) {
return powerLevelMapping[userId];
}
property var setUserPowerLevel: mockHelper.promise()
property var ensureStateEvent: mockHelper.promise()
property var kickUser: mockHelper.promise()
property var banUser: mockHelper.promise()
property var unbanUser: mockHelper.promise()
}
property var userPage: null
property var userPageComp: Component {
Kazv.UserPage {
userId: '@mew:example.com'
user: ({ userId: '@mew:example.com', name: 'mew' })
room: upper.room
}
}
+ property var userPageWithoutName: null
+ property var userPageWithoutNameComp: Component {
+ Kazv.UserPage {
+ userId: '@mew:example.com'
+ user: ({ userId: '@mew:example.com', name: '' })
+ }
+ }
+
ColumnLayout {
id: layout
}
TestCase {
id: userPageTest
name: 'UserPageTest'
when: windowShown
function init() {
mockHelper.clearAll();
userPage = userPageComp.createObject(layout);
- layout.children = [userPage];
+ userPageWithoutName = userPageWithoutNameComp.createObject(layout);
+ layout.children = [userPage, userPageWithoutName];
if (MK.KazvUtil.kfQtMajorVersion === 6) {
userPage.contentItem.clip = false;
}
}
function cleanup() {
layout.children = [];
userPage.destroy();
userPage = null;
gnMap.map = {};
}
function test_powerLevel() {
verify(findChild(userPage, 'powerLevelLabel').visible);
verify(findChild(userPage, 'powerLevelLabel').text.includes('100'));
verify(!findChild(userPage, 'newPowerLevelInput').visible);
const editButton = findChild(userPage, 'editPowerLevelButton');
verify(editButton.visible);
verify(!editButton.enabled);
tryVerify(() => userPage.room.ensureStateEvent.calledTimes() === 2);
tryVerify(() => userPage.room.ensureStateEvent.args[1][0] === 'm.room.power_levels', 1000);
tryVerify(() => userPage.room.ensureStateEvent.retVals[1], 1000);
userPage.room.ensureStateEvent.retVals[1].resolve(true, {});
tryVerify(() => userPage.powerLevelsLoaded);
tryVerify(() => editButton.enabled);
tryVerify(() => editButton.visible);
mouseClick(editButton);
const textField = findChild(userPage, 'newPowerLevelInput');
tryVerify(() => textField.visible);
textField.text = '50';
const saveButton = findChild(userPage, 'savePowerLevelButton');
verify(saveButton.visible);
mouseClick(saveButton);
tryVerify(() => room.setUserPowerLevel.calledTimes(), 1000);
tryVerify(() => room.setUserPowerLevel.lastRetVal(), 1000);
// Text field is not closed until the request succeeded
verify(textField.visible);
verify(textField.readOnly);
verify(room.setUserPowerLevel.lastArgs()[0] === '@mew:example.com');
verify(room.setUserPowerLevel.lastArgs()[1] === 50);
// the buttons are disabled until the request responded
verify(!saveButton.enabled);
verify(!findChild(userPage, 'discardPowerLevelButton').enabled);
room.setUserPowerLevel.lastRetVal().resolve(true, {});
tryVerify(() => !textField.visible, 1000);
// Text field is editable when we try to edit the power level again
mouseClick(editButton);
tryVerify(() => textField.visible, 1000);
verify(!textField.readOnly);
}
function test_powerLevelEditFailure() {
tryVerify(() => userPage.room.ensureStateEvent.retVals[1], 1000);
userPage.room.ensureStateEvent.retVals[1].resolve(true, {});
const editButton = findChild(userPage, 'editPowerLevelButton');
tryVerify(() => editButton.enabled);
mouseClick(editButton);
const textField = findChild(userPage, 'newPowerLevelInput');
tryVerify(() => textField.visible, 1000);
textField.text = '50';
const saveButton = findChild(userPage, 'savePowerLevelButton');
mouseClick(saveButton);
tryVerify(() => room.setUserPowerLevel.lastRetVal(), 1000);
// Text field is not closed until the request succeeded
room.setUserPowerLevel.lastRetVal().resolve(false, {});
// text field is no longer readonly if failed
tryVerify(() => !textField.readOnly, 1000);
// show error message
verify(upper.showPassiveNotification.calledTimes());
// the buttons are enabled when the request responded
verify(saveButton.enabled);
verify(findChild(userPage, 'discardPowerLevelButton').enabled);
}
function test_banUser() {
// Ban user is only available if membership ! = "ban"
userPage.user = ({ membership: "join", userId: '@mew:example.com' });
const banButton = findChild(userPage, 'banUserButton');
verify(banButton.visible);
verify(banButton.enabled);
const reasonDialog = findChild(userPage, 'banUserReasonDialog');
verify(!reasonDialog.opened);
const textField = findChild(userPage, 'banUserReasonInput');
mouseClick(banButton);
verify(!reasonDialog.opened);
verify(textField.visible);
verify(!textField.readOnly);
textField.text = 'some reason';
reasonDialog.accept();
tryVerify(() => !reasonDialog.opened, 1000);
tryVerify(() => !banButton.enabled, 1000);
tryVerify(() => room.banUser.lastRetVal(), 1000);
verify(room.banUser.lastArgs()[0] === '@mew:example.com');
verify(room.banUser.lastArgs()[1] === 'some reason');
room.banUser.lastRetVal().resolve(true, {});
tryVerify(() => textField.text === '', 1000);
tryVerify(() => banButton.enabled, 1000);
}
function test_banUserFailure() {
// Ban user is only available if membership ! = "ban"
userPage.user = ({ membership: "join" });
const banButton = findChild(userPage, 'banUserButton');
verify(banButton.visible);
verify(banButton.enabled);
const reasonDialog = findChild(userPage, 'banUserReasonDialog');
verify(!reasonDialog.opened);
const textField = findChild(userPage, 'banUserReasonInput');
mouseClick(banButton);
verify(!reasonDialog.opened);
verify(textField.visible);
verify(!textField.readOnly);
textField.text = 'some reason';
reasonDialog.accept();
tryVerify(() => !reasonDialog.opened, 1000);
tryVerify(() => !banButton.enabled, 1000);
tryVerify(() => room.banUser.lastRetVal(), 1000);
verify(room.banUser.lastArgs()[0] === '@mew:example.com');
verify(room.banUser.lastArgs()[1] === 'some reason');
room.banUser.lastRetVal().resolve(false, {});
tryVerify(() => upper.showPassiveNotification.calledTimes(), 1000);
// text field is not cleared when failed
tryVerify(() => textField.text === 'some reason', 1000);
tryVerify(() => banButton.enabled, 1000);
}
function test_unbanUser() {
// Unban user is only available if membership == "ban"
userPage.user = ({ membership: "ban" });
const unbanButton = findChild(userPage, 'unbanUserButton');
verify(unbanButton.visible);
verify(unbanButton.enabled);
mouseClick(unbanButton);
tryVerify(() => !unbanButton.enabled, 1000);
verify(room.unbanUser.lastArgs()[0] == '@mew:example.com');
verify(room.unbanUser.lastRetVal());
room.unbanUser.lastRetVal().resolve(true, {});
tryVerify(() => unbanButton.enabled, 1000);
}
function test_unbanUserFailure() {
// Unban user is only available if membership == "ban"
userPage.user = ({ membership: "ban" });
const unbanButton = findChild(userPage, 'unbanUserButton');
verify(unbanButton.visible);
verify(unbanButton.enabled);
mouseClick(unbanButton);
tryVerify(() => !unbanButton.enabled, 1000);
verify(room.unbanUser.lastArgs()[0] == '@mew:example.com');
verify(room.unbanUser.lastRetVal());
room.unbanUser.lastRetVal().resolve(false, {});
tryVerify(() => upper.showPassiveNotification.calledTimes(), 1000);
tryVerify(() => unbanButton.enabled, 1000);
}
function test_kickUser() {
const kickButton = findChild(userPage, 'kickUserButton');
verify(kickButton.visible);
verify(kickButton.enabled);
const reasonDialog = findChild(userPage, 'kickUserReasonDialog');
verify(!reasonDialog.opened);
const textField = findChild(userPage, 'kickUserReasonInput');
mouseClick(kickButton);
verify(!reasonDialog.opened);
verify(!textField.readOnly);
textField.text = 'some reason';
reasonDialog.accept();
tryVerify(() => !reasonDialog.opened, 1000);
tryVerify(() => !kickButton.enabled, 1000);
tryVerify(() => room.kickUser.lastRetVal(), 1000);
verify(room.kickUser.lastArgs()[0] === '@mew:example.com');
verify(room.kickUser.lastArgs()[1] === 'some reason');
room.kickUser.lastRetVal().resolve(true, {});
tryVerify(() => textField.text === '', 1000);
// still available only because this is a mock call, no real action
// is performed on the user
tryVerify(() => kickButton.enabled, 1000);
}
function test_overridedName() {
const input = findChild(userPage, 'nameOverrideInput');
compare(input.text, '');
gnMap.map = { '@mew:example.com': 'something' };
tryCompare(input, 'text', 'something');
}
function test_noOverridedNameForSelf() {
userPage.userId = '@foo:example.com';
verify(!findChild(userPage, 'nameOverrideInput').visible);
}
function test_updateOverridedName() {
const input = findChild(userPage, 'nameOverrideInput');
compare(input.text, '');
verify(input.enabled);
input.text = 'something';
const saveButton = findChild(userPage, 'saveNameOverrideButton');
verify(saveButton.enabled);
mouseClick(saveButton);
tryVerify(() => !input.enabled);
verify(!saveButton.enabled);
compare(gnMap.setAndUpload.calledTimes(), 1);
verify(JsHelpers.deepEqual(gnMap.setAndUpload.lastArgs(), ['@mew:example.com', 'something']));
gnMap.setAndUpload.lastRetVal().resolve(true, {});
tryVerify(() => input.enabled);
verify(saveButton.enabled);
}
function test_updateOverridedNameFailed() {
const input = findChild(userPage, 'nameOverrideInput');
compare(input.text, '');
verify(input.enabled);
input.text = 'something';
const saveButton = findChild(userPage, 'saveNameOverrideButton');
verify(saveButton.enabled);
mouseClick(saveButton);
tryVerify(() => !input.enabled);
verify(!saveButton.enabled);
compare(gnMap.setAndUpload.calledTimes(), 1);
verify(JsHelpers.deepEqual(gnMap.setAndUpload.lastArgs(), ['@mew:example.com', 'something']));
gnMap.setAndUpload.lastRetVal().resolve(false, {});
tryVerify(() => input.enabled);
verify(saveButton.enabled);
}
function test_removeOverridedName() {
gnMap.map = { '@mew:example.com': 'something' };
const input = findChild(userPage, 'nameOverrideInput');
compare(input.text, 'something');
input.text = '';
const saveButton = findChild(userPage, 'saveNameOverrideButton');
verify(saveButton.enabled);
mouseClick(saveButton);
tryVerify(() => !input.enabled);
verify(!saveButton.enabled);
compare(gnMap.setAndUpload.calledTimes(), 1);
verify(JsHelpers.deepEqual(gnMap.setAndUpload.lastArgs(), ['@mew:example.com', null]));
gnMap.setAndUpload.lastRetVal().resolve(true, {});
tryVerify(() => input.enabled);
verify(saveButton.enabled);
}
+
+ function test_showUserNameAndId() {
+ const nameLabel = findChild(userPage, 'userNameLabel');
+ const idLabel = findChild(userPage, 'userIdLabel');
+ verify(nameLabel.text === 'mew');
+ verify(idLabel.text === '@mew:example.com');
+ verify(idLabel.visible);
+ }
+
+ function test_showUserIdOnly() {
+ const nameLabel = findChild(userPageWithoutName, 'userNameLabel');
+ const idLabel = findChild(userPageWithoutName, 'userIdLabel');
+ verify(nameLabel.text === '@mew:example.com');
+ verify(!idLabel.visible);
+ }
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 11:53 PM (1 d, 7 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
55528
Default Alt Text
(25 KB)

Event Timeline