diff --git a/src/contents/ui/EventReadIndicator.qml b/src/contents/ui/EventReadIndicator.qml --- a/src/contents/ui/EventReadIndicator.qml +++ b/src/contents/ui/EventReadIndicator.qml @@ -11,6 +11,7 @@ import org.kde.kirigami 2.13 as Kirigami import '.' as Kazv +import 'matrix-helpers.js' as Helpers ItemDelegate { id: readIndicator @@ -35,10 +36,13 @@ Kirigami.Avatar { objectName: `readIndicatorAvatar${index}` property var member: readIndicator.model.at(index) + property var nameProvider: Kazv.UserNameProvider { + user: member + } Layout.preferredWidth: readIndicator.avatarSize Layout.preferredHeight: readIndicator.avatarSize source: member.avatarMxcUri ? matrixSdk.mxcUriToHttp(member.avatarMxcUri) : '' - name: member.name || member.userId + name: nameProvider.name } } @@ -66,6 +70,9 @@ implicitWidth: itemLayout.implicitWidth implicitHeight: itemLayout.implicitHeight property var member: readIndicator.model.at(index) + property var nameProvider: Kazv.UserNameProvider { + user: member + } onPressed: { activateUserPage(member, room); readIndicatorDrawer.close(); @@ -78,11 +85,11 @@ Layout.preferredWidth: readIndicator.avatarSize Layout.preferredHeight: readIndicator.avatarSize source: member.avatarMxcUri ? matrixSdk.mxcUriToHttp(member.avatarMxcUri) : '' - name: member.name || member.userId + name: nameProvider.name } Label { - text: member.name || member.userId + text: nameProvider.name Layout.fillWidth: true } diff --git a/src/contents/ui/Notifier.qml b/src/contents/ui/Notifier.qml --- a/src/contents/ui/Notifier.qml +++ b/src/contents/ui/Notifier.qml @@ -17,6 +17,8 @@ } } + property var nameProvider: Kazv.UserNameProvider {} + property var conn: Connections { target: matrixSdk function onReceivedMessage(roomId, eventId) { @@ -24,7 +26,7 @@ const room = roomList.room(roomId); const event = room.messageById(eventId); const sender = room.member(event.sender); - const senderName = sender.name || sender.userId || ''; + const senderName = nameProvider.getName(sender); if (matrixSdk.shouldNotify(event)) { console.debug('Push rules say we should notify this'); const notification = notificationComp.createObject( diff --git a/src/contents/ui/RoomMemberListViewItemDelegate.qml b/src/contents/ui/RoomMemberListViewItemDelegate.qml --- a/src/contents/ui/RoomMemberListViewItemDelegate.qml +++ b/src/contents/ui/RoomMemberListViewItemDelegate.qml @@ -10,10 +10,15 @@ import org.kde.kirigami 2.13 as Kirigami +import '.' as Kazv + Kirigami.SwipeListItem { id: roomMemberListViewItemDelegate property var member - property var displayName: member.name || member.userId + property var nameProvider: Kazv.UserNameProvider { + user: member + } + property var displayName: nameProvider.name property var iconSize RowLayout { diff --git a/src/contents/ui/TypingIndicator.qml b/src/contents/ui/TypingIndicator.qml --- a/src/contents/ui/TypingIndicator.qml +++ b/src/contents/ui/TypingIndicator.qml @@ -8,18 +8,22 @@ import QtQuick.Layouts 1.15 import QtQuick.Controls 2.15 +import '.' as Kazv +import 'matrix-helpers.js' as Helpers + Label { id: typingIndicator property var typingUsers: null property var count: typingUsers ? typingUsers.count : 0 property var firstTypingUser: typingUsers && typingUsers.at(0) property var secondTypingUser: typingUsers && typingUsers.at(1) + property var nameProvider: Kazv.UserNameProvider {} visible: count text: getText() function getName(user) { - return user ? (user.name || user.userId) : ''; + return nameProvider.getName(user); } function getText() { diff --git a/src/contents/ui/UserNameProvider.qml b/src/contents/ui/UserNameProvider.qml new file mode 100644 --- /dev/null +++ b/src/contents/ui/UserNameProvider.qml @@ -0,0 +1,29 @@ +/* + * This file is part of kazv. + * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 + +import 'matrix-helpers.js' as Helpers + +QtObject { + property var user + property var name: getName(user) + + function getName(u) { + if (!u) { + return ''; + } + + return Helpers.userDisplayName( + u.name, + u.userId, + sdkVars.userGivenNicknameMap.map, + l10n, + ); + } +} diff --git a/src/contents/ui/UserPage.qml b/src/contents/ui/UserPage.qml --- a/src/contents/ui/UserPage.qml +++ b/src/contents/ui/UserPage.qml @@ -20,6 +20,9 @@ property string userId: '' property var user: ({}) + property var nameProvider: Kazv.UserNameProvider { + user: userPage.user + } property var room property var roomName: Helpers.roomNameOrHeroes(room, l10n) property var editingPowerLevel: false @@ -38,7 +41,7 @@ } } - title: user.name || userId + title: nameProvider.name property var ensureMemberEvent: Kazv.AsyncHandler { trigger: () => room.ensureStateEvent('m.room.member', userId) @@ -126,6 +129,7 @@ source: userPage.user.avatarMxcUri ? matrixSdk.mxcUriToHttp(userPage.user.avatarMxcUri) : '' sourceSize.width: Kirigami.Units.iconSizes.enormous sourceSize.height: Kirigami.Units.iconSizes.enormous + name: nameProvider.name } RowLayout { @@ -191,7 +195,7 @@ Layout.fillWidth: true text: l10n.get('user-page-kick-user-confirm-dialog-content', { userId: userPage.user.userId, - name: userPage.user.name, + name: nameProvider.name, roomName: userPage.roomName, }) wrapMode: Text.Wrap @@ -237,7 +241,7 @@ Layout.fillWidth: true text: l10n.get('user-page-ban-user-confirm-dialog-content', { userId: userPage.user.userId, - name: userPage.user.name, + name: nameProvider.name, roomName: userPage.roomName, }) wrapMode: Text.Wrap diff --git a/src/contents/ui/event-types/Simple.qml b/src/contents/ui/event-types/Simple.qml --- a/src/contents/ui/event-types/Simple.qml +++ b/src/contents/ui/event-types/Simple.qml @@ -1,6 +1,6 @@ /* * This file is part of kazv. - * SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe> + * SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe> * SPDX-License-Identifier: AGPL-3.0-or-later */ @@ -10,12 +10,19 @@ import org.kde.kirigami 2.13 as Kirigami +import '..' as Kazv + ColumnLayout { property var event property var sender default property var children + property var nameProvider: Kazv.UserNameProvider { + user: sender + } + property var senderNickname: nameProvider.name + property var contentMaxWidth: { (parent.width // avatar size and margins @@ -52,7 +59,7 @@ sourceSize.width: iconSize sourceSize.height: iconSize source: sender.avatarMxcUri ? matrixSdk.mxcUriToHttp(sender.avatarMxcUri) : '' - name: sender.name || sender.userId + name: senderNickname visible: !compactMode && !shouldCollapseSender TapHandler { @@ -82,13 +89,14 @@ sourceSize.width: Kirigami.Units.iconSizes.sizeForLabels sourceSize.height: Kirigami.Units.iconSizes.sizeForLabels source: sender.avatarMxcUri ? matrixSdk.mxcUriToHttp(sender.avatarMxcUri) : '' - name: sender.name || sender.userId + name: senderNickname visible: compactMode } Label { id: userNicknameText - text: sender.name || sender.userId + objectName: 'userNicknameText' + text: senderNickname Layout.fillWidth: true property var reasonableWidth: Math.max(contentWidth, Kirigami.Units.gridUnit) diff --git a/src/contents/ui/main.qml b/src/contents/ui/main.qml --- a/src/contents/ui/main.qml +++ b/src/contents/ui/main.qml @@ -44,6 +44,7 @@ property var sdkVars: QtObject { property var roomList: matrixSdk.roomList() + property var userGivenNicknameMap: matrixSdk.userGivenNicknameMap() property string currentRoomId: '' } @@ -251,6 +252,7 @@ function reloadSdkVariables() { sdkVars.roomList = matrixSdk.roomList(); + sdkVars.userGivenNicknameMap = matrixSdk.userGivenNicknameMap(); sdkVars.currentRoomId = ''; } diff --git a/src/js/matrix-helpers.js b/src/js/matrix-helpers.js --- a/src/js/matrix-helpers.js +++ b/src/js/matrix-helpers.js @@ -97,3 +97,23 @@ } return event.content.body; } + +/** + * Get the display name for a user, possibly overrided. + * + * @param name The user's global display name. + * @param userId The user's id. + * @param overrides A map from user id to overrided names. + * @param l10n The fluent object. + * @return A calculated display name for the user. + */ +function userDisplayName(name, userId, overrides = {}, l10n) +{ + const overridedName = overrides[userId]; + const globalName = name || userId; + if (overridedName) { + return l10n.get('user-name-overrided', { overridedName, globalName }); + } else { + return globalName; + } +} diff --git a/src/l10n/cmn-Hans/100-ui.ftl b/src/l10n/cmn-Hans/100-ui.ftl --- a/src/l10n/cmn-Hans/100-ui.ftl +++ b/src/l10n/cmn-Hans/100-ui.ftl @@ -5,6 +5,7 @@ app-title = { -kt-app-name } user-name-with-id = { $name } ({ $userId }) +user-name-overrided = { $overridedName } ({ $globalName }) global-drawer-title = { -kt-app-name } global-drawer-action-switch-account = 切换账号 diff --git a/src/l10n/en/100-ui.ftl b/src/l10n/en/100-ui.ftl --- a/src/l10n/en/100-ui.ftl +++ b/src/l10n/en/100-ui.ftl @@ -5,6 +5,7 @@ app-title = { -kt-app-name } user-name-with-id = { $name } ({ $userId }) +user-name-overrided = { $overridedName } ({ $globalName }) global-drawer-title = { -kt-app-name } global-drawer-action-switch-account = Switch account diff --git a/src/matrix-sdk.hpp b/src/matrix-sdk.hpp --- a/src/matrix-sdk.hpp +++ b/src/matrix-sdk.hpp @@ -28,6 +28,7 @@ class MatrixSdkTest; class MatrixEvent; class MatrixStickerPackList; +class MatrixUserGivenAttrsMap; struct MatrixSdkPrivate; @@ -221,6 +222,8 @@ */ MatrixPromise *updateStickerPack(MatrixStickerPackSource source); + MatrixUserGivenAttrsMap *userGivenNicknameMap(); + private: MatrixPromise *sendAccountDataImpl(Kazv::Event event); diff --git a/src/matrix-sdk.cpp b/src/matrix-sdk.cpp --- a/src/matrix-sdk.cpp +++ b/src/matrix-sdk.cpp @@ -41,10 +41,15 @@ #include "qt-job-handler.hpp" #include "device-mgmt/matrix-device-list.hpp" #include "matrix-sticker-pack-list.hpp" +#include "matrix-user-given-attrs-map.hpp" #include "kazv-log.hpp" using namespace Kazv; +static const auto USER_GIVEN_NICKNAME_EVENT_TYPES = immer::array<std::string>{ + "work.banananet.msc3865.user_given.user.displayname" +}; + // Sdk with qt event loop, identity transform and no enhancers using SdkT = decltype(makeSdk( @@ -628,6 +633,27 @@ } } +MatrixUserGivenAttrsMap *MatrixSdk::userGivenNicknameMap() +{ + return new MatrixUserGivenAttrsMap( + m_d->clientOnSecondaryRoot.accountData() + .map([](const auto &accountData) { + for (const auto &type : USER_GIVEN_NICKNAME_EVENT_TYPES) { + if (accountData.count(type) > 0) { + return accountData[type]; + } + } + return Event(); + }), + [client=m_d->clientOnSecondaryRoot](json content) { + return client.setAccountData(json{ + {"type", USER_GIVEN_NICKNAME_EVENT_TYPES[0]}, + {"content", std::move(content)}, + }); + } + ); +} + MatrixPromise *MatrixSdk::sendAccountDataImpl(Event event) { return new MatrixPromise(m_d->clientOnSecondaryRoot.setAccountData(event)); diff --git a/src/resources.qrc b/src/resources.qrc --- a/src/resources.qrc +++ b/src/resources.qrc @@ -48,6 +48,7 @@ <file alias="JoinRoomPage.qml">contents/ui/JoinRoomPage.qml</file> <file alias="RoomMemberListView.qml">contents/ui/RoomMemberListView.qml</file> <file alias="RoomMemberListViewItemDelegate.qml">contents/ui/RoomMemberListViewItemDelegate.qml</file> + <file alias="UserNameProvider.qml">contents/ui/UserNameProvider.qml</file> <file alias="AsyncHandler.qml">contents/ui/AsyncHandler.qml</file> <file alias="UploadFileHelper.qml">contents/ui/UploadFileHelper.qml</file> diff --git a/src/tests/quick-tests/tst_EventReadIndicator.qml b/src/tests/quick-tests/tst_EventReadIndicator.qml --- a/src/tests/quick-tests/tst_EventReadIndicator.qml +++ b/src/tests/quick-tests/tst_EventReadIndicator.qml @@ -19,6 +19,11 @@ property var l10n: Helpers.fluentMock property var matrixSdk: TestHelpers.MatrixSdkMock {} + property var sdkVars: QtObject { + property var userGivenNicknameMap: QtObject { + property var map: ({}) + } + } property var kazvIOManager: TestHelpers.KazvIOManagerMock {} function makeModel(size) { diff --git a/src/tests/quick-tests/tst_EventView.qml b/src/tests/quick-tests/tst_EventView.qml --- a/src/tests/quick-tests/tst_EventView.qml +++ b/src/tests/quick-tests/tst_EventView.qml @@ -61,6 +61,13 @@ property var matrixSdk: TestHelpers.MatrixSdkMock { property var userId: '@foo:tusooa.xyz' } + property var sdkVars: QtObject { + property var userGivenNicknameMap: QtObject { + property var map: ({ + '@foo:tusooa.xyz': 'something', + }) + } + } property var kazvIOManager: TestHelpers.KazvIOManagerMock {} property var localEcho: ({ eventId: '', @@ -490,5 +497,17 @@ item.room.test_ensureStateEventPromise.resolve(true, {}); tryVerify(() => item.activateUserPageCalled); } + + function test_senderNameOverrided() { + const name = findChild(eventViewText, 'userNicknameText'); + compare(name.text, l10n.get( + 'user-name-overrided', + { overridedName: 'something', globalName: 'foo' })); + } + + function test_senderNameNotOverrided() { + const name = findChild(eventViewTextBySomeoneElse, 'userNicknameText'); + compare(name.text, 'bar'); + } } } diff --git a/src/tests/quick-tests/tst_TypingIndicator.qml b/src/tests/quick-tests/tst_TypingIndicator.qml --- a/src/tests/quick-tests/tst_TypingIndicator.qml +++ b/src/tests/quick-tests/tst_TypingIndicator.qml @@ -42,6 +42,12 @@ property var l10n: Helpers.fluentMock + property var sdkVars: QtObject { + property var userGivenNicknameMap: QtObject { + property var map: ({}) + } + } + ColumnLayout { Kazv.TypingIndicator { id: ind0 diff --git a/src/tests/quick-tests/tst_UserNameProvider.qml b/src/tests/quick-tests/tst_UserNameProvider.qml new file mode 100644 --- /dev/null +++ b/src/tests/quick-tests/tst_UserNameProvider.qml @@ -0,0 +1,101 @@ +/* + * 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 Helpers + +TestCase { + id: userNameProviderTest + name: 'UserNameProviderTest' + when: true + + property var sdkVars: QtObject { + property var userGivenNicknameMap: QtObject { + property var map: ({}) + } + } + property var l10n: Helpers.fluentMock + + property var userOverrided: ({ + name: 'foo', + userId: '@foo:tusooa.xyz' + }) + + property var userNotOverrided: ({ + name: 'bar', + userId: '@bar:tusooa.xyz' + }) + + property var userIdOnly: ({ userId: '@bar:tusooa.xyz' }) + + property var providerFunctional: Kazv.UserNameProvider {} + property var providerReactive: Kazv.UserNameProvider {} + property var userComp: Component { + QtObject { + property var name: 'bar' + property var userId: '@bar:tusooa.xyz' + } + } + + function init() { + sdkVars.userGivenNicknameMap.map = { + '@foo:tusooa.xyz': 'something', + }; + } + + function test_functional() { + compare( + providerFunctional.getName(userOverrided), + l10n.get( + 'user-name-overrided', { overridedName: 'something', globalName: 'foo' })); + compare( + providerFunctional.getName(userNotOverrided), + 'bar' + ); + + compare( + providerFunctional.getName(userIdOnly), + '@bar:tusooa.xyz' + ); + } + + function test_reactiveAgainstUser() { + providerReactive.user = userComp.createObject(userNameProviderTest); + compare(providerReactive.name, 'bar'); + providerReactive.user.userId = '@foo:tusooa.xyz'; + compare( + providerReactive.name, + l10n.get( + 'user-name-overrided', { overridedName: 'something', globalName: 'bar' })); + + providerReactive.user = userComp.createObject(userNameProviderTest, { + userId: '@foo:tusooa.xyz', + name: 'foo', + }); + compare( + providerReactive.name, + l10n.get( + 'user-name-overrided', { overridedName: 'something', globalName: 'foo' })); + } + + function test_reactiveAgainstOverrides() { + providerReactive.user = userComp.createObject(userNameProviderTest); + compare(providerReactive.name, 'bar'); + sdkVars.userGivenNicknameMap.map = { + '@bar:tusooa.xyz': 'something', + }; + + compare( + providerReactive.name, + l10n.get( + 'user-name-overrided', { overridedName: 'something', globalName: 'bar' })); + } +}