diff --git a/README.md b/README.md
index a190e14..030b914 100644
--- a/README.md
+++ b/README.md
@@ -99,6 +99,7 @@ The following changes are already implemented:
* 🎨 [Add preferred theme colors to login page and footer](https://github.com/etkecc/synapse-admin/pull/155)
* 🔰 [Add "Assign Admin" button to the rooms](https://github.com/etkecc/synapse-admin/pull/156)
* 🖼️ [Add rooms' avatars](https://github.com/etkecc/synapse-admin/pull/158)
+* 🤖 [User Badges](https://github.com/etkecc/synapse-admin/pull/160)
_the list will be updated as new changes are added_
diff --git a/docs/user-badges.md b/docs/user-badges.md
new file mode 100644
index 0000000..8025535
--- /dev/null
+++ b/docs/user-badges.md
@@ -0,0 +1,36 @@
+# User Badges
+
+To help with identifying users with certain roles or permissions, we have implemented a badge system.
+These badges are displayed on the user's avatar and have a handy tooltip that explains what the badge means.
+
+## Available Badges
+
+### 🧙 You
+
+This badge is displayed on your user's avatar.
+Tooltip for this badge will contain additional information, e.g.: `You (Admin)`.
+
+### 👑 Admin
+
+This badge is displayed on homeserver admins' avatars.
+Tooltip for this badge is `Admin`.
+
+### 🛡️ Appservice/System-managed
+
+This badge is displayed on users that are managed by an appservices (or system), [more details](./system-users.md).
+Tooltip for this badge will contain additional information, e.g.: `System-managed (Bot)`.
+
+### 🤖 Bot
+
+This badge is displayed on bots' avatars (users with the `user_type` set to `bot`).
+Tooltip for this badge is `Bot`.
+
+### 📞 Support
+
+This badge is displayed on users that are part of the support team (users with the `user_type` set to `support`).
+Tooltip for this badge is `Support`.
+
+### 👤 Regular User
+
+This badge is displayed on regular users' avatars.
+Tooltip for this badge is `Regular User`.
diff --git a/src/components/AvatarField.tsx b/src/components/AvatarField.tsx
index 880c84f..a7988b1 100644
--- a/src/components/AvatarField.tsx
+++ b/src/components/AvatarField.tsx
@@ -1,8 +1,10 @@
import { get } from "lodash";
-import { Avatar, AvatarProps } from "@mui/material";
-import { FieldProps, useRecordContext } from "react-admin";
+import { Avatar, AvatarProps, Badge, Tooltip } from "@mui/material";
+import { FieldProps, useRecordContext, useTranslate } from "react-admin";
import { useState, useEffect, useCallback } from "react";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
+import { isMXID, isASManaged } from "./mxid";
+import storage from "../storage";
const AvatarField = ({ source, ...rest }: AvatarProps & FieldProps) => {
const { alt, classes, sizes, sx, variant } = rest;
@@ -45,6 +47,59 @@ const AvatarField = ({ source, ...rest }: AvatarProps & FieldProps) => {
letter = record.displayname[0].toUpperCase();
}
+ // hacky way to determine the user type
+ let badge = "";
+ let tooltip = "";
+ if (isMXID(record?.id)) {
+ const translate = useTranslate();
+ switch (record?.user_type) {
+ case "bot":
+ badge = "🤖";
+ tooltip = translate("resources.users.badge.bot");
+ break;
+ case "support":
+ badge = "📞";
+ tooltip = translate("resources.users.badge.support");
+ break;
+ default:
+ badge = "👤";
+ tooltip = translate("resources.users.badge.regular");
+ break;
+ }
+ if (record?.admin) {
+ badge = "👑";
+ tooltip = translate("resources.users.badge.admin");
+ }
+ if (isASManaged(record?.name)) {
+ badge = "🛡️";
+ tooltip = `${translate("resources.users.badge.system_managed")} (${tooltip})`;
+ }
+ if (storage.getItem("user_id") === record?.id) {
+ badge = "🧙";
+ tooltip = `${translate("resources.users.badge.you")} (${tooltip})`;
+ }
+ }
+
+ // if there is a badge, wrap the Avatar in a Badge and a Tooltip
+ if (badge) {
+ return (
+
+
+
+ {letter}
+
+
+ );
+ }
+
return (
{letter}
);
diff --git a/src/components/mxid.tsx b/src/components/mxid.tsx
index da7dc32..ba850ea 100644
--- a/src/components/mxid.tsx
+++ b/src/components/mxid.tsx
@@ -1,6 +1,15 @@
import { Identifier } from "ra-core";
import { GetConfig } from "./config";
+const mxidPattern = /^@[^@:]+:[^@:]+$/;
+
+/*
+ * Check if id is a valid Matrix ID (user)
+ * @param id The ID to check
+ * @returns Whether the ID is a valid Matrix ID
+ */
+export const isMXID = (id: string | Identifier): boolean => mxidPattern.test(id as string);
+
/**
* Check if a user is managed by an application service
* @param id The user ID to check
diff --git a/src/i18n/de.ts b/src/i18n/de.ts
index 0cd4bd1..eae2070 100644
--- a/src/i18n/de.ts
+++ b/src/i18n/de.ts
@@ -187,6 +187,14 @@ const de: SynapseTranslationMessages = {
modify_managed_user_error: "Das Ändern eines vom System verwalteten Benutzers ist nicht zulässig.",
username_available: "Benutzername verfügbar",
},
+ badge: {
+ you: "Sie",
+ bot: "Bot",
+ admin: "Administrator",
+ support: "Unterstützung",
+ regular: "Normaler Benutzer",
+ system_managed: "Systemverwalteter Benutzer",
+ },
action: {
erase: "Lösche Benutzerdaten",
erase_avatar: "Avatar löschen",
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index 6fa422d..91fba59 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -171,6 +171,14 @@ const en: SynapseTranslationMessages = {
overwrite_cancel: "Cancel",
overwrite_confirm: "Overwrite",
},
+ badge: {
+ you: "You",
+ bot: "Bot",
+ admin: "Admin",
+ support: "Support",
+ regular: "Regular User",
+ system_managed: "System-managed",
+ },
limits: {
messages_per_second: "Messages per second",
messages_per_second_text: "The number of actions that can be performed in a second.",
diff --git a/src/i18n/fa.ts b/src/i18n/fa.ts
index 80ac563..d1d1ccb 100644
--- a/src/i18n/fa.ts
+++ b/src/i18n/fa.ts
@@ -152,6 +152,14 @@ const fa: SynapseTranslationMessages = {
modify_managed_user_error: "لا يُسمح بتغيير المستخدم الذي يديره النظام.",
username_available: "نام کاربری موجود",
},
+ badge: {
+ you: "شما",
+ bot: "ربات",
+ admin: "مدیر",
+ support: "پشتیبان",
+ regular: "کاربر عادی",
+ system_managed: "مدیریت سیستم",
+ },
action: {
erase: "پاک کردن اطلاعات کاربر",
erase_avatar: "محو الصورة الرمزية",
diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts
index 7804aaa..7834aaf 100644
--- a/src/i18n/fr.ts
+++ b/src/i18n/fr.ts
@@ -155,6 +155,14 @@ const fr: SynapseTranslationMessages = {
modify_managed_user_error: "La modification d'un utilisateur géré par le système n'est pas autorisée.",
username_available: "Nom d'utilisateur disponible",
},
+ badge: {
+ you: "Vous",
+ bot: "Bot",
+ admin: "Admin",
+ support: "Support",
+ regular: "Utilisateur régulier",
+ system_managed: "Géré par le système",
+ },
action: {
erase: "Effacer les données de l'utilisateur",
erase_avatar: "Effacer l'avatar",
diff --git a/src/i18n/index.d.ts b/src/i18n/index.d.ts
index 8812e54..f69689a 100644
--- a/src/i18n/index.d.ts
+++ b/src/i18n/index.d.ts
@@ -163,6 +163,14 @@ interface SynapseTranslationMessages extends TranslationMessages {
overwrite_cancel: string;
overwrite_confirm: string;
};
+ badge: {
+ you: string;
+ bot: string;
+ admin: string;
+ support: string;
+ regular: string;
+ system_managed: string;
+ }
limits: {
messages_per_second: string;
messages_per_second_text: string;
diff --git a/src/i18n/it.ts b/src/i18n/it.ts
index 7975e3d..c9ab358 100644
--- a/src/i18n/it.ts
+++ b/src/i18n/it.ts
@@ -153,6 +153,14 @@ const it: SynapseTranslationMessages = {
modify_managed_user_error: "La modifica di un utente gestito dal sistema non è consentita.",
username_available: "Nome utente disponibile",
},
+ badge: {
+ you: "Tu",
+ bot: "Bot",
+ admin: "Amministratore",
+ support: "Supporto",
+ regular: "Utente normale",
+ system_managed: "Gestito dal sistema",
+ },
action: {
erase: "Cancella i dati dell'utente",
erase_avatar: "Cancella l'avatar dell'utente",
diff --git a/src/i18n/ru.ts b/src/i18n/ru.ts
index 3f2d133..04f4d5e 100644
--- a/src/i18n/ru.ts
+++ b/src/i18n/ru.ts
@@ -147,10 +147,10 @@ const ru: SynapseTranslationMessages = {
helper: {
send: "Это API удаляет локальные файлы с вашего собственного сервера, включая локальные миниатюры и копии скачанных файлов. \
Данный API не затрагивает файлы, загруженные во внешние хранилища.",
- },
- },
- resources: {
- users: {
+ },
+ },
+ resources: {
+ users: {
name: "Пользователь |||| Пользователи",
email: "Почта",
msisdn: "Телефон",
@@ -190,6 +190,14 @@ const ru: SynapseTranslationMessages = {
modify_managed_user_error: "Изменение пользователя, управляемого системой, не допускается.",
username_available: "Имя пользователя доступно",
},
+ badge: {
+ you: "Вы",
+ bot: "Бот",
+ admin: "Админ",
+ support: "Поддержка",
+ regular: "Обычный пользователь",
+ system_managed: "Управляемый системой",
+ },
action: {
erase: "Удалить данные пользователя",
erase_avatar: "Удалить аватар",
@@ -208,266 +216,266 @@ const ru: SynapseTranslationMessages = {
burst_count_text: "Количество действий, которые могут быть выполнены до ограничения.",
}
},
- rooms: {
- name: "Комната |||| Комнаты",
- fields: {
- room_id: "ID комнаты",
- name: "Название",
- canonical_alias: "Псевдоним",
- joined_members: "Участники",
- joined_local_members: "Локальные участники",
- joined_local_devices: "Локальные устройства",
- state_events: "События состояния / Сложность",
- version: "Версия",
- is_encrypted: "Зашифровано",
- encryption: "Шифрование",
- federatable: "Федерация",
- public: "Отображается в каталоге комнат",
- creator: "Создатель",
- join_rules: "Правила входа",
- guest_access: "Гостевой доступ",
- history_visibility: "Видимость истории",
- topic: "Тема",
- avatar: "Аватар",
- actions: "Действия",
- },
- helper: {
- forward_extremities:
- "Оконечности — это события-листья в конце ориентированного ациклического графа (DAG) в комнате, т.е. события без дочерних элементов. \
- Чем больше их в комнате, тем больше Synapse работает над разрешением состояния (это дорогостоящая операция). \
- Хотя Synapse старается не допускать существования слишком большого числа таких событий в комнате, из-за ошибок они иногда снова появляются. \
- Если в комнате >10 оконечностей, стоит найти комнату-виновника и попробовать удалить их с помощью SQL-запросов из #1760.",
- },
- enums: {
- join_rules: {
- public: "Для всех",
- knock: "Надо постучать",
- invite: "По приглашению",
- private: "Приватная",
- },
- guest_access: {
- can_join: "Гости могут войти",
- forbidden: "Гости не могут войти",
- },
- history_visibility: {
- invited: "С момента приглашения",
- joined: "С момента входа",
- shared: "С момента открытия доступа",
- world_readable: "Для всех",
- },
- unencrypted: "Без шифрования",
- },
- action: {
- erase: {
- title: "Удалить комнату",
- content:
- "Действительно удалить эту комнату? Это действие будет невозможно отменить. Все сообщения и файлы в комнате будут удалены с сервера!",
- fields: {
- block: "Заблокировать и запретить пользователям присоединяться к комнате",
- },
- success: "Комната/ы успешно удалены",
- failure: "Комната/ы не могут быть удалены.",
- },
- make_admin: {
- assign_admin: "Назначить администратора",
- title: "Назначить администратора комнате %{roomName}",
- confirm: "Назначить администратора",
- content: "Введите полную MXID пользователя, которого нужно назначить администратором.\nПредупреждение: для этого должен быть назначен хотя бы один локальный участник в качестве администратора.",
- success: "Пользователь назначен администратором комнаты.",
- failure: "Пользователь не может быть назначен администратором комнаты. %{errMsg}",
- }
- },
+ rooms: {
+ name: "Комната |||| Комнаты",
+ fields: {
+ room_id: "ID комнаты",
+ name: "Название",
+ canonical_alias: "Псевдоним",
+ joined_members: "Участники",
+ joined_local_members: "Локальные участники",
+ joined_local_devices: "Локальные устройства",
+ state_events: "События состояния / Сложность",
+ version: "Версия",
+ is_encrypted: "Зашифровано",
+ encryption: "Шифрование",
+ federatable: "Федерация",
+ public: "Отображается в каталоге комнат",
+ creator: "Создатель",
+ join_rules: "Правила входа",
+ guest_access: "Гостевой доступ",
+ history_visibility: "Видимость истории",
+ topic: "Тема",
+ avatar: "Аватар",
+ actions: "Действия",
},
- reports: {
- name: "Жалоба |||| Жалобы",
- fields: {
- id: "ID",
- received_ts: "Дата и время жалобы",
- user_id: "Автор жалобы",
- name: "Название комнаты",
- score: "Баллы",
- reason: "Причина",
- event_id: "ID события",
- event_json: {
- origin: "Исходнный сервер",
- origin_server_ts: "Дата и время отправки",
- type: "Тип события",
- content: {
- msgtype: "Тип содержимого",
- body: "Содержимое",
- format: "Формат",
- formatted_body: "Форматированное содержимое",
- algorithm: "Алгоритм",
- url: "Ссылка",
- info: {
- mimetype: "Тип",
- },
- },
- },
- },
- action: {
- erase: {
- title: "Удалить жалобу",
- content: "Действительно удалить жалобу? Это действие будет невозможно отменить.",
- },
- },
+ helper: {
+ forward_extremities:
+ "Оконечности — это события-листья в конце ориентированного ациклического графа (DAG) в комнате, т.е. события без дочерних элементов. \
+ Чем больше их в комнате, тем больше Synapse работает над разрешением состояния (это дорогостоящая операция). \
+ Хотя Synapse старается не допускать существования слишком большого числа таких событий в комнате, из-за ошибок они иногда снова появляются. \
+ Если в комнате >10 оконечностей, стоит найти комнату-виновника и попробовать удалить их с помощью SQL-запросов из #1760.",
},
- connections: {
- name: "Подключения",
- fields: {
- last_seen: "Дата",
- ip: "IP адрес",
- user_agent: "Юзер-агент",
+ enums: {
+ join_rules: {
+ public: "Для всех",
+ knock: "Надо постучать",
+ invite: "По приглашению",
+ private: "Приватная",
},
+ guest_access: {
+ can_join: "Гости могут войти",
+ forbidden: "Гости не могут войти",
+ },
+ history_visibility: {
+ invited: "С момента приглашения",
+ joined: "С момента входа",
+ shared: "С момента открытия доступа",
+ world_readable: "Для всех",
+ },
+ unencrypted: "Без шифрования",
},
- devices: {
- name: "Устройство |||| Устройства",
- fields: {
- device_id: "ID устройства",
- display_name: "Название",
- last_seen_ts: "Дата и время",
- last_seen_ip: "IP адрес",
- },
- action: {
- erase: {
- title: "Удаление %{id}",
- content: 'Действительно удалить устройство "%{name}"?',
- success: "Устройство успешно удалено.",
- failure: "Произошла ошибка.",
- },
- },
- },
- users_media: {
- name: "Файлы",
- fields: {
- media_id: "ID файла",
- media_length: "Размер файла (в байтах)",
- media_type: "Тип",
- upload_name: "Имя файла",
- quarantined_by: "На карантине",
- safe_from_quarantine: "Защитить от карантина",
- created_ts: "Создано",
- last_access_ts: "Последний доступ",
- },
- action: {
- open: "Открыть файл в новом окне",
- },
- },
- protect_media: {
- action: {
- create: "Не защищён, установить защиту",
- delete: "Защищён, снять защиту",
- none: "На карантине",
- send_success: "Статус защиты успешно изменён.",
- send_failure: "Произошла ошибка.",
- },
- },
- quarantine_media: {
- action: {
- name: "Карантин",
- create: "Поместить на карантин",
- delete: "На карантине, снять карантин",
- none: "Защищено от карантина",
- send_success: "Статус карантина успешно изменён.",
- send_failure: "Произошла ошибка.",
- },
- },
- pushers: {
- name: "Пушер |||| Пушеры",
- fields: {
- app: "Приложение",
- app_display_name: "Название приложения",
- app_id: "ID приложения",
- device_display_name: "Название устройства",
- kind: "Вид",
- lang: "Язык",
- profile_tag: "Тег профиля",
- pushkey: "Ключ",
- data: { url: "URL" },
- },
- },
- servernotices: {
- name: "Серверные уведомления",
- send: "Отправить серверные уведомления",
- fields: {
- body: "Сообщение",
- },
- action: {
- send: "Отправить",
- send_success: "Серверное уведомление успешно отправлено.",
- send_failure: "Произошла ошибка.",
- },
- helper: {
- send: 'Отправить серверное уведомление выбранным пользователям. На сервере должна быть активна функция "Server Notices".',
- },
- },
- user_media_statistics: {
- name: "Файлы пользователей",
- fields: {
- media_count: "Количество файлов",
- media_length: "Размер файлов",
- },
- },
- forward_extremities: {
- name: "Оконечности",
- fields: {
- id: "ID события",
- received_ts: "Дата и время",
- depth: "Глубина",
- state_group: "Группа состояния",
- },
- },
- room_state: {
- name: "События состояния",
- fields: {
- type: "Тип",
- content: "Содержимое",
- origin_server_ts: "Дата отправки",
- sender: "Отправитель",
- },
- },
- room_directory: {
- name: "Каталог комнат",
- fields: {
- world_readable: "Гости могут просматривать без входа",
- guest_can_join: "Гости могут войти",
- },
- action: {
- title:
- "Удалить комнату из каталога |||| Удалить %{smart_count} комнаты из каталога |||| Удалить %{smart_count} комнат из каталога",
+ action: {
+ erase: {
+ title: "Удалить комнату",
content:
- "Действительно удалить комнату из каталога? |||| Действительно удалить %{smart_count} комнаты из каталога? |||| Действительно удалить %{smart_count} комнат из каталога?",
- erase: "Удалить из каталога комнат",
- create: "Опубликовать в каталоге комнат",
- send_success: "Комната успешно опубликована.",
- send_failure: "Произошла ошибка.",
+ "Действительно удалить эту комнату? Это действие будет невозможно отменить. Все сообщения и файлы в комнате будут удалены с сервера!",
+ fields: {
+ block: "Заблокировать и запретить пользователям присоединяться к комнате",
+ },
+ success: "Комната/ы успешно удалены",
+ failure: "Комната/ы не могут быть удалены.",
+ },
+ make_admin: {
+ assign_admin: "Назначить администратора",
+ title: "Назначить администратора комнате %{roomName}",
+ confirm: "Назначить администратора",
+ content: "Введите полную MXID пользователя, которого нужно назначить администратором.\nПредупреждение: для этого должен быть назначен хотя бы один локальный участник в качестве администратора.",
+ success: "Пользователь назначен администратором комнаты.",
+ failure: "Пользователь не может быть назначен администратором комнаты. %{errMsg}",
+ }
+ },
+ },
+ reports: {
+ name: "Жалоба |||| Жалобы",
+ fields: {
+ id: "ID",
+ received_ts: "Дата и время жалобы",
+ user_id: "Автор жалобы",
+ name: "Название комнаты",
+ score: "Баллы",
+ reason: "Причина",
+ event_id: "ID события",
+ event_json: {
+ origin: "Исходнный сервер",
+ origin_server_ts: "Дата и время отправки",
+ type: "Тип события",
+ content: {
+ msgtype: "Тип содержимого",
+ body: "Содержимое",
+ format: "Формат",
+ formatted_body: "Форматированное содержимое",
+ algorithm: "Алгоритм",
+ url: "Ссылка",
+ info: {
+ mimetype: "Тип",
+ },
+ },
},
},
- destinations: {
- name: "Федерация",
- fields: {
- destination: "Назначение",
- failure_ts: "Дата и время ошибки",
- retry_last_ts: "Дата и время последней попытки",
- retry_interval: "Интервал между попытками",
- last_successful_stream_ordering: "Последний успешный поток",
- stream_ordering: "Поток",
+ action: {
+ erase: {
+ title: "Удалить жалобу",
+ content: "Действительно удалить жалобу? Это действие будет невозможно отменить.",
},
- action: { reconnect: "Переподключиться" },
},
- registration_tokens: {
- name: "Токены регистрации",
- fields: {
- token: "Токен",
- valid: "Рабочий токен",
- uses_allowed: "Количество использований",
- pending: "Ожидает",
- completed: "Завершено",
- expiry_time: "Дата окончания",
- length: "Длина",
+ },
+ connections: {
+ name: "Подключения",
+ fields: {
+ last_seen: "Дата",
+ ip: "IP адрес",
+ user_agent: "Юзер-агент",
+ },
+ },
+ devices: {
+ name: "Устройство |||| Устройства",
+ fields: {
+ device_id: "ID устройства",
+ display_name: "Название",
+ last_seen_ts: "Дата и время",
+ last_seen_ip: "IP адрес",
+ },
+ action: {
+ erase: {
+ title: "Удаление %{id}",
+ content: 'Действительно удалить устройство "%{name}"?',
+ success: "Устройство успешно удалено.",
+ failure: "Произошла ошибка.",
},
- helper: { length: "Длина токена, если токен не задан." },
},
-},
+ },
+ users_media: {
+ name: "Файлы",
+ fields: {
+ media_id: "ID файла",
+ media_length: "Размер файла (в байтах)",
+ media_type: "Тип",
+ upload_name: "Имя файла",
+ quarantined_by: "На карантине",
+ safe_from_quarantine: "Защитить от карантина",
+ created_ts: "Создано",
+ last_access_ts: "Последний доступ",
+ },
+ action: {
+ open: "Открыть файл в новом окне",
+ },
+ },
+ protect_media: {
+ action: {
+ create: "Не защищён, установить защиту",
+ delete: "Защищён, снять защиту",
+ none: "На карантине",
+ send_success: "Статус защиты успешно изменён.",
+ send_failure: "Произошла ошибка.",
+ },
+ },
+ quarantine_media: {
+ action: {
+ name: "Карантин",
+ create: "Поместить на карантин",
+ delete: "На карантине, снять карантин",
+ none: "Защищено от карантина",
+ send_success: "Статус карантина успешно изменён.",
+ send_failure: "Произошла ошибка.",
+ },
+ },
+ pushers: {
+ name: "Пушер |||| Пушеры",
+ fields: {
+ app: "Приложение",
+ app_display_name: "Название приложения",
+ app_id: "ID приложения",
+ device_display_name: "Название устройства",
+ kind: "Вид",
+ lang: "Язык",
+ profile_tag: "Тег профиля",
+ pushkey: "Ключ",
+ data: { url: "URL" },
+ },
+ },
+ servernotices: {
+ name: "Серверные уведомления",
+ send: "Отправить серверные уведомления",
+ fields: {
+ body: "Сообщение",
+ },
+ action: {
+ send: "Отправить",
+ send_success: "Серверное уведомление успешно отправлено.",
+ send_failure: "Произошла ошибка.",
+ },
+ helper: {
+ send: 'Отправить серверное уведомление выбранным пользователям. На сервере должна быть активна функция "Server Notices".',
+ },
+ },
+ user_media_statistics: {
+ name: "Файлы пользователей",
+ fields: {
+ media_count: "Количество файлов",
+ media_length: "Размер файлов",
+ },
+ },
+ forward_extremities: {
+ name: "Оконечности",
+ fields: {
+ id: "ID события",
+ received_ts: "Дата и время",
+ depth: "Глубина",
+ state_group: "Группа состояния",
+ },
+ },
+ room_state: {
+ name: "События состояния",
+ fields: {
+ type: "Тип",
+ content: "Содержимое",
+ origin_server_ts: "Дата отправки",
+ sender: "Отправитель",
+ },
+ },
+ room_directory: {
+ name: "Каталог комнат",
+ fields: {
+ world_readable: "Гости могут просматривать без входа",
+ guest_can_join: "Гости могут войти",
+ },
+ action: {
+ title:
+ "Удалить комнату из каталога |||| Удалить %{smart_count} комнаты из каталога |||| Удалить %{smart_count} комнат из каталога",
+ content:
+ "Действительно удалить комнату из каталога? |||| Действительно удалить %{smart_count} комнаты из каталога? |||| Действительно удалить %{smart_count} комнат из каталога?",
+ erase: "Удалить из каталога комнат",
+ create: "Опубликовать в каталоге комнат",
+ send_success: "Комната успешно опубликована.",
+ send_failure: "Произошла ошибка.",
+ },
+ },
+ destinations: {
+ name: "Федерация",
+ fields: {
+ destination: "Назначение",
+ failure_ts: "Дата и время ошибки",
+ retry_last_ts: "Дата и время последней попытки",
+ retry_interval: "Интервал между попытками",
+ last_successful_stream_ordering: "Последний успешный поток",
+ stream_ordering: "Поток",
+ },
+ action: { reconnect: "Переподключиться" },
+ },
+ registration_tokens: {
+ name: "Токены регистрации",
+ fields: {
+ token: "Токен",
+ valid: "Рабочий токен",
+ uses_allowed: "Количество использований",
+ pending: "Ожидает",
+ completed: "Завершено",
+ expiry_time: "Дата окончания",
+ length: "Длина",
+ },
+ helper: { length: "Длина токена, если токен не задан." },
+ },
+ },
};
export default ru;
diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts
index 6e29bfc..919654c 100644
--- a/src/i18n/zh.ts
+++ b/src/i18n/zh.ts
@@ -178,6 +178,14 @@ const zh: SynapseTranslationMessages = {
modify_managed_user_error: "不允许修改系统管理的用户。",
username_available: "用户名可用",
},
+ badge: {
+ you: "您",
+ bot: "机器人",
+ admin: "管理员",
+ support: "支持",
+ regular: "普通用户",
+ system_managed: "系统管理",
+ },
action: {
erase: "抹除用户信息",
erase_avatar: "抹掉头像",
diff --git a/src/resources/users.tsx b/src/resources/users.tsx
index 46c8cd5..19c6316 100644
--- a/src/resources/users.tsx
+++ b/src/resources/users.tsx
@@ -320,9 +320,6 @@ const UserTitle = () => {
}
let username = record ? (record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : ""
- if (isASManaged(record?.id)) {
- username += " 🤖";
- }
return (
{translate("resources.users.name", {
diff --git a/src/synapse/synapse.ts b/src/synapse/synapse.ts
index 4fa025a..63e07f3 100644
--- a/src/synapse/synapse.ts
+++ b/src/synapse/synapse.ts
@@ -1,6 +1,7 @@
import { Identifier, fetchUtils } from "react-admin";
import storage from "../storage";
+import { isMXID } from "../components/mxid";
export const splitMxid = mxid => {
const re = /^@(?[a-zA-Z0-9._=\-/]+):(?[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/;
@@ -77,8 +78,8 @@ export function returnMXID(input: string | Identifier): string {
// Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
const mxidPattern = /^@[^@:]+:[^@:]+$/;
- if (typeof input === 'string' && mxidPattern.test(input)) {
- return input; // Already a valid MXID
+ if (isMXID(input)) {
+ return input as string; // Already a valid MXID
}
// If input is not a valid MXID, assume it's a localpart and construct the MXID