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' }));
+  }
+}