From 05ffa2e5ba47eebc157ad8b2b19154347062ea02 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 10 Jun 2025 13:17:49 +0000 Subject: [PATCH 01/12] Upgrade dependency to matrix-js-sdk@37.9.0-rc.0 --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index db4dea80d1..c91983ad0b 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "maplibre-gl": "^5.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "37.9.0-rc.0", "matrix-widget-api": "^1.10.0", "memoize-one": "^6.0.0", "mime": "^4.0.4", diff --git a/yarn.lock b/yarn.lock index 2c5b901548..069a5ebd52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9219,9 +9219,10 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "37.7.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c387f30e5c23c897877dd418545a306e6b8cf0c9" +matrix-js-sdk@37.9.0-rc.0: + version "37.9.0-rc.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-37.9.0-rc.0.tgz#7a0b536abe110096ed3d86f05a33cae0d4258fe4" + integrity sha512-LHezGwAJwABI5IYkwinBqnte8yosVGTa8kPQBmnrhioDP9EZQtQuGtjhHXMR+oWo257UbjhWKRVDJijhDQuxNA== dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm" "^14.2.0" From 67bd11c904d1710be700d99109d2dc3307f9557a Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 10 Jun 2025 13:23:03 +0000 Subject: [PATCH 02/12] v1.11.104-rc.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c91983ad0b..a433f22aea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.103", + "version": "1.11.104-rc.0", "description": "Element: the future of secure communication", "author": "New Vector Ltd.", "repository": { From 6f0d288c1dba449c4b3371849f9fdd3113987bb1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 12 Jun 2025 16:42:30 +0100 Subject: [PATCH 03/12] Use nav for new room list and label sections (#30134) * Use nav for new room list and label sections The old room list had a nav element but it was missed in the new one, so add it and albel the sections. Also remove the test ID and use this instead. * Update snapshots * Use the function we define above --- .../left-panel/room-list-panel/room-list-filter-sort.spec.ts | 4 +++- .../e2e/left-panel/room-list-panel/room-list-panel.spec.ts | 4 ++-- playwright/e2e/left-panel/room-list-panel/room-list.spec.ts | 2 +- src/components/structures/RoomView.tsx | 2 +- src/components/views/rooms/RoomListPanel/RoomListPanel.tsx | 5 +++-- src/i18n/strings/en_EN.json | 1 + .../structures/__snapshots__/RoomView-test.tsx.snap | 3 +++ 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts index 7f87f3f43c..26d27cc01c 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts @@ -326,7 +326,9 @@ test.describe("Room list filters and sort", () => { async ({ page, app, user }) => { const emptyRoomList = getEmptyRoomList(page); await expect(emptyRoomList).toMatchScreenshot("default-empty-room-list.png"); - await expect(page.getByTestId("room-list-panel")).toMatchScreenshot("room-panel-empty-room-list.png"); + await expect(page.getByRole("navigation", { name: "Room list" })).toMatchScreenshot( + "room-panel-empty-room-list.png", + ); }, ); diff --git a/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts index 8ca138a707..d0503e2caf 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts @@ -19,7 +19,7 @@ test.describe("Room list panel", () => { * @param page */ function getRoomListView(page: Page) { - return page.getByTestId("room-list-panel"); + return page.getByRole("navigation", { name: "Room list" }); } test.beforeEach(async ({ page, app, user }) => { @@ -44,7 +44,7 @@ test.describe("Room list panel", () => { test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page }) => { await page.setViewportSize({ width: 575, height: 600 }); - const roomListPanel = page.getByTestId("room-list-panel"); + const roomListPanel = getRoomListView(page); await expect(roomListPanel).toMatchScreenshot("room-list-panel-smallscreen.png"); }); }); diff --git a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts index 73eb98512b..0567b8a162 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts @@ -278,7 +278,7 @@ test.describe("Room list", () => { }); test("should be a video room", { tag: "@screenshot" }, async ({ page, app, user }) => { - await page.getByTestId("room-list-panel").getByRole("button", { name: "Add" }).click(); + await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click(); await page.getByRole("menuitem", { name: "New video room" }).click(); await page.getByRole("textbox", { name: "Name" }).fill("video room"); await page.getByRole("button", { name: "Create video room" }).click(); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index d64f4b7a36..b6f8bc06b3 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -315,7 +315,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
-
+
diff --git a/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx b/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx index 291794399f..f5c0620a66 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx @@ -13,6 +13,7 @@ import { RoomListSearch } from "./RoomListSearch"; import { RoomListHeaderView } from "./RoomListHeaderView"; import { RoomListView } from "./RoomListView"; import { Flex } from "../../../utils/Flex"; +import { _t } from "../../../../languageHandler"; type RoomListPanelProps = { /** @@ -30,11 +31,11 @@ export const RoomListPanel: React.FC = ({ activeSpace }) => return ( {displayRoomSearch && } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5763e0a781..56f22dc6d7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2066,6 +2066,7 @@ "read_topic": "Click to read topic", "rejecting": "Rejecting invite…", "rejoin_button": "Re-join", + "room_content": "Room content", "room_is_low_priority": "This is a low priority room", "search": { "all_rooms_button": "Search all rooms", diff --git a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap index 8d909a6797..f194d0bb33 100644 --- a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -403,6 +403,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
Date: Fri, 13 Jun 2025 01:22:53 -0500 Subject: [PATCH 04/12] [create-pull-request] automated change (#30139) Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com> --- src/i18n/strings/cs.json | 1 + src/i18n/strings/et.json | 3 +++ src/i18n/strings/id.json | 35 ++++++++++++++++++++++++++++++++++- src/i18n/strings/nb_NO.json | 10 ++++++++-- src/i18n/strings/sv.json | 3 ++- 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 33868e7682..d736aa3d6f 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -2122,6 +2122,7 @@ "filters": { "favourite": "Oblíbené", "invites": "Pozvánky", + "low_priority": "Nízká priorita", "mentions": "Zmínky", "people": "Lidé", "rooms": "Místnosti", diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 9a27318316..24f5661733 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -2115,6 +2115,7 @@ "add_space_label": "Lisa kogukonnakeskus", "breadcrumbs_empty": "Hiljuti külastatud jututubasid ei leidu", "breadcrumbs_label": "Hiljuti külastatud jututoad", + "collapse_filters": "Ahenda filtriloendit", "empty": { "no_chats": "Vestlusi veel ei leidu", "no_chats_description": "Alusta sellest, et leia mõni vestluspartner või loo oma jututuba", @@ -2122,6 +2123,7 @@ "no_favourites": "Sa pole veel ühtegi vestlust märkinud lemmikuks", "no_favourites_description": "Vestluse saad märkida lemmikuks tema seadistustest", "no_invites": "Sul pole lugemata kutseid", + "no_lowpriority": "Sul pole ühtegi vähetähtsat jututuba", "no_mentions": "Sul pole lugemata mainimisi", "no_people": "Sul pole veel ühtegi otsevestlust kellegagi", "no_people_description": "Kõikide muude vestluste nägemiseks eemalda otsingufiltrid", @@ -2131,6 +2133,7 @@ "show_activity": "Vaata kõiki tegevusi", "show_chats": "Näita kõiki vestlusi" }, + "expand_filters": "Laienda filtriloendit", "failed_add_tag": "Sildi %(tagName)s lisamine jututoale ebaõnnestus", "failed_remove_tag": "Sildi %(tagName)s eemaldamine jututoast ebaõnnestus", "failed_set_dm_tag": "Otsevestluse sildi seadmine ei õnnestunud", diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index 9f7c8b10f8..331a1a597e 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -784,6 +784,7 @@ "cross_signing_status": "Status penandatanganan silang:", "cross_signing_untrusted": "Akun Anda memiliki identitas penandatanganan silang dalam penyimpanan rahasia, tetapi belum dipercaya oleh sesi ini.", "crypto_not_available": "Modul kriptografi tidak tersedia", + "device_id": "ID Perangkat", "key_backup_active_version": "Versi cadangan aktif:", "key_backup_active_version_none": "Tidak ada", "key_backup_inactive_warning": "Kunci Anda tidak dicadangkan dari sesi ini.", @@ -796,6 +797,8 @@ "secret_storage_ready": "siap", "secret_storage_status": "Penyimpanan rahasia:", "self_signing_private_key_cached_status": "Kunci pribadi penandatanganan sendiri:", + "session": "Sesi", + "session_fingerprint": "Sidik jari (kunci sesi)", "title": "Enkripsi ujung ke ujung", "user_signing_private_key_cached_status": "Kunci pribadi penandatanganan pengguna:" }, @@ -821,6 +824,7 @@ "low_bandwidth_mode": "Mode bandwidth rendah", "low_bandwidth_mode_description": "Membutuhkan homeserver yang kompatibel.", "main_timeline": "Lini masa utama", + "manual_device_verification": "Verifikasi perangkat manual", "no_receipt_found": "Tidak ada laporan yang ditemukan", "notification_state": "Keadaan notifikasi adalah %(notificationState)s", "notifications_debug": "Pengawakutuan notifikasi", @@ -1004,6 +1008,21 @@ "incoming_sas_dialog_waiting": "Menunggu pengguna untuk konfirmasi…", "incoming_sas_user_dialog_text_1": "Verifikasi pengguna ini untuk menandainya sebagai terpercaya. Mempercayai pengguna memberikan Anda ketenangan saat menggunakan pesan terenkripsi secara ujung ke ujung.", "incoming_sas_user_dialog_text_2": "Memverifikasi pengguna ini akan menandai sesinya sebagai terpercaya, dan juga menandai sesi Anda sebagai terpercaya kepadanya.", + "manual": { + "already_verified": "Perangkat ini sudah diverifikasi", + "already_verified_and_wrong_fingerprint": "Sidik jari yang disediakan tidak cocok, tetapi perangkat sudah diverifikasi!", + "device_id": "ID Perangkat", + "failure_description": "Gagal memverifikasi '%(deviceId)s': %(error)s", + "failure_title": "Verifikasi gagal", + "fingerprint": "Sidik jari (kunci sesi)", + "no_crypto": "Tidak dapat memverifikasi perangkat - kripto tidak diaktifkan", + "no_device": "Tidak dapat memverifikasi perangkat - perangkat %(deviceId)s '' tidak ditemukan", + "no_userid": "Tidak dapat memverifikasi perangkat - tidak dapat menemukan ID Pengguna kami", + "success_description": "Perangkat (%(deviceId)s) sekarang ditandatangani silang", + "success_title": "Verifikasi berhasil", + "text": "Berikan ID dan sidik jari salah satu perangkat Anda untuk memverifikasinya. PERHATIKAN bahwa ini memungkinkan perangkat lain untuk mengirim dan menerima pesan seperti Anda. JIKA SESEORANG MEMINTA ANDA UNTUK MENEMPELKAN SESUATU DI SINI, KEMUNGKINAN ANDA SEDANG DITIPU!", + "wrong_fingerprint": "Tidak dapat memverifikasi perangkat '%(deviceId)s' - sidik jari yang disediakan '%(fingerprint)s' tidak cocok dengan sidik jari perangkat, '%(fprint)s'" + }, "no_key_or_device": "Sepertinya Anda tidak memiliki Kunci Pemulihan atau perangkat lain yang dapat Anda verifikasi. Perangkat ini tidak akan dapat mengakses pesan terenkripsi lama. Untuk memverifikasi identitas Anda di perangkat ini, Anda harus mengatur ulang kunci verifikasi Anda.", "no_support_qr_emoji": "Perangkat yang Anda sedang verifikasi tidak mendukung pemindaian kode QR atau verifikasi emoji, yang didukung oleh %(brand)s. Coba menggunakan klien yang lain.", "other_party_cancelled": "Pengguna yang lain membatalkan proses verifikasi ini.", @@ -1949,6 +1968,7 @@ }, "face_pile_tooltip_shortcut": "Termasuk %(commaSeparatedMembers)s", "face_pile_tooltip_shortcut_joined": "Termasuk Anda, %(commaSeparatedMembers)s", + "failed_determine_user": "Tidak dapat menentukan pengguna mana yang akan diabaikan karena peristiwa anggota telah berubah.", "failed_reject_invite": "Gagal untuk menolak undangan", "forget_room": "Lupakan ruangan ini", "forget_space": "Lupakan space ini", @@ -2039,6 +2059,7 @@ "read_topic": "Klik untuk membaca topik", "rejecting": "Menolak undangan…", "rejoin_button": "Bergabung Ulang", + "room_is_low_priority": "Ini adalah ruangan dengan prioritas rendah", "search": { "all_rooms_button": "Cari semua ruangan", "placeholder": "Cari pesan...", @@ -2086,6 +2107,7 @@ "add_space_label": "Tambahkan space", "breadcrumbs_empty": "Tidak ada ruangan yang baru saja dilihat", "breadcrumbs_label": "Ruangan yang baru saja dilihat", + "collapse_filters": "Tutup daftar filter", "empty": { "no_chats": "Belum ada obrolan", "no_chats_description": "Mulailah dengan mengirim pesan kepada seseorang atau dengan membuat ruangan", @@ -2093,6 +2115,7 @@ "no_favourites": "Anda belum memiliki obrolan favorit", "no_favourites_description": "Anda dapat menambahkan obrolan ke favorit Anda di pengaturan obrolan", "no_invites": "Anda tidak memiliki undangan yang belum dibaca", + "no_lowpriority": "Anda tidak memiliki ruangan dengan prioritas rendah", "no_mentions": "Anda tidak memiliki sebutan yang belum dibaca", "no_people": "Anda belum memiliki obrolan langsung dengan siapa pun", "no_people_description": "Anda dapat membatalkan pilihan saringan untuk melihat percakapan Anda yang lain", @@ -2102,12 +2125,14 @@ "show_activity": "Lihat semua aktivitas", "show_chats": "Tampilkan semua obrolan" }, + "expand_filters": "Buka daftar filter", "failed_add_tag": "Gagal menambahkan tag %(tagName)s ke ruangan", "failed_remove_tag": "Gagal menghapus tanda %(tagName)s dari ruangan", "failed_set_dm_tag": "Gagal menetapkan tanda pesan langsung", "filters": { "favourite": "Favorit", "invites": "Undangan", + "low_priority": "Prioritas rendah", "mentions": "Sebutan", "people": "Orang", "rooms": "Ruangan", @@ -2675,6 +2700,9 @@ "inline_url_previews_room": "Aktifkan tampilan URL secara bawaan untuk anggota di ruangan ini", "inline_url_previews_room_account": "Aktifkan tampilan URL secara bawaan (hanya memengaruhi Anda)", "insert_trailing_colon_mentions": "Tambahkan sebuah karakter titik dua sesudah sebutan pengguna dari awal pesan", + "invite_controls": { + "default_label": "Izinkan pengguna mengundang Anda ke ruangan" + }, "jump_to_bottom_on_send": "Pergi ke bawah lini masa ketika Anda mengirim pesan", "key_backup": { "backup_in_progress": "Kunci Anda sedang dicadangkan (cadangan pertama mungkin membutuhkan beberapa menit).", @@ -2741,6 +2769,7 @@ "show_in_private": "Di ruangan privat", "show_media": "Selalu tampilkan" }, + "not_supported": "Server Anda tidak menerapkan fitur ini.", "notifications": { "default_setting_description": "Pengaturan ini akan diterapkan secara bawaan ke semua ruangan Anda.", "default_setting_section": "Saya ingin diberi tahu (Pengaturan Bawaan)", @@ -2798,6 +2827,7 @@ "voip": "Panggilan Audio dan Video" }, "preferences": { + "Electron.enableContentProtection": "Mencegah konten jendela agar tidak ditangkap oleh aplikasi lain", "Electron.enableHardwareAcceleration": "Aktifkan akselerasi perangkat keras (mulai ulang %(appName)s untuk menerapkan)", "always_show_menu_bar": "Selalu tampilkan bilah menu window", "autocomplete_delay": "Delay penyelesaian otomatis (md)", @@ -2970,6 +3000,7 @@ "show_chat_effects": "Tampilkan efek (animasi ketika menerima konfeti, misalnya)", "show_displayname_changes": "Tampilkan perubahan nama tampilan", "show_join_leave": "Tampilkan pesan-pesan gabung/keluar (undangan/pengeluaran/cekalan tidak terpengaruh)", + "show_message_previews": "Tampilkan pratinjau pesan", "show_nsfw_content": "Tampilkan konten NSFW", "show_read_receipts": "Tampilkan laporan dibaca terkirim oleh pengguna lain", "show_redaction_placeholder": "Tampilkan sebuah penampung untuk pesan terhapus", @@ -3076,6 +3107,8 @@ "jumptodate": "Pergi ke tanggal yang diberikan di lini masa", "jumptodate_invalid_input": "Kami tidak dapat mengerti tanggal yang dicantumkan (%(inputDate)s). Coba menggunakan format TTTT-BB-HH.", "lenny": "Menambahkan ( ͡° ͜ʖ ͡°) ke pesan teks biasa", + "manual_device_verification_confirm_description": "Ini akan memungkinkan perangkat lain untuk mengirim dan menerima pesan seperti Anda. JIKA SESEORANG MENYURUH ANDA MENEMPELKAN SESUATU DI SINI, KEMUNGKINAN ANDA SEDANG DITIPU! Apakah Anda yakin ingin memverifikasi perangkat lain ini?", + "manual_device_verification_confirm_title": "Perhatian: verifikasi perangkat manual", "me": "Menampilkan aksi", "msg": "Mengirim sebuah pesan ke pengguna yang dicantumkan", "myavatar": "Ubah foto profil Anda dalam semua ruangan", @@ -3116,7 +3149,7 @@ "upgraderoom": "Meningkatkan ruangan ke versi yang baru", "upgraderoom_permission_error": "Anda tidak memiliki izin yang dibutuhkan untuk menggunakan perintah ini.", "usage": "Penggunaan", - "verify": "Memverifikasi sebuah pengguna, sesi, dan tupel pubkey", + "verify": "Verifikasi salah satu perangkat Anda secara manual", "view": "Menampilkan ruangan dengan alamat yang ditentukan", "whois": "Menampilkan informasi tentang sebuah pengguna" }, diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json index 799e67b4a6..e0f08b7ff8 100644 --- a/src/i18n/strings/nb_NO.json +++ b/src/i18n/strings/nb_NO.json @@ -1020,7 +1020,10 @@ "fingerprint": "Fingeravtrykk (sesjonsnøkkel)", "no_crypto": "Kan ikke verifisere enheten - krypto er ikke aktivert", "no_device": "Kunne ikke verifisere enheten - enheten '%(deviceId)s' ble ikke funnet", - "no_userid": "Kan ikke verifisere enheten - finner ikke bruker-ID" + "no_userid": "Kan ikke verifisere enheten - finner ikke bruker-ID", + "success_description": "Enheten (%(deviceId)s) er nå krysssignert", + "success_title": "Verifiseringen var vellykket", + "wrong_fingerprint": "Kan ikke verifisere enheten %(deviceId)s '- det medfølgende fingeravtrykket'%(fingerprint)s «samsvarer ikke med enhetens fingeravtrykk»%(fprint)s '" }, "no_key_or_device": "Det ser ut til at du ikke har en gjenopprettingsnøkkel eller andre enheter du kan verifisere mot. Denne enheten vil ikke kunne få tilgang til gamle krypterte meldinger. For å bekrefte identiteten din på denne enheten, må du tilbakestille verifiseringsnøklene dine.", "no_support_qr_emoji": "Enheten du prøver å bekrefte støtter ikke skanning av en QR-kode eller emoji-verifikasjon, som er det som %(brand)s støtter. Prøv med en annen klient.", @@ -2118,6 +2121,7 @@ "no_favourites": "Du har ikke favorittchat ennå", "no_favourites_description": "Du kan legge til en chat til dine favoritter i chat-innstillingene", "no_invites": "Du har ingen uleste invitasjoner", + "no_lowpriority": "Du har ingen rom med lav prioritet", "no_mentions": "Du har ingen uleste omtaler", "no_people": "Du har ikke direkte chatter med noen ennå", "no_people_description": "Du kan fjerne merket for filtre for å se de andre chattene dine", @@ -2127,6 +2131,7 @@ "show_activity": "Se alle aktiviteter", "show_chats": "Vis alle chatter" }, + "expand_filters": "Utvid filterlisten", "failed_add_tag": "Kunne ikke legge til tagg %(tagName)s til rom", "failed_remove_tag": "Kunne ikke fjerne tagg %(tagName)s fra rommet", "failed_set_dm_tag": "Kan ikke sette kode på direktemeldingen", @@ -3107,6 +3112,7 @@ "jumptodate": "Gå til den gitte datoen i tidslinjen", "jumptodate_invalid_input": "Vi klarte ikke å forstå den gitte datoen (%(inputDate)s). Prøv å bruke formatet ÅÅÅÅ-MM-DD.", "lenny": "Legger til ( ͡° ͜ʖ ͡°) foran en ren tekstmelding", + "manual_device_verification_confirm_title": "Forsiktig: manuell enhetsverifisering", "me": "Viser handling", "msg": "Sender en melding til den angitte brukeren", "myavatar": "Endrer profilbildet ditt i alle rom", @@ -3147,7 +3153,7 @@ "upgraderoom": "Oppgraderer et rom til en ny versjon", "upgraderoom_permission_error": "Du har ikke de rette tilgangene til å bruke denne kommandoen.", "usage": "Bruk", - "verify": "Verifiserer en bruker-, økt- og pubkey-tuple", + "verify": "Verifiser en av dine egne enheter manuelt", "view": "Viser rom med oppgitt adresse", "whois": "Viser informasjon om en bruker" }, diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 6dedd8e10a..84deb17d98 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -3098,7 +3098,7 @@ "upgraderoom": "Uppgraderar ett rum till en ny version", "upgraderoom_permission_error": "Du har inte de behörigheter som krävs för att använda det här kommandot.", "usage": "Användande", - "verify": "Verifierar en användar-, sessions- och pubkey-tupel", + "verify": "Verifiera en av dina egna enheter manuellt", "view": "Visar rum med den angivna adressen", "whois": "Visar information om en användare" }, @@ -3207,6 +3207,7 @@ "heading_without_query": "Sök efter", "join_button_text": "Gå med i %(roomAddress)s", "keyboard_scroll_hint": "Använd för att skrolla", + "messages_label": "Meddelanden", "other_rooms_in_space": "Andra rum i %(spaceName)s", "public_rooms_label": "Offentliga rum", "public_spaces_label": "Offentliga utrymmen", From 0f0f904cb0266c5478ff9128bbdf89e35cae5241 Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 13 Jun 2025 09:08:29 +0200 Subject: [PATCH 05/12] Mvvm split user info, create userinfoadmintools container component (#29808) * feat: mvvm split user info, create userinfoadmintools container component * test: mvvm userinfoadmintools and view * feat: user info admin components more split and comments * test: mvvm user admin info mute view models more coverage * chore: rename user-info folder to user_info --- .../UserInfoAdminToolsContainerViewModel.tsx | 82 ++++ .../admin/UserInfoBanButtonViewModel.tsx | 153 ++++++ .../admin/UserInfoKickButtonViewModel.tsx | 142 ++++++ .../admin/UserInfoMuteButtonViewModel.tsx | 120 +++++ .../admin/UserInfoRedactButtonViewModel.tsx | 39 ++ src/components/views/right_panel/UserInfo.tsx | 461 +----------------- .../user_info/UserInfoAdminToolsContainer.tsx | 220 +++++++++ ...rInfoAdminToolsContainerViewModel-test.tsx | 173 +++++++ .../admin/UserInfoBanButtonViewModel-test.tsx | 224 +++++++++ .../UserInfoKickButtonViewModel-test.tsx | 232 +++++++++ .../UserInfoMuteButtonViewModel-test.tsx | 230 +++++++++ .../UserInfoRedactButtonViewModel-test.tsx | 98 ++++ .../views/right_panel/UserInfo-test.tsx | 445 +---------------- .../UserInfoAdminToolsContainer-test.tsx | 306 ++++++++++++ 14 files changed, 2024 insertions(+), 901 deletions(-) create mode 100644 src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel.tsx create mode 100644 src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel.tsx create mode 100644 src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel.tsx create mode 100644 src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel.tsx create mode 100644 src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel.tsx create mode 100644 src/components/views/right_panel/user_info/UserInfoAdminToolsContainer.tsx create mode 100644 test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel-test.tsx create mode 100644 test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel-test.tsx create mode 100644 test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel-test.tsx create mode 100644 test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel-test.tsx create mode 100644 test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel-test.tsx create mode 100644 test/unit-tests/components/views/right_panel/UserInfoAdminToolsContainer-test.tsx diff --git a/src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel.tsx b/src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel.tsx new file mode 100644 index 0000000000..54ed32ceb5 --- /dev/null +++ b/src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel.tsx @@ -0,0 +1,82 @@ +/* +Copyright 2025 New Vector Ltd. +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { type Room, type RoomMember, type IPowerLevelsContent } from "matrix-js-sdk/src/matrix"; + +import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; + +/** + * Interface used by admin tools container subcomponents props + */ +export interface RoomAdminToolsProps { + room: Room; + member: RoomMember; + isUpdating: boolean; + startUpdating: () => void; + stopUpdating: () => void; +} + +/** + * Interface used by admin tools container props + */ +export interface RoomAdminToolsContainerProps { + room: Room; + member: RoomMember; + powerLevels: IPowerLevelsContent; +} + +interface UserInfoAdminToolsContainerState { + shouldShowKickButton: boolean; + shouldShowBanButton: boolean; + shouldShowMuteButton: boolean; + shouldShowRedactButton: boolean; + isCurrentUserInTheRoom: boolean; +} + +/** + * The view model for the user info admin tools container + * @param {RoomAdminToolsContainerProps} props - the object containing the necceray props for the view model + * @param {Room} props.room - the room that display the admin tools + * @param {RoomMember} props.member - the selected member + * @param {IPowerLevelsContent} props.powerLevels - current room power levels + * @returns {UserInfoAdminToolsContainerState} the user info admin tools container state + */ +export const useUserInfoAdminToolsContainerViewModel = ( + props: RoomAdminToolsContainerProps, +): UserInfoAdminToolsContainerState => { + const cli = useMatrixClientContext(); + const { room, member, powerLevels } = props; + + const editPowerLevel = + (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || powerLevels.state_default; + + // if these do not exist in the event then they should default to 50 as per the spec + const { ban: banPowerLevel = 50, kick: kickPowerLevel = 50, redact: redactPowerLevel = 50 } = powerLevels; + + const me = room.getMember(cli.getUserId() || ""); + const isCurrentUserInTheRoom = me !== null; + + if (!isCurrentUserInTheRoom) { + return { + shouldShowKickButton: false, + shouldShowBanButton: false, + shouldShowMuteButton: false, + shouldShowRedactButton: false, + isCurrentUserInTheRoom: false, + }; + } + + const isMe = me.userId === member.userId; + const canAffectUser = member.powerLevel < me.powerLevel || isMe; + + return { + shouldShowKickButton: !isMe && canAffectUser && me.powerLevel >= kickPowerLevel, + shouldShowRedactButton: me.powerLevel >= redactPowerLevel && !room.isSpaceRoom(), + shouldShowBanButton: !isMe && canAffectUser && me.powerLevel >= banPowerLevel, + shouldShowMuteButton: !isMe && canAffectUser && me.powerLevel >= Number(editPowerLevel) && !room.isSpaceRoom(), + isCurrentUserInTheRoom, + }; +}; diff --git a/src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel.tsx b/src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel.tsx new file mode 100644 index 0000000000..525b10e093 --- /dev/null +++ b/src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel.tsx @@ -0,0 +1,153 @@ +/* +Copyright 2025 New Vector Ltd. +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { logger } from "@sentry/browser"; +import { type Room } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; + +import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; +import { _t } from "../../../../../languageHandler"; +import Modal from "../../../../../Modal"; +import { bulkSpaceBehaviour } from "../../../../../utils/space"; +import ConfirmSpaceUserActionDialog from "../../../../views/dialogs/ConfirmSpaceUserActionDialog"; +import ConfirmUserActionDialog from "../../../../views/dialogs/ConfirmUserActionDialog"; +import ErrorDialog from "../../../../views/dialogs/ErrorDialog"; +import { type RoomAdminToolsProps } from "./UserInfoAdminToolsContainerViewModel"; + +export interface BanButtonState { + /** + * The function to call when the button is clicked + */ + onBanOrUnbanClick: () => Promise; + /** + * The label of the ban button can be ban or unban + */ + banLabel: string; +} +/** + * The view model for the room ban button used in the UserInfoAdminToolsContainer + * @param {RoomAdminToolsProps} props - the object containing the necceray props for banButton the view model + * @param {Room} props.room - the room to ban/unban the user in + * @param {RoomMember} props.member - the member to ban/unban + * @param {boolean} props.isUpdating - whether the operation is currently in progress + * @param {function} props.startUpdating - callback function to start the operation + * @param {function} props.stopUpdating - callback function to stop the operation + * @returns {BanButtonState} the room ban/unban button state + */ +export const useBanButtonViewModel = (props: RoomAdminToolsProps): BanButtonState => { + const { isUpdating, startUpdating, stopUpdating, room, member } = props; + + const cli = useMatrixClientContext(); + + const isBanned = member.membership === KnownMembership.Ban; + + let banLabel = room.isSpaceRoom() ? _t("user_info|ban_button_space") : _t("user_info|ban_button_room"); + if (isBanned) { + banLabel = room.isSpaceRoom() ? _t("user_info|unban_button_space") : _t("user_info|unban_button_room"); + } + + const onBanOrUnbanClick = async (): Promise => { + if (isUpdating) return; // only allow one operation at a time + startUpdating(); + + const commonProps = { + member, + action: room.isSpaceRoom() + ? isBanned + ? _t("user_info|unban_button_space") + : _t("user_info|ban_button_space") + : isBanned + ? _t("user_info|unban_button_room") + : _t("user_info|ban_button_room"), + title: isBanned + ? _t("user_info|unban_room_confirm_title", { roomName: room.name }) + : _t("user_info|ban_room_confirm_title", { roomName: room.name }), + askReason: !isBanned, + danger: !isBanned, + }; + + let finished: Promise<[success?: boolean, reason?: string, rooms?: Room[]]>; + + if (room.isSpaceRoom()) { + ({ finished } = Modal.createDialog( + ConfirmSpaceUserActionDialog, + { + ...commonProps, + space: room, + spaceChildFilter: isBanned + ? (child: Room) => { + // Return true if the target member is banned and we have sufficient PL to unban + const myMember = child.getMember(cli.credentials.userId || ""); + const theirMember = child.getMember(member.userId); + return ( + !!myMember && + !!theirMember && + theirMember.membership === KnownMembership.Ban && + myMember.powerLevel > theirMember.powerLevel && + child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel) + ); + } + : (child: Room) => { + // Return true if the target member isn't banned and we have sufficient PL to ban + const myMember = child.getMember(cli.credentials.userId || ""); + const theirMember = child.getMember(member.userId); + return ( + !!myMember && + !!theirMember && + theirMember.membership !== KnownMembership.Ban && + myMember.powerLevel > theirMember.powerLevel && + child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel) + ); + }, + allLabel: isBanned ? _t("user_info|unban_space_everything") : _t("user_info|ban_space_everything"), + specificLabel: isBanned ? _t("user_info|unban_space_specific") : _t("user_info|ban_space_specific"), + warningMessage: isBanned ? _t("user_info|unban_space_warning") : _t("user_info|kick_space_warning"), + }, + "mx_ConfirmSpaceUserActionDialog_wrapper", + )); + } else { + ({ finished } = Modal.createDialog(ConfirmUserActionDialog, commonProps)); + } + + const [proceed, reason, rooms = []] = await finished; + if (!proceed) { + stopUpdating(); + return; + } + + const fn = (roomId: string): Promise => { + if (isBanned) { + return cli.unban(roomId, member.userId); + } else { + return cli.ban(roomId, member.userId, reason || undefined); + } + }; + + bulkSpaceBehaviour(room, rooms, (room) => fn(room.roomId)) + .then( + () => { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + logger.info("Ban success"); + }, + function (err) { + logger.error("Ban error: " + err); + Modal.createDialog(ErrorDialog, { + title: _t("common|error"), + description: _t("user_info|error_ban_user"), + }); + }, + ) + .finally(() => { + stopUpdating(); + }); + }; + + return { + onBanOrUnbanClick, + banLabel, + }; +}; diff --git a/src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel.tsx b/src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel.tsx new file mode 100644 index 0000000000..8ae179ac07 --- /dev/null +++ b/src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel.tsx @@ -0,0 +1,142 @@ +/* +Copyright 2025 New Vector Ltd. +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { logger } from "@sentry/browser"; +import { type Room } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; + +import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; +import { _t } from "../../../../../languageHandler"; +import Modal from "../../../../../Modal"; +import { bulkSpaceBehaviour } from "../../../../../utils/space"; +import ConfirmSpaceUserActionDialog from "../../../../views/dialogs/ConfirmSpaceUserActionDialog"; +import ConfirmUserActionDialog from "../../../../views/dialogs/ConfirmUserActionDialog"; +import ErrorDialog from "../../../../views/dialogs/ErrorDialog"; +import { type RoomAdminToolsProps } from "./UserInfoAdminToolsContainerViewModel"; + +interface RoomKickButtonState { + /** + * The function to call when the button is clicked + */ + onKickClick: () => Promise; + /** + * Whether the user can be kicked based on membership value. If the user already join or was invited, it can be kicked + */ + canUserBeKicked: boolean; + /** + * The label of the kick button can be kick or disinvite + */ + kickLabel: string; +} + +/** + * The view model for the room kick button used in the UserInfoAdminToolsContainer + * @param {RoomAdminToolsProps} props - the object containing the necceray props for kickButton the view model + * @param {Room} props.room - the room to kick/disinvite the user from + * @param {RoomMember} props.member - the member to kick/disinvite + * @param {boolean} props.isUpdating - whether the operation is currently in progress + * @param {function} props.startUpdating - callback function to start the operation + * @param {function} props.stopUpdating - callback function to stop the operation + * @returns {KickButtonState} the room kick/disinvite button state + */ +export function useRoomKickButtonViewModel(props: RoomAdminToolsProps): RoomKickButtonState { + const { isUpdating, startUpdating, stopUpdating, room, member } = props; + + const cli = useMatrixClientContext(); + + const onKickClick = async (): Promise => { + if (isUpdating) return; // only allow one operation at a time + startUpdating(); + + const commonProps = { + member, + action: room.isSpaceRoom() + ? member.membership === KnownMembership.Invite + ? _t("user_info|disinvite_button_space") + : _t("user_info|kick_button_space") + : member.membership === KnownMembership.Invite + ? _t("user_info|disinvite_button_room") + : _t("user_info|kick_button_room"), + title: + member.membership === KnownMembership.Invite + ? _t("user_info|disinvite_button_room_name", { roomName: room.name }) + : _t("user_info|kick_button_room_name", { roomName: room.name }), + askReason: member.membership === KnownMembership.Join, + danger: true, + }; + + let finished: Promise<[success?: boolean, reason?: string, rooms?: Room[]]>; + + if (room.isSpaceRoom()) { + ({ finished } = Modal.createDialog( + ConfirmSpaceUserActionDialog, + { + ...commonProps, + space: room, + spaceChildFilter: (child: Room) => { + // Return true if the target member is not banned and we have sufficient PL to ban them + const myMember = child.getMember(cli.credentials.userId || ""); + const theirMember = child.getMember(member.userId); + return ( + !!myMember && + !!theirMember && + theirMember.membership === member.membership && + myMember.powerLevel > theirMember.powerLevel && + child.currentState.hasSufficientPowerLevelFor("kick", myMember.powerLevel) + ); + }, + allLabel: _t("user_info|kick_button_space_everything"), + specificLabel: _t("user_info|kick_space_specific"), + warningMessage: _t("user_info|kick_space_warning"), + }, + "mx_ConfirmSpaceUserActionDialog_wrapper", + )); + } else { + ({ finished } = Modal.createDialog(ConfirmUserActionDialog, commonProps)); + } + + const [proceed, reason, rooms = []] = await finished; + if (!proceed) { + stopUpdating(); + return; + } + + bulkSpaceBehaviour(room, rooms, (room) => cli.kick(room.roomId, member.userId, reason || undefined)) + .then( + () => { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + logger.info("Kick success"); + }, + function (err) { + logger.error("Kick error: " + err); + Modal.createDialog(ErrorDialog, { + title: _t("user_info|error_kicking_user"), + description: err?.message ?? "Operation failed", + }); + }, + ) + .finally(() => { + stopUpdating(); + }); + }; + + const canUserBeKicked = member.membership === KnownMembership.Invite || member.membership === KnownMembership.Join; + + const kickLabel = room.isSpaceRoom() + ? member.membership === KnownMembership.Invite + ? _t("user_info|disinvite_button_space") + : _t("user_info|kick_button_space") + : member.membership === KnownMembership.Invite + ? _t("user_info|disinvite_button_room") + : _t("user_info|kick_button_room"); + + return { + onKickClick, + canUserBeKicked, + kickLabel, + }; +} diff --git a/src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel.tsx b/src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel.tsx new file mode 100644 index 0000000000..1608628198 --- /dev/null +++ b/src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel.tsx @@ -0,0 +1,120 @@ +/* +Copyright 2025 New Vector Ltd. +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { logger } from "@sentry/browser"; +import { type RoomMember, type IPowerLevelsContent } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; + +import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; +import { _t } from "../../../../../languageHandler"; +import Modal from "../../../../../Modal"; +import ErrorDialog from "../../../../views/dialogs/ErrorDialog"; +import { type RoomAdminToolsProps } from "./UserInfoAdminToolsContainerViewModel"; + +interface MuteButtonState { + /** + * Whether the member is in the roomn based on the membership value + */ + isMemberInTheRoom: boolean; + /** + * The label of the mute button can be mute or unmute + */ + muteLabel: string; + /** + * The function to call when the mute button is clicked + */ + onMuteButtonClick: () => Promise; +} + +/** + * The view model for the room mute button used in the UserInfoAdminToolsContainer + * @param {RoomAdminToolsProps} props - the object containing the necceray props for muteButton the view model + * @param {Room} props.room - the room to mute/unmute the user in + * @param {RoomMember} props.member - the member to mute/unmute + * @param {boolean} props.isUpdating - whether the operation is currently in progress + * @param {function} props.startUpdating - callback function to start the operation + * @param {function} props.stopUpdating - callback function to stop the operation + * @returns {MuteButtonState} the room mute/unmute button state + */ +export const useMuteButtonViewModel = (props: RoomAdminToolsProps): MuteButtonState => { + const { isUpdating, startUpdating, stopUpdating, room, member } = props; + + const cli = useMatrixClientContext(); + + const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent): boolean => { + if (!powerLevelContent || !member) return false; + + const levelToSend = + (powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) || + powerLevelContent.events_default; + + // levelToSend could be undefined as .events_default is optional. Coercing in this case using + // Number() would always return false, so this preserves behaviour + // FIXME: per the spec, if `events_default` is unset, it defaults to zero. If + // the member has a negative powerlevel, this will give an incorrect result. + if (levelToSend === undefined) return false; + + return member.powerLevel < levelToSend; + }; + + const muted = isMuted(member, room.currentState.getStateEvents("m.room.power_levels", "")?.getContent() || {}); + const muteLabel = muted ? _t("common|unmute") : _t("common|mute"); + + const isMemberInTheRoom = member.membership == KnownMembership.Join; + + const onMuteButtonClick = async (): Promise => { + if (isUpdating) return; // only allow one operation at a time + startUpdating(); + + const roomId = member.roomId; + const target = member.userId; + + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + const powerLevels = powerLevelEvent?.getContent(); + const levelToSend = powerLevels?.events?.["m.room.message"] ?? powerLevels?.events_default; + + let level; + if (muted) { + // unmute + level = levelToSend; + } else { + // mute + level = levelToSend - 1; + } + level = parseInt(level); + + console.log("level", level); + if (isNaN(level)) { + stopUpdating(); + return; + } + + cli.setPowerLevel(roomId, target, level) + .then( + () => { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + logger.info("Mute toggle success"); + }, + function (err) { + logger.error("Mute error: " + err); + Modal.createDialog(ErrorDialog, { + title: _t("common|error"), + description: _t("user_info|error_mute_user"), + }); + }, + ) + .finally(() => { + stopUpdating(); + }); + }; + + return { + isMemberInTheRoom, + onMuteButtonClick, + muteLabel, + }; +}; diff --git a/src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel.tsx b/src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel.tsx new file mode 100644 index 0000000000..73b8ea70f0 --- /dev/null +++ b/src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel.tsx @@ -0,0 +1,39 @@ +/* +Copyright 2025 New Vector Ltd. +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { type RoomMember } from "matrix-js-sdk/src/matrix"; + +import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; +import Modal from "../../../../../Modal"; +import BulkRedactDialog from "../../../../views/dialogs/BulkRedactDialog"; + +export interface RedactMessagesButtonState { + onRedactAllMessagesClick: () => void; +} + +/** + * The view model for the redact messages button used in the UserInfoAdminToolsContainer + * @param {RoomMember} member - the selected member to redact messages for + * @returns {RedactMessagesButtonState} the redact messages button state + */ +export const useRedactMessagesButtonViewModel = (member: RoomMember): RedactMessagesButtonState => { + const cli = useMatrixClientContext(); + + const onRedactAllMessagesClick = (): void => { + const room = cli.getRoom(member.roomId); + if (!room) return; + + Modal.createDialog(BulkRedactDialog, { + matrixClient: cli, + room, + member, + }); + }; + + return { + onRedactAllMessagesClick, + }; +}; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 7bba4f0950..542e6421de 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -34,10 +34,6 @@ import MentionIcon from "@vector-im/compound-design-tokens/assets/web/icons/ment import InviteIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add"; import BlockIcon from "@vector-im/compound-design-tokens/assets/web/icons/block"; import DeleteIcon from "@vector-im/compound-design-tokens/assets/web/icons/delete"; -import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; -import ChatProblemIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat-problem"; -import VisibilityOffIcon from "@vector-im/compound-design-tokens/assets/web/icons/visibility-off"; -import LeaveIcon from "@vector-im/compound-design-tokens/assets/web/icons/leave"; import dis from "../../../dispatcher/dispatcher"; import Modal from "../../../Modal"; @@ -61,15 +57,11 @@ import Spinner from "../elements/Spinner"; import PowerSelector from "../elements/PowerSelector"; import MemberAvatar from "../avatars/MemberAvatar"; import PresenceLabel from "../rooms/PresenceLabel"; -import BulkRedactDialog from "../dialogs/BulkRedactDialog"; import { ShareDialog } from "../dialogs/ShareDialog"; import ErrorDialog from "../dialogs/ErrorDialog"; import QuestionDialog from "../dialogs/QuestionDialog"; -import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog"; import { mediaFromMxc } from "../../../customisations/Media"; import { type ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; -import ConfirmSpaceUserActionDialog from "../dialogs/ConfirmSpaceUserActionDialog"; -import { bulkSpaceBehaviour } from "../../../utils/space"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; import { TimelineRenderingType } from "../../../contexts/RoomContext"; @@ -83,6 +75,7 @@ import { SdkContextClass } from "../../../contexts/SDKContext"; import { Flex } from "../../utils/Flex"; import CopyableText from "../elements/CopyableText"; import { useUserTimezone } from "../../../hooks/useUserTimezone"; +import { UserInfoAdminToolsContainer } from "./user_info/UserInfoAdminToolsContainer"; export interface IDevice extends Device { ambiguous?: boolean; @@ -314,7 +307,7 @@ const Container: React.FC<{ return
{children}
; }; -interface IPowerLevelsContent { +export interface IPowerLevelsContent { events?: Record; // eslint-disable-next-line camelcase users_default?: number; @@ -368,362 +361,6 @@ export const useRoomPowerLevels = (cli: MatrixClient, room: Room): IPowerLevelsC return powerLevels; }; -interface IBaseProps { - member: RoomMember; - isUpdating: boolean; - startUpdating(): void; - stopUpdating(): void; -} - -export const RoomKickButton = ({ - room, - member, - isUpdating, - startUpdating, - stopUpdating, -}: Omit): JSX.Element | null => { - const cli = useContext(MatrixClientContext); - - // check if user can be kicked/disinvited - if (member.membership !== KnownMembership.Invite && member.membership !== KnownMembership.Join) return <>; - - const onKick = async (): Promise => { - if (isUpdating) return; // only allow one operation at a time - startUpdating(); - - const commonProps = { - member, - action: room.isSpaceRoom() - ? member.membership === KnownMembership.Invite - ? _t("user_info|disinvite_button_space") - : _t("user_info|kick_button_space") - : member.membership === KnownMembership.Invite - ? _t("user_info|disinvite_button_room") - : _t("user_info|kick_button_room"), - title: - member.membership === KnownMembership.Invite - ? _t("user_info|disinvite_button_room_name", { roomName: room.name }) - : _t("user_info|kick_button_room_name", { roomName: room.name }), - askReason: member.membership === KnownMembership.Join, - danger: true, - }; - - let finished: Promise<[success?: boolean, reason?: string, rooms?: Room[]]>; - - if (room.isSpaceRoom()) { - ({ finished } = Modal.createDialog( - ConfirmSpaceUserActionDialog, - { - ...commonProps, - space: room, - spaceChildFilter: (child: Room) => { - // Return true if the target member is not banned and we have sufficient PL to ban them - const myMember = child.getMember(cli.credentials.userId || ""); - const theirMember = child.getMember(member.userId); - return ( - !!myMember && - !!theirMember && - theirMember.membership === member.membership && - myMember.powerLevel > theirMember.powerLevel && - child.currentState.hasSufficientPowerLevelFor("kick", myMember.powerLevel) - ); - }, - allLabel: _t("user_info|kick_button_space_everything"), - specificLabel: _t("user_info|kick_space_specific"), - warningMessage: _t("user_info|kick_space_warning"), - }, - "mx_ConfirmSpaceUserActionDialog_wrapper", - )); - } else { - ({ finished } = Modal.createDialog(ConfirmUserActionDialog, commonProps)); - } - - const [proceed, reason, rooms = []] = await finished; - if (!proceed) { - stopUpdating(); - return; - } - - bulkSpaceBehaviour(room, rooms, (room) => cli.kick(room.roomId, member.userId, reason || undefined)) - .then( - () => { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - logger.log("Kick success"); - }, - function (err) { - logger.error("Kick error: " + err); - Modal.createDialog(ErrorDialog, { - title: _t("user_info|error_kicking_user"), - description: err?.message ?? "Operation failed", - }); - }, - ) - .finally(() => { - stopUpdating(); - }); - }; - - const kickLabel = room.isSpaceRoom() - ? member.membership === KnownMembership.Invite - ? _t("user_info|disinvite_button_space") - : _t("user_info|kick_button_space") - : member.membership === KnownMembership.Invite - ? _t("user_info|disinvite_button_room") - : _t("user_info|kick_button_room"); - - return ( - { - ev.preventDefault(); - onKick(); - }} - disabled={isUpdating} - label={kickLabel} - kind="critical" - Icon={LeaveIcon} - /> - ); -}; - -const RedactMessagesButton: React.FC = ({ member }) => { - const cli = useContext(MatrixClientContext); - - const onRedactAllMessages = (): void => { - const room = cli.getRoom(member.roomId); - if (!room) return; - - Modal.createDialog(BulkRedactDialog, { - matrixClient: cli, - room, - member, - }); - }; - - return ( - { - ev.preventDefault(); - onRedactAllMessages(); - }} - label={_t("user_info|redact_button")} - kind="critical" - Icon={CloseIcon} - /> - ); -}; - -export const BanToggleButton = ({ - room, - member, - isUpdating, - startUpdating, - stopUpdating, -}: Omit): JSX.Element => { - const cli = useContext(MatrixClientContext); - - const isBanned = member.membership === KnownMembership.Ban; - const onBanOrUnban = async (): Promise => { - if (isUpdating) return; // only allow one operation at a time - startUpdating(); - - const commonProps = { - member, - action: room.isSpaceRoom() - ? isBanned - ? _t("user_info|unban_button_space") - : _t("user_info|ban_button_space") - : isBanned - ? _t("user_info|unban_button_room") - : _t("user_info|ban_button_room"), - title: isBanned - ? _t("user_info|unban_room_confirm_title", { roomName: room.name }) - : _t("user_info|ban_room_confirm_title", { roomName: room.name }), - askReason: !isBanned, - danger: !isBanned, - }; - - let finished: Promise<[success?: boolean, reason?: string, rooms?: Room[]]>; - - if (room.isSpaceRoom()) { - ({ finished } = Modal.createDialog( - ConfirmSpaceUserActionDialog, - { - ...commonProps, - space: room, - spaceChildFilter: isBanned - ? (child: Room) => { - // Return true if the target member is banned and we have sufficient PL to unban - const myMember = child.getMember(cli.credentials.userId || ""); - const theirMember = child.getMember(member.userId); - return ( - !!myMember && - !!theirMember && - theirMember.membership === KnownMembership.Ban && - myMember.powerLevel > theirMember.powerLevel && - child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel) - ); - } - : (child: Room) => { - // Return true if the target member isn't banned and we have sufficient PL to ban - const myMember = child.getMember(cli.credentials.userId || ""); - const theirMember = child.getMember(member.userId); - return ( - !!myMember && - !!theirMember && - theirMember.membership !== KnownMembership.Ban && - myMember.powerLevel > theirMember.powerLevel && - child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel) - ); - }, - allLabel: isBanned ? _t("user_info|unban_space_everything") : _t("user_info|ban_space_everything"), - specificLabel: isBanned ? _t("user_info|unban_space_specific") : _t("user_info|ban_space_specific"), - warningMessage: isBanned ? _t("user_info|unban_space_warning") : _t("user_info|kick_space_warning"), - }, - "mx_ConfirmSpaceUserActionDialog_wrapper", - )); - } else { - ({ finished } = Modal.createDialog(ConfirmUserActionDialog, commonProps)); - } - - const [proceed, reason, rooms = []] = await finished; - if (!proceed) { - stopUpdating(); - return; - } - - const fn = (roomId: string): Promise => { - if (isBanned) { - return cli.unban(roomId, member.userId); - } else { - return cli.ban(roomId, member.userId, reason || undefined); - } - }; - - bulkSpaceBehaviour(room, rooms, (room) => fn(room.roomId)) - .then( - () => { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - logger.log("Ban success"); - }, - function (err) { - logger.error("Ban error: " + err); - Modal.createDialog(ErrorDialog, { - title: _t("common|error"), - description: _t("user_info|error_ban_user"), - }); - }, - ) - .finally(() => { - stopUpdating(); - }); - }; - - let label = room.isSpaceRoom() ? _t("user_info|ban_button_space") : _t("user_info|ban_button_room"); - if (isBanned) { - label = room.isSpaceRoom() ? _t("user_info|unban_button_space") : _t("user_info|unban_button_room"); - } - - return ( - { - ev.preventDefault(); - onBanOrUnban(); - }} - disabled={isUpdating} - label={label} - kind="critical" - Icon={ChatProblemIcon} - /> - ); -}; - -interface IBaseRoomProps extends IBaseProps { - room: Room; - powerLevels: IPowerLevelsContent; - children?: ReactNode; -} - -// We do not show a Mute button for ourselves so it doesn't need to handle warning self demotion -const MuteToggleButton: React.FC = ({ - member, - room, - powerLevels, - isUpdating, - startUpdating, - stopUpdating, -}) => { - const cli = useContext(MatrixClientContext); - - // Don't show the mute/unmute option if the user is not in the room - if (member.membership !== KnownMembership.Join) return null; - - const muted = isMuted(member, powerLevels); - const onMuteToggle = async (): Promise => { - if (isUpdating) return; // only allow one operation at a time - startUpdating(); - - const roomId = member.roomId; - const target = member.userId; - - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - const powerLevels = powerLevelEvent?.getContent(); - const levelToSend = powerLevels?.events?.["m.room.message"] ?? powerLevels?.events_default; - let level; - if (muted) { - // unmute - level = levelToSend; - } else { - // mute - level = levelToSend - 1; - } - level = parseInt(level); - - if (isNaN(level)) { - stopUpdating(); - return; - } - - cli.setPowerLevel(roomId, target, level) - .then( - () => { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - logger.log("Mute toggle success"); - }, - function (err) { - logger.error("Mute error: " + err); - Modal.createDialog(ErrorDialog, { - title: _t("common|error"), - description: _t("user_info|error_mute_user"), - }); - }, - ) - .finally(() => { - stopUpdating(); - }); - }; - - const muteLabel = muted ? _t("common|unmute") : _t("common|mute"); - return ( - { - ev.preventDefault(); - onMuteToggle(); - }} - disabled={isUpdating} - label={muteLabel} - kind="critical" - Icon={VisibilityOffIcon} - /> - ); -}; - const IgnoreToggleButton: React.FC<{ member: User | RoomMember; }> = ({ member }) => { @@ -786,96 +423,6 @@ const IgnoreToggleButton: React.FC<{ ); }; -export const RoomAdminToolsContainer: React.FC = ({ - room, - children, - member, - isUpdating, - startUpdating, - stopUpdating, - powerLevels, -}) => { - const cli = useContext(MatrixClientContext); - let kickButton; - let banButton; - let muteButton; - let redactButton; - - const editPowerLevel = - (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || powerLevels.state_default; - - // if these do not exist in the event then they should default to 50 as per the spec - const { ban: banPowerLevel = 50, kick: kickPowerLevel = 50, redact: redactPowerLevel = 50 } = powerLevels; - - const me = room.getMember(cli.getUserId() || ""); - if (!me) { - // we aren't in the room, so return no admin tooling - return
; - } - - const isMe = me.userId === member.userId; - const canAffectUser = member.powerLevel < me.powerLevel || isMe; - - if (!isMe && canAffectUser && me.powerLevel >= kickPowerLevel) { - kickButton = ( - - ); - } - if (me.powerLevel >= redactPowerLevel && !room.isSpaceRoom()) { - redactButton = ( - - ); - } - if (!isMe && canAffectUser && me.powerLevel >= banPowerLevel) { - banButton = ( - - ); - } - if (!isMe && canAffectUser && me.powerLevel >= Number(editPowerLevel) && !room.isSpaceRoom()) { - muteButton = ( - - ); - } - - if (kickButton || banButton || muteButton || redactButton || children) { - return ( - - {muteButton} - {redactButton} - {kickButton} - {banButton} - {children} - - ); - } - - return
; -}; - const useIsSynapseAdmin = (cli?: MatrixClient): boolean => { return useAsyncMemo(async () => (cli ? cli.isSynapseAdministrator().catch(() => false) : false), [cli], false); }; @@ -1283,7 +830,7 @@ const BasicUserInfo: React.FC<{ } adminToolsContainer = ( - {synapseDeactivateButton} - + ); } else if (synapseDeactivateButton) { adminToolsContainer = {synapseDeactivateButton}; diff --git a/src/components/views/right_panel/user_info/UserInfoAdminToolsContainer.tsx b/src/components/views/right_panel/user_info/UserInfoAdminToolsContainer.tsx new file mode 100644 index 0000000000..1f3eeb706d --- /dev/null +++ b/src/components/views/right_panel/user_info/UserInfoAdminToolsContainer.tsx @@ -0,0 +1,220 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import React, { type JSX, type ReactNode } from "react"; +import classNames from "classnames"; +import { type RoomMember, type Room } from "matrix-js-sdk/src/matrix"; +import { MenuItem } from "@vector-im/compound-web"; +import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; +import ChatProblemIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat-problem"; +import VisibilityOffIcon from "@vector-im/compound-design-tokens/assets/web/icons/visibility-off"; +import LeaveIcon from "@vector-im/compound-design-tokens/assets/web/icons/leave"; + +import { _t } from "../../../../languageHandler"; +import { type IPowerLevelsContent } from "../UserInfo"; +import { useUserInfoAdminToolsContainerViewModel } from "../../../viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel"; +import { useMuteButtonViewModel } from "../../../viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel"; +import { useBanButtonViewModel } from "../../../viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel"; +import { useRoomKickButtonViewModel } from "../../../viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel"; +import { useRedactMessagesButtonViewModel } from "../../../viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel"; + +const Container: React.FC<{ + children: ReactNode; + className?: string; +}> = ({ children, className }) => { + const classes = classNames("mx_UserInfo_container", className); + return
{children}
; +}; + +interface IBaseProps { + member: RoomMember; + isUpdating: boolean; + startUpdating(): void; + stopUpdating(): void; +} + +export const RoomKickButton = ({ + room, + member, + isUpdating, + startUpdating, + stopUpdating, +}: Omit): JSX.Element | null => { + const vm = useRoomKickButtonViewModel({ room, member, isUpdating, startUpdating, stopUpdating }); + // check if user can be kicked/disinvited + if (!vm.canUserBeKicked) return <>; + + return ( + { + ev.preventDefault(); + vm.onKickClick(); + }} + disabled={isUpdating} + label={vm.kickLabel} + kind="critical" + Icon={LeaveIcon} + /> + ); +}; + +const RedactMessagesButton: React.FC = ({ member }) => { + const vm = useRedactMessagesButtonViewModel(member); + + return ( + { + ev.preventDefault(); + vm.onRedactAllMessagesClick(); + }} + label={_t("user_info|redact_button")} + kind="critical" + Icon={CloseIcon} + /> + ); +}; + +export const BanToggleButton = ({ + room, + member, + isUpdating, + startUpdating, + stopUpdating, +}: Omit): JSX.Element => { + const vm = useBanButtonViewModel({ room, member, isUpdating, startUpdating, stopUpdating }); + + return ( + { + ev.preventDefault(); + vm.onBanOrUnbanClick(); + }} + disabled={isUpdating} + label={vm.banLabel} + kind="critical" + Icon={ChatProblemIcon} + /> + ); +}; + +interface IBaseRoomProps extends IBaseProps { + room: Room; + powerLevels: IPowerLevelsContent; + children?: ReactNode; +} + +// We do not show a Mute button for ourselves so it doesn't need to handle warning self demotion +const MuteToggleButton: React.FC = ({ + member, + room, + powerLevels, + isUpdating, + startUpdating, + stopUpdating, +}) => { + const vm = useMuteButtonViewModel({ room, member, isUpdating, startUpdating, stopUpdating }); + // Don't show the mute/unmute option if the user is not in the room + if (!vm.isMemberInTheRoom) return null; + + return ( + { + ev.preventDefault(); + vm.onMuteButtonClick(); + }} + disabled={isUpdating} + label={vm.muteLabel} + kind="critical" + Icon={VisibilityOffIcon} + /> + ); +}; + +export const UserInfoAdminToolsContainer: React.FC = ({ + room, + children, + member, + isUpdating, + startUpdating, + stopUpdating, + powerLevels, +}) => { + let kickButton; + let banButton; + let muteButton; + let redactButton; + + const vm = useUserInfoAdminToolsContainerViewModel({ room, member, powerLevels }); + + if (!vm.isCurrentUserInTheRoom) { + // we aren't in the room, so return no admin tooling + return
; + } + + if (vm.shouldShowKickButton) { + kickButton = ( + + ); + } + if (vm.shouldShowRedactButton) { + redactButton = ( + + ); + } + if (vm.shouldShowBanButton) { + banButton = ( + + ); + } + if (vm.shouldShowMuteButton) { + muteButton = ( + + ); + } + + if (kickButton || banButton || muteButton || redactButton || children) { + return ( + + {muteButton} + {redactButton} + {kickButton} + {banButton} + {children} + + ); + } + + return
; +}; diff --git a/test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel-test.tsx b/test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel-test.tsx new file mode 100644 index 0000000000..d07bc1ad7a --- /dev/null +++ b/test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel-test.tsx @@ -0,0 +1,173 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { renderHook } from "jest-matrix-react"; +import { type Mocked, mocked } from "jest-mock"; +import { type Room, type MatrixClient, RoomMember, type IPowerLevelsContent } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; + +import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg"; +import { + type RoomAdminToolsContainerProps, + useUserInfoAdminToolsContainerViewModel, +} from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel"; +import { withClientContextRenderOptions } from "../../../../../../test-utils"; + +describe("UserInfoAdminToolsContainerViewModel", () => { + const defaultRoomId = "!fkfk"; + const defaultUserId = "@user:example.com"; + + let mockRoom: Mocked; + let mockClient: Mocked; + let mockPowerLevels: IPowerLevelsContent; + + const defaultMember = new RoomMember(defaultRoomId, defaultUserId); + + let defaultContainerProps: RoomAdminToolsContainerProps; + + beforeEach(() => { + mockRoom = mocked({ + roomId: defaultRoomId, + getType: jest.fn().mockReturnValue(undefined), + isSpaceRoom: jest.fn().mockReturnValue(false), + getMember: jest.fn().mockReturnValue(undefined), + getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"), + name: "test room", + on: jest.fn(), + off: jest.fn(), + currentState: { + getStateEvents: jest.fn(), + on: jest.fn(), + off: jest.fn(), + }, + getEventReadUpTo: jest.fn(), + } as unknown as Room); + + mockPowerLevels = { + users: { + "@currentuser:example.com": 100, + }, + events: {}, + state_default: 50, + ban: 50, + kick: 50, + redact: 50, + }; + + defaultContainerProps = { + room: mockRoom, + member: defaultMember, + powerLevels: mockPowerLevels, + }; + + mockClient = mocked({ + getUser: jest.fn(), + isGuest: jest.fn().mockReturnValue(false), + isUserIgnored: jest.fn(), + getIgnoredUsers: jest.fn(), + setIgnoredUsers: jest.fn(), + getUserId: jest.fn().mockReturnValue(defaultUserId), + getSafeUserId: jest.fn(), + getDomain: jest.fn(), + on: jest.fn(), + off: jest.fn(), + isSynapseAdministrator: jest.fn().mockResolvedValue(false), + doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false), + doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false), + getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")), + mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"), + removeListener: jest.fn(), + currentState: { + on: jest.fn(), + }, + getRoom: jest.fn(), + credentials: {}, + setPowerLevel: jest.fn(), + } as unknown as MatrixClient); + + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); + jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient); + }); + + const renderAdminToolsContainerHook = (props = defaultContainerProps) => { + return renderHook( + () => useUserInfoAdminToolsContainerViewModel(props), + withClientContextRenderOptions(mockClient), + ); + }; + + describe("useUserInfoAdminToolsContainerViewModel", () => { + it("should return false when user is not in the room", () => { + mockRoom.getMember.mockReturnValue(null); + const { result } = renderAdminToolsContainerHook(); + expect(result.current).toEqual({ + isCurrentUserInTheRoom: false, + shouldShowKickButton: false, + shouldShowBanButton: false, + shouldShowMuteButton: false, + shouldShowRedactButton: false, + }); + }); + + it("should not show kick, ban and mute buttons if user is me", () => { + const mockMeMember = new RoomMember(mockRoom.roomId, "arbitraryId"); + mockMeMember.powerLevel = 51; // defaults to 50 + mockRoom.getMember.mockReturnValueOnce(mockMeMember); + + const props = { + ...defaultContainerProps, + room: mockRoom, + member: mockMeMember, + powerLevels: mockPowerLevels, + }; + const { result } = renderAdminToolsContainerHook(props); + + expect(result.current).toEqual({ + isCurrentUserInTheRoom: true, + shouldShowKickButton: false, + shouldShowBanButton: false, + shouldShowMuteButton: false, + shouldShowRedactButton: true, + }); + }); + + it("returns mute toggle button if conditions met", () => { + const mockMeMember = new RoomMember(mockRoom.roomId, "arbitraryId"); + mockMeMember.powerLevel = 51; // defaults to 50 + mockRoom.getMember.mockReturnValueOnce(mockMeMember); + + const defaultMemberWithPowerLevelAndJoinMembership = { + ...defaultMember, + powerLevel: 0, + membership: KnownMembership.Join, + } as RoomMember; + + const { result } = renderAdminToolsContainerHook({ + ...defaultContainerProps, + member: defaultMemberWithPowerLevelAndJoinMembership, + powerLevels: { events: { "m.room.power_levels": 1 } }, + }); + + expect(result.current.shouldShowMuteButton).toBe(true); + }); + + it("should not show mute button for one's own member", () => { + const mockMeMember = new RoomMember(mockRoom.roomId, mockClient.getSafeUserId()); + mockMeMember.powerLevel = 51; // defaults to 50 + mockRoom.getMember.mockReturnValueOnce(mockMeMember); + mockClient.getUserId.mockReturnValueOnce(mockMeMember.userId); + + const { result } = renderAdminToolsContainerHook({ + ...defaultContainerProps, + member: mockMeMember, + powerLevels: { events: { "m.room.power_levels": 100 } }, + }); + + expect(result.current.shouldShowMuteButton).toBe(false); + }); + }); +}); diff --git a/test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel-test.tsx b/test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel-test.tsx new file mode 100644 index 0000000000..a4825d1550 --- /dev/null +++ b/test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel-test.tsx @@ -0,0 +1,224 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { cleanup, renderHook } from "jest-matrix-react"; +import { type Mocked, mocked } from "jest-mock"; +import { type Room, type MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; + +import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg"; +import { type RoomAdminToolsProps } from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel"; +import { useBanButtonViewModel } from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel"; +import Modal from "../../../../../../../src/Modal"; +import { withClientContextRenderOptions } from "../../../../../../test-utils"; + +describe("useBanButtonViewModel", () => { + const defaultRoomId = "!fkfk"; + const defaultUserId = "@user:example.com"; + + let mockRoom: Mocked; + let mockSpace: Mocked; + let mockClient: Mocked; + + const defaultMember = new RoomMember(defaultRoomId, defaultUserId); + + const memberWithBanMembership = { ...defaultMember, membership: KnownMembership.Ban } as RoomMember; + + let defaultAdminToolsProps: RoomAdminToolsProps; + const createDialogSpy: jest.SpyInstance = jest.spyOn(Modal, "createDialog"); + + beforeEach(() => { + mockRoom = mocked({ + roomId: defaultRoomId, + getType: jest.fn().mockReturnValue(undefined), + isSpaceRoom: jest.fn().mockReturnValue(false), + getMember: jest.fn().mockReturnValue(undefined), + getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"), + name: "test room", + on: jest.fn(), + off: jest.fn(), + currentState: { + getStateEvents: jest.fn(), + on: jest.fn(), + off: jest.fn(), + }, + getEventReadUpTo: jest.fn(), + } as unknown as Room); + + mockSpace = mocked({ + roomId: defaultRoomId, + getType: jest.fn().mockReturnValue("m.space"), + isSpaceRoom: jest.fn().mockReturnValue(true), + getMember: jest.fn().mockReturnValue(undefined), + getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"), + name: "test room", + on: jest.fn(), + off: jest.fn(), + currentState: { + getStateEvents: jest.fn(), + on: jest.fn(), + off: jest.fn(), + }, + getEventReadUpTo: jest.fn(), + } as unknown as Room); + + defaultAdminToolsProps = { + room: mockRoom, + member: defaultMember, + isUpdating: false, + startUpdating: jest.fn(), + stopUpdating: jest.fn(), + }; + + mockClient = mocked({ + getUser: jest.fn(), + isGuest: jest.fn().mockReturnValue(false), + isUserIgnored: jest.fn(), + getIgnoredUsers: jest.fn(), + setIgnoredUsers: jest.fn(), + getUserId: jest.fn().mockReturnValue(defaultUserId), + getSafeUserId: jest.fn(), + getDomain: jest.fn(), + on: jest.fn(), + off: jest.fn(), + isSynapseAdministrator: jest.fn().mockResolvedValue(false), + doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false), + doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false), + getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")), + mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"), + removeListener: jest.fn(), + currentState: { + on: jest.fn(), + }, + getRoom: jest.fn(), + credentials: {}, + setPowerLevel: jest.fn(), + } as unknown as MatrixClient); + + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); + jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient); + mockRoom.getMember.mockReturnValue(defaultMember); + }); + + const renderBanButtonHook = (props = defaultAdminToolsProps) => { + return renderHook(() => useBanButtonViewModel(props), withClientContextRenderOptions(mockClient)); + }; + + it("renders the correct labels for banned and unbanned members", () => { + // test for room + const propsWithBanMembership = { + ...defaultAdminToolsProps, + member: memberWithBanMembership, + }; + + // defaultMember is not banned + const { result } = renderBanButtonHook(); + expect(result.current.banLabel).toBe("Ban from room"); + cleanup(); + + const { result: result2 } = renderBanButtonHook(propsWithBanMembership); + expect(result2.current.banLabel).toBe("Unban from room"); + cleanup(); + + // test for space + const { result: result3 } = renderBanButtonHook({ ...defaultAdminToolsProps, room: mockSpace }); + expect(result3.current.banLabel).toBe("Ban from space"); + cleanup(); + + const { result: result4 } = renderBanButtonHook({ + ...propsWithBanMembership, + room: mockSpace, + }); + expect(result4.current.banLabel).toBe("Unban from space"); + cleanup(); + }); + + it("clicking the ban or unban button calls Modal.createDialog with the correct arguments if user is not banned", async () => { + createDialogSpy.mockReturnValueOnce({ finished: Promise.resolve([]), close: jest.fn() }); + + const propsWithSpace = { + ...defaultAdminToolsProps, + room: mockSpace, + }; + const { result } = renderBanButtonHook(propsWithSpace); + await result.current.onBanOrUnbanClick(); + + // check the last call arguments and the presence of the spaceChildFilter callback + expect(createDialogSpy).toHaveBeenLastCalledWith( + expect.any(Function), + expect.objectContaining({ spaceChildFilter: expect.any(Function) }), + "mx_ConfirmSpaceUserActionDialog_wrapper", + ); + + // test the spaceChildFilter callback + const callback = createDialogSpy.mock.lastCall[1].spaceChildFilter; + + // make dummy values for myMember and theirMember, then we will test + // null vs their member followed by + // truthy my member vs their member + const mockMyMember = { powerLevel: 1 }; + const mockTheirMember = { membership: "is not ban", powerLevel: 0 }; + + const mockRoom = { + getMember: jest + .fn() + .mockReturnValueOnce(null) + .mockReturnValueOnce(mockTheirMember) + .mockReturnValueOnce(mockMyMember) + .mockReturnValueOnce(mockTheirMember), + currentState: { + hasSufficientPowerLevelFor: jest.fn().mockReturnValue(true), + }, + }; + + expect(callback(mockRoom)).toBe(false); + expect(callback(mockRoom)).toBe(true); + }); + + it("clicking the ban or unban button calls Modal.createDialog with the correct arguments if user _is_ banned", async () => { + createDialogSpy.mockReturnValueOnce({ finished: Promise.resolve([]), close: jest.fn() }); + + const propsWithBanMembership = { + ...defaultAdminToolsProps, + member: memberWithBanMembership, + room: mockSpace, + }; + const { result } = renderBanButtonHook(propsWithBanMembership); + await result.current.onBanOrUnbanClick(); + + // check the last call arguments and the presence of the spaceChildFilter callback + expect(createDialogSpy).toHaveBeenLastCalledWith( + expect.any(Function), + expect.objectContaining({ spaceChildFilter: expect.any(Function) }), + "mx_ConfirmSpaceUserActionDialog_wrapper", + ); + + // test the spaceChildFilter callback + const callback = createDialogSpy.mock.lastCall[1].spaceChildFilter; + + // make dummy values for myMember and theirMember, then we will test + // null vs their member followed by + // my member vs their member + const mockMyMember = { powerLevel: 1 }; + const mockTheirMember = { membership: KnownMembership.Ban, powerLevel: 0 }; + + const mockRoom = { + getMember: jest + .fn() + .mockReturnValueOnce(null) + .mockReturnValueOnce(mockTheirMember) + .mockReturnValueOnce(mockMyMember) + .mockReturnValueOnce(mockTheirMember), + currentState: { + hasSufficientPowerLevelFor: jest.fn().mockReturnValue(true), + }, + }; + + expect(callback(mockRoom)).toBe(false); + expect(callback(mockRoom)).toBe(true); + }); +}); diff --git a/test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel-test.tsx b/test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel-test.tsx new file mode 100644 index 0000000000..e4e0d578be --- /dev/null +++ b/test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel-test.tsx @@ -0,0 +1,232 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { cleanup, renderHook } from "jest-matrix-react"; +import { type Mocked, mocked } from "jest-mock"; +import { type Room, type MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; + +import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg"; +import { type RoomAdminToolsProps } from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel"; +import { useRoomKickButtonViewModel } from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel"; +import Modal from "../../../../../../../src/Modal"; +import { withClientContextRenderOptions } from "../../../../../../test-utils"; + +describe("useRoomKickButtonViewModel", () => { + const defaultRoomId = "!fkfk"; + const defaultUserId = "@user:example.com"; + + let mockRoom: Mocked; + let mockSpace: Mocked; + let mockClient: Mocked; + + const defaultMember = new RoomMember(defaultRoomId, defaultUserId); + const memberWithInviteMembership = { ...defaultMember, membership: KnownMembership.Invite } as RoomMember; + const memberWithJoinMembership = { ...defaultMember, membership: KnownMembership.Join } as RoomMember; + + const createDialogSpy: jest.SpyInstance = jest.spyOn(Modal, "createDialog"); + + let defaultAdminToolsProps: RoomAdminToolsProps; + + beforeEach(() => { + mockRoom = mocked({ + roomId: defaultRoomId, + getType: jest.fn().mockReturnValue(undefined), + isSpaceRoom: jest.fn().mockReturnValue(false), + getMember: jest.fn().mockReturnValue(undefined), + getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"), + name: "test room", + on: jest.fn(), + off: jest.fn(), + currentState: { + getStateEvents: jest.fn(), + on: jest.fn(), + off: jest.fn(), + }, + getEventReadUpTo: jest.fn(), + } as unknown as Room); + + mockSpace = mocked({ + roomId: defaultRoomId, + getType: jest.fn().mockReturnValue("m.space"), + isSpaceRoom: jest.fn().mockReturnValue(true), + getMember: jest.fn().mockReturnValue(undefined), + getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"), + name: "test room", + on: jest.fn(), + off: jest.fn(), + currentState: { + getStateEvents: jest.fn(), + on: jest.fn(), + off: jest.fn(), + }, + getEventReadUpTo: jest.fn(), + } as unknown as Room); + + defaultAdminToolsProps = { + room: mockRoom, + member: defaultMember, + isUpdating: false, + startUpdating: jest.fn(), + stopUpdating: jest.fn(), + }; + + mockClient = mocked({ + getUser: jest.fn(), + isGuest: jest.fn().mockReturnValue(false), + isUserIgnored: jest.fn(), + getIgnoredUsers: jest.fn(), + setIgnoredUsers: jest.fn(), + getUserId: jest.fn().mockReturnValue(defaultUserId), + getSafeUserId: jest.fn(), + getDomain: jest.fn(), + on: jest.fn(), + off: jest.fn(), + isSynapseAdministrator: jest.fn().mockResolvedValue(false), + doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false), + doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false), + getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")), + mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"), + removeListener: jest.fn(), + currentState: { + on: jest.fn(), + }, + getRoom: jest.fn(), + credentials: {}, + setPowerLevel: jest.fn(), + } as unknown as MatrixClient); + + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); + jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient); + // mock useContext to return mockClient + // jest.spyOn(React, "useContext").mockReturnValue(mockClient); + + mockRoom.getMember.mockReturnValue(defaultMember); + }); + + afterEach(() => { + createDialogSpy.mockReset(); + }); + + const renderKickButtonHook = (props = defaultAdminToolsProps) => { + return renderHook(() => useRoomKickButtonViewModel(props), withClientContextRenderOptions(mockClient)); + }; + + it("renders nothing if member.membership is undefined", () => { + // .membership is undefined in our member by default + const { result } = renderKickButtonHook(); + expect(result.current.canUserBeKicked).toBe(false); + }); + + it("renders something if member.membership is 'invite' or 'join'", () => { + let props = { + ...defaultAdminToolsProps, + member: memberWithInviteMembership, + }; + const { result } = renderKickButtonHook(props); + expect(result.current.canUserBeKicked).toBe(true); + + cleanup(); + + props = { + ...defaultAdminToolsProps, + member: memberWithJoinMembership, + }; + const { result: result2 } = renderKickButtonHook(props); + expect(result2.current.canUserBeKicked).toBe(true); + }); + + it("renders the correct label", () => { + // test for room + const propsWithJoinMembership = { + ...defaultAdminToolsProps, + member: memberWithJoinMembership, + }; + + const { result } = renderKickButtonHook(propsWithJoinMembership); + expect(result.current.kickLabel).toBe("Remove from room"); + cleanup(); + + const propsWithInviteMembership = { + ...defaultAdminToolsProps, + member: memberWithInviteMembership, + }; + + const { result: result2 } = renderKickButtonHook(propsWithInviteMembership); + expect(result2.current.kickLabel).toBe("Disinvite from room"); + cleanup(); + }); + + it("renders the correct label for space", () => { + const propsWithInviteMembership = { + ...defaultAdminToolsProps, + room: mockSpace, + member: memberWithInviteMembership, + }; + + const propsWithJoinMembership = { + ...defaultAdminToolsProps, + room: mockSpace, + member: memberWithJoinMembership, + }; + + const { result: result3 } = renderKickButtonHook(propsWithJoinMembership); + expect(result3.current.kickLabel).toBe("Remove from space"); + cleanup(); + + const { result: result4 } = renderKickButtonHook(propsWithInviteMembership); + expect(result4.current.kickLabel).toBe("Disinvite from space"); + cleanup(); + }); + + it("clicking the kick button calls Modal.createDialog with the correct arguments when room is a space", async () => { + createDialogSpy.mockReturnValueOnce({ finished: Promise.resolve([]), close: jest.fn() }); + + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); + jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient); + + const propsWithInviteMembership = { + ...defaultAdminToolsProps, + room: mockSpace, + member: memberWithInviteMembership, + }; + const { result } = renderKickButtonHook(propsWithInviteMembership); + + await result.current.onKickClick(); + + // check the last call arguments and the presence of the spaceChildFilter callback + expect(createDialogSpy).toHaveBeenLastCalledWith( + expect.any(Function), + expect.objectContaining({ spaceChildFilter: expect.any(Function) }), + "mx_ConfirmSpaceUserActionDialog_wrapper", + ); + + // test the spaceChildFilter callback + const callback = createDialogSpy.mock.lastCall[1].spaceChildFilter; + + // make dummy values for myMember and theirMember, then we will test + // null vs their member followed by + // my member vs their member + const mockMyMember = { powerLevel: 1 }; + const mockTheirMember = { membership: KnownMembership.Invite, powerLevel: 0 }; + + const mockRoom = { + getMember: jest + .fn() + .mockReturnValueOnce(null) + .mockReturnValueOnce(mockTheirMember) + .mockReturnValueOnce(mockMyMember) + .mockReturnValueOnce(mockTheirMember), + currentState: { + hasSufficientPowerLevelFor: jest.fn().mockReturnValue(true), + }, + }; + + expect(callback(mockRoom)).toBe(false); + expect(callback(mockRoom)).toBe(true); + }); +}); diff --git a/test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel-test.tsx b/test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel-test.tsx new file mode 100644 index 0000000000..5ebd537855 --- /dev/null +++ b/test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel-test.tsx @@ -0,0 +1,230 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { renderHook } from "jest-matrix-react"; +import { type Mocked, mocked } from "jest-mock"; +import { + type Room, + type MatrixClient, + RoomMember, + type MatrixEvent, + type ISendEventResponse, +} from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; + +import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg"; +import { type RoomAdminToolsProps } from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel"; +import { useMuteButtonViewModel } from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel"; +import { isMuted } from "../../../../../../../src/components/views/right_panel/UserInfo"; +import { withClientContextRenderOptions } from "../../../../../../test-utils"; + +describe("useMuteButtonViewModel", () => { + const defaultRoomId = "!fkfk"; + const defaultUserId = "@user:example.com"; + + let mockRoom: Mocked; + let mockClient: Mocked; + + const defaultMember = new RoomMember(defaultRoomId, defaultUserId); + + let defaultAdminToolsProps: RoomAdminToolsProps; + + beforeEach(() => { + mockRoom = mocked({ + roomId: defaultRoomId, + getType: jest.fn().mockReturnValue(undefined), + isSpaceRoom: jest.fn().mockReturnValue(false), + getMember: jest.fn().mockReturnValue(undefined), + getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"), + name: "test room", + on: jest.fn(), + off: jest.fn(), + currentState: { + getStateEvents: jest.fn(), + on: jest.fn(), + off: jest.fn(), + }, + getEventReadUpTo: jest.fn(), + } as unknown as Room); + + defaultAdminToolsProps = { + room: mockRoom, + member: defaultMember, + isUpdating: false, + startUpdating: jest.fn(), + stopUpdating: jest.fn(), + }; + + mockClient = mocked({ + getUser: jest.fn(), + isGuest: jest.fn().mockReturnValue(false), + isUserIgnored: jest.fn(), + getIgnoredUsers: jest.fn(), + setIgnoredUsers: jest.fn(), + getUserId: jest.fn().mockReturnValue(defaultUserId), + getSafeUserId: jest.fn(), + getDomain: jest.fn(), + on: jest.fn(), + off: jest.fn(), + isSynapseAdministrator: jest.fn().mockResolvedValue(false), + doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false), + doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false), + getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")), + mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"), + removeListener: jest.fn(), + currentState: { + on: jest.fn(), + }, + getRoom: jest.fn(), + credentials: {}, + setPowerLevel: jest.fn(), + } as unknown as MatrixClient); + + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); + jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient); + + mockClient.setPowerLevel.mockImplementation(() => Promise.resolve({} as ISendEventResponse)); + + mockRoom.currentState.getStateEvents.mockReturnValueOnce({ + getContent: jest.fn().mockReturnValue({ + events: { + "m.room.message": 0, + }, + events_default: 0, + }), + } as unknown as MatrixEvent); + + jest.spyOn(mockClient, "setPowerLevel").mockImplementation(() => Promise.resolve({} as ISendEventResponse)); + jest.spyOn(mockRoom.currentState, "getStateEvents").mockReturnValue({ + getContent: jest.fn().mockReturnValue({ + events: { + "m.room.message": 0, + }, + events_default: 0, + }), + } as unknown as MatrixEvent); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const renderMuteButtonHook = (props = defaultAdminToolsProps) => { + return renderHook(() => useMuteButtonViewModel(props), withClientContextRenderOptions(mockClient)); + }; + + it("should early return when isUpdating=true", async () => { + const defaultMemberWithPowerLevelAndJoinMembership = { + ...defaultMember, + powerLevel: 0, + membership: KnownMembership.Join, + } as RoomMember; + + const { result } = renderMuteButtonHook({ + ...defaultAdminToolsProps, + member: defaultMemberWithPowerLevelAndJoinMembership, + isUpdating: true, + }); + + const resultClick = await result.current.onMuteButtonClick(); + + expect(resultClick).toBe(undefined); + }); + + it("should stop updating when level is NaN", async () => { + const { result } = renderMuteButtonHook({ + ...defaultAdminToolsProps, + member: defaultMember, + isUpdating: false, + }); + + jest.spyOn(mockRoom.currentState, "getStateEvents").mockReturnValueOnce({ + getContent: jest.fn().mockReturnValue({ + events: { + "m.room.message": NaN, + }, + events_default: NaN, + }), + } as unknown as MatrixEvent); + + await result.current.onMuteButtonClick(); + + expect(defaultAdminToolsProps.stopUpdating).toHaveBeenCalled(); + }); + + it("should set powerlevel to default when user is muted", async () => { + const defaultMutedMember = { + ...defaultMember, + powerLevel: -1, + membership: KnownMembership.Join, + } as RoomMember; + + const { result } = renderMuteButtonHook({ + ...defaultAdminToolsProps, + member: defaultMutedMember, + isUpdating: false, + }); + + await result.current.onMuteButtonClick(); + + expect(mockClient.setPowerLevel).toHaveBeenCalledWith(mockRoom.roomId, defaultMember.userId, 0); + }); + + it("should set powerlevel - 1 when user is unmuted", async () => { + const defaultUnmutedMember = { + ...defaultMember, + powerLevel: 0, + membership: KnownMembership.Join, + } as RoomMember; + + const { result } = renderMuteButtonHook({ + ...defaultAdminToolsProps, + member: defaultUnmutedMember, + isUpdating: false, + }); + + await result.current.onMuteButtonClick(); + + expect(mockClient.setPowerLevel).toHaveBeenCalledWith(mockRoom.roomId, defaultMember.userId, -1); + }); + + it("returns false if either argument is falsy", () => { + // @ts-ignore to let us purposely pass incorrect args + expect(isMuted(defaultMember, null)).toBe(false); + // @ts-ignore to let us purposely pass incorrect args + expect(isMuted(null, {})).toBe(false); + }); + + it("when powerLevelContent.events and .events_default are undefined, returns false", () => { + const powerLevelContents = {}; + expect(isMuted(defaultMember, powerLevelContents)).toBe(false); + }); + + it("when powerLevelContent.events is undefined, uses .events_default", () => { + const higherPowerLevelContents = { events_default: 10 }; + expect(isMuted(defaultMember, higherPowerLevelContents)).toBe(true); + + const lowerPowerLevelContents = { events_default: -10 }; + expect(isMuted(defaultMember, lowerPowerLevelContents)).toBe(false); + }); + + it("when powerLevelContent.events is defined but '.m.room.message' isn't, uses .events_default", () => { + const higherPowerLevelContents = { events: {}, events_default: 10 }; + expect(isMuted(defaultMember, higherPowerLevelContents)).toBe(true); + + const lowerPowerLevelContents = { events: {}, events_default: -10 }; + expect(isMuted(defaultMember, lowerPowerLevelContents)).toBe(false); + }); + + it("when powerLevelContent.events and '.m.room.message' are defined, uses the value", () => { + const higherPowerLevelContents = { events: { "m.room.message": -10 }, events_default: 10 }; + expect(isMuted(defaultMember, higherPowerLevelContents)).toBe(false); + + const lowerPowerLevelContents = { events: { "m.room.message": 10 }, events_default: -10 }; + expect(isMuted(defaultMember, lowerPowerLevelContents)).toBe(true); + }); +}); diff --git a/test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel-test.tsx b/test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel-test.tsx new file mode 100644 index 0000000000..cb3187a82c --- /dev/null +++ b/test/unit-tests/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel-test.tsx @@ -0,0 +1,98 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { renderHook } from "jest-matrix-react"; +import { type Mocked, mocked } from "jest-mock"; +import { type Room, type MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix"; + +import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg"; +import { useRedactMessagesButtonViewModel } from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel"; +import Modal from "../../../../../../../src/Modal"; +import BulkRedactDialog from "../../../../../../../src/components/views/dialogs/BulkRedactDialog"; +import { withClientContextRenderOptions } from "../../../../../../test-utils"; + +describe("useRedactMessagesButtonViewModel", () => { + const defaultRoomId = "!fkfk"; + const defaultUserId = "@user:example.com"; + + let mockRoom: Mocked; + let mockClient: Mocked; + + const defaultMember = new RoomMember(defaultRoomId, defaultUserId); + + beforeEach(() => { + mockRoom = mocked({ + roomId: defaultRoomId, + getType: jest.fn().mockReturnValue(undefined), + isSpaceRoom: jest.fn().mockReturnValue(false), + getMember: jest.fn().mockReturnValue(undefined), + getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"), + name: "test room", + on: jest.fn(), + off: jest.fn(), + currentState: { + getStateEvents: jest.fn(), + on: jest.fn(), + off: jest.fn(), + }, + getEventReadUpTo: jest.fn(), + } as unknown as Room); + + mockClient = mocked({ + getUser: jest.fn(), + isGuest: jest.fn().mockReturnValue(false), + isUserIgnored: jest.fn(), + getIgnoredUsers: jest.fn(), + setIgnoredUsers: jest.fn(), + getUserId: jest.fn().mockReturnValue(defaultUserId), + getSafeUserId: jest.fn(), + getDomain: jest.fn(), + on: jest.fn(), + off: jest.fn(), + isSynapseAdministrator: jest.fn().mockResolvedValue(false), + doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false), + doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false), + getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")), + mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"), + removeListener: jest.fn(), + currentState: { + on: jest.fn(), + }, + getRoom: jest.fn(), + credentials: {}, + setPowerLevel: jest.fn(), + } as unknown as MatrixClient); + + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); + jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient); + }); + + const renderRedactButtonHook = (props = defaultMember) => { + return renderHook(() => useRedactMessagesButtonViewModel(props), withClientContextRenderOptions(mockClient)); + }; + + it("should show BulkRedactDialog upon clicking the Remove messages button", async () => { + const spy = jest.spyOn(Modal, "createDialog"); + + mockClient.getRoom.mockReturnValue(mockRoom); + mockClient.getUserId.mockReturnValue("@arbitraryId:server"); + const mockMeMember = new RoomMember(mockRoom.roomId, mockClient.getUserId()!); + mockMeMember.powerLevel = 51; // defaults to 50 + const defaultMemberWithPowerLevel = { ...defaultMember, powerLevel: 0 } as RoomMember; + mockRoom.getMember.mockImplementation((userId) => + userId === mockClient.getUserId() ? mockMeMember : defaultMemberWithPowerLevel, + ); + + const { result } = renderRedactButtonHook(); + await result.current.onRedactAllMessagesClick(); + + expect(spy).toHaveBeenCalledWith( + BulkRedactDialog, + expect.objectContaining({ member: defaultMemberWithPowerLevel }), + ); + }); +}); diff --git a/test/unit-tests/components/views/right_panel/UserInfo-test.tsx b/test/unit-tests/components/views/right_panel/UserInfo-test.tsx index 1a58b1e2e0..ce0edca0ac 100644 --- a/test/unit-tests/components/views/right_panel/UserInfo-test.tsx +++ b/test/unit-tests/components/views/right_panel/UserInfo-test.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { fireEvent, render, screen, cleanup, act, waitForElementToBeRemoved, waitFor } from "jest-matrix-react"; +import { fireEvent, render, screen, act, waitForElementToBeRemoved, waitFor } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; import { type Mocked, mocked } from "jest-mock"; import { @@ -19,7 +19,6 @@ import { EventType, Device, } from "matrix-js-sdk/src/matrix"; -import { KnownMembership } from "matrix-js-sdk/src/types"; import { EventEmitter } from "events"; import { UserVerificationStatus, @@ -30,13 +29,9 @@ import { } from "matrix-js-sdk/src/crypto-api"; import UserInfo, { - BanToggleButton, disambiguateDevices, getPowerLevels, - isMuted, PowerLevelEditor, - RoomAdminToolsContainer, - RoomKickButton, UserInfoHeader, UserOptionsSection, } from "../../../../../src/components/views/right_panel/UserInfo"; @@ -53,7 +48,6 @@ import { shouldShowComponent } from "../../../../../src/customisations/helpers/U import { UIComponent } from "../../../../../src/settings/UIFeature"; import { Action } from "../../../../../src/dispatcher/actions"; import { ShareDialog } from "../../../../../src/components/views/dialogs/ShareDialog"; -import BulkRedactDialog from "../../../../../src/components/views/dialogs/BulkRedactDialog"; jest.mock("../../../../../src/utils/direct-messages", () => ({ ...jest.requireActual("../../../../../src/utils/direct-messages"), @@ -92,7 +86,6 @@ const defaultUserId = "@user:example.com"; const defaultUser = new User(defaultUserId); let mockRoom: Mocked; -let mockSpace: Mocked; let mockClient: Mocked; let mockCrypto: Mocked; const origDate = global.Date.prototype.toLocaleString; @@ -115,23 +108,6 @@ beforeEach(() => { getEventReadUpTo: jest.fn(), } as unknown as Room); - mockSpace = mocked({ - roomId: defaultRoomId, - getType: jest.fn().mockReturnValue("m.space"), - isSpaceRoom: jest.fn().mockReturnValue(true), - getMember: jest.fn().mockReturnValue(undefined), - getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"), - name: "test room", - on: jest.fn(), - off: jest.fn(), - currentState: { - getStateEvents: jest.fn(), - on: jest.fn(), - off: jest.fn(), - }, - getEventReadUpTo: jest.fn(), - } as unknown as Room); - mockCrypto = mocked({ getDeviceVerificationStatus: jest.fn(), getUserDeviceInfo: jest.fn(), @@ -800,384 +776,6 @@ describe("", () => { }); }); -describe("", () => { - const defaultMember = new RoomMember(defaultRoomId, defaultUserId); - const memberWithInviteMembership = { ...defaultMember, membership: KnownMembership.Invite }; - const memberWithJoinMembership = { ...defaultMember, membership: KnownMembership.Join }; - - let defaultProps: Parameters[0]; - beforeEach(() => { - defaultProps = { - room: mockRoom, - member: defaultMember, - startUpdating: jest.fn(), - stopUpdating: jest.fn(), - isUpdating: false, - }; - }); - - const renderComponent = (props = {}) => { - const Wrapper = (wrapperProps = {}) => { - return ; - }; - - return render(, { - wrapper: Wrapper, - }); - }; - - const createDialogSpy: jest.SpyInstance = jest.spyOn(Modal, "createDialog"); - - afterEach(() => { - createDialogSpy.mockReset(); - }); - - it("renders nothing if member.membership is undefined", () => { - // .membership is undefined in our member by default - const { container } = renderComponent(); - expect(container).toBeEmptyDOMElement(); - }); - - it("renders something if member.membership is 'invite' or 'join'", () => { - let result = renderComponent({ member: memberWithInviteMembership }); - expect(result.container).not.toBeEmptyDOMElement(); - - cleanup(); - - result = renderComponent({ member: memberWithJoinMembership }); - expect(result.container).not.toBeEmptyDOMElement(); - }); - - it("renders the correct label", () => { - // test for room - renderComponent({ member: memberWithJoinMembership }); - expect(screen.getByText(/remove from room/i)).toBeInTheDocument(); - cleanup(); - - renderComponent({ member: memberWithInviteMembership }); - expect(screen.getByText(/disinvite from room/i)).toBeInTheDocument(); - cleanup(); - - // test for space - mockRoom.isSpaceRoom.mockReturnValue(true); - renderComponent({ member: memberWithJoinMembership }); - expect(screen.getByText(/remove from space/i)).toBeInTheDocument(); - cleanup(); - - renderComponent({ member: memberWithInviteMembership }); - expect(screen.getByText(/disinvite from space/i)).toBeInTheDocument(); - cleanup(); - mockRoom.isSpaceRoom.mockReturnValue(false); - }); - - it("clicking the kick button calls Modal.createDialog with the correct arguments", async () => { - createDialogSpy.mockReturnValueOnce({ finished: Promise.resolve([]), close: jest.fn() }); - - renderComponent({ room: mockSpace, member: memberWithInviteMembership }); - await userEvent.click(screen.getByText(/disinvite from/i)); - - // check the last call arguments and the presence of the spaceChildFilter callback - expect(createDialogSpy).toHaveBeenLastCalledWith( - expect.any(Function), - expect.objectContaining({ spaceChildFilter: expect.any(Function) }), - "mx_ConfirmSpaceUserActionDialog_wrapper", - ); - - // test the spaceChildFilter callback - const callback = createDialogSpy.mock.lastCall[1].spaceChildFilter; - - // make dummy values for myMember and theirMember, then we will test - // null vs their member followed by - // my member vs their member - const mockMyMember = { powerLevel: 1 }; - const mockTheirMember = { membership: KnownMembership.Invite, powerLevel: 0 }; - - const mockRoom = { - getMember: jest - .fn() - .mockReturnValueOnce(null) - .mockReturnValueOnce(mockTheirMember) - .mockReturnValueOnce(mockMyMember) - .mockReturnValueOnce(mockTheirMember), - currentState: { - hasSufficientPowerLevelFor: jest.fn().mockReturnValue(true), - }, - }; - - expect(callback(mockRoom)).toBe(false); - expect(callback(mockRoom)).toBe(true); - }); -}); - -describe("", () => { - const defaultMember = new RoomMember(defaultRoomId, defaultUserId); - const memberWithBanMembership = { ...defaultMember, membership: KnownMembership.Ban }; - let defaultProps: Parameters[0]; - beforeEach(() => { - defaultProps = { - room: mockRoom, - member: defaultMember, - startUpdating: jest.fn(), - stopUpdating: jest.fn(), - isUpdating: false, - }; - }); - - const renderComponent = (props = {}) => { - const Wrapper = (wrapperProps = {}) => { - return ; - }; - - return render(, { - wrapper: Wrapper, - }); - }; - - const createDialogSpy: jest.SpyInstance = jest.spyOn(Modal, "createDialog"); - - afterEach(() => { - createDialogSpy.mockReset(); - }); - - it("renders the correct labels for banned and unbanned members", () => { - // test for room - // defaultMember is not banned - renderComponent(); - expect(screen.getByText("Ban from room")).toBeInTheDocument(); - cleanup(); - - renderComponent({ member: memberWithBanMembership }); - expect(screen.getByText("Unban from room")).toBeInTheDocument(); - cleanup(); - - // test for space - mockRoom.isSpaceRoom.mockReturnValue(true); - renderComponent(); - expect(screen.getByText("Ban from space")).toBeInTheDocument(); - cleanup(); - - renderComponent({ member: memberWithBanMembership }); - expect(screen.getByText("Unban from space")).toBeInTheDocument(); - cleanup(); - mockRoom.isSpaceRoom.mockReturnValue(false); - }); - - it("clicking the ban or unban button calls Modal.createDialog with the correct arguments if user is not banned", async () => { - createDialogSpy.mockReturnValueOnce({ finished: Promise.resolve([]), close: jest.fn() }); - - renderComponent({ room: mockSpace }); - await userEvent.click(screen.getByText(/ban from/i)); - - // check the last call arguments and the presence of the spaceChildFilter callback - expect(createDialogSpy).toHaveBeenLastCalledWith( - expect.any(Function), - expect.objectContaining({ spaceChildFilter: expect.any(Function) }), - "mx_ConfirmSpaceUserActionDialog_wrapper", - ); - - // test the spaceChildFilter callback - const callback = createDialogSpy.mock.lastCall[1].spaceChildFilter; - - // make dummy values for myMember and theirMember, then we will test - // null vs their member followed by - // truthy my member vs their member - const mockMyMember = { powerLevel: 1 }; - const mockTheirMember = { membership: "is not ban", powerLevel: 0 }; - - const mockRoom = { - getMember: jest - .fn() - .mockReturnValueOnce(null) - .mockReturnValueOnce(mockTheirMember) - .mockReturnValueOnce(mockMyMember) - .mockReturnValueOnce(mockTheirMember), - currentState: { - hasSufficientPowerLevelFor: jest.fn().mockReturnValue(true), - }, - }; - - expect(callback(mockRoom)).toBe(false); - expect(callback(mockRoom)).toBe(true); - }); - - it("clicking the ban or unban button calls Modal.createDialog with the correct arguments if user _is_ banned", async () => { - createDialogSpy.mockReturnValueOnce({ finished: Promise.resolve([]), close: jest.fn() }); - - renderComponent({ room: mockSpace, member: memberWithBanMembership }); - await userEvent.click(screen.getByText(/ban from/i)); - - // check the last call arguments and the presence of the spaceChildFilter callback - expect(createDialogSpy).toHaveBeenLastCalledWith( - expect.any(Function), - expect.objectContaining({ spaceChildFilter: expect.any(Function) }), - "mx_ConfirmSpaceUserActionDialog_wrapper", - ); - - // test the spaceChildFilter callback - const callback = createDialogSpy.mock.lastCall[1].spaceChildFilter; - - // make dummy values for myMember and theirMember, then we will test - // null vs their member followed by - // my member vs their member - const mockMyMember = { powerLevel: 1 }; - const mockTheirMember = { membership: KnownMembership.Ban, powerLevel: 0 }; - - const mockRoom = { - getMember: jest - .fn() - .mockReturnValueOnce(null) - .mockReturnValueOnce(mockTheirMember) - .mockReturnValueOnce(mockMyMember) - .mockReturnValueOnce(mockTheirMember), - currentState: { - hasSufficientPowerLevelFor: jest.fn().mockReturnValue(true), - }, - }; - - expect(callback(mockRoom)).toBe(false); - expect(callback(mockRoom)).toBe(true); - }); -}); - -describe("", () => { - const defaultMember = new RoomMember(defaultRoomId, defaultUserId); - defaultMember.membership = KnownMembership.Invite; - - let defaultProps: Parameters[0]; - beforeEach(() => { - defaultProps = { - room: mockRoom, - member: defaultMember, - isUpdating: false, - startUpdating: jest.fn(), - stopUpdating: jest.fn(), - powerLevels: {}, - }; - }); - - const renderComponent = (props = {}) => { - const Wrapper = (wrapperProps = {}) => { - return ; - }; - - return render(, { - wrapper: Wrapper, - }); - }; - - it("returns a single empty div if room.getMember is falsy", () => { - const { asFragment } = renderComponent(); - expect(asFragment()).toMatchInlineSnapshot(` - -
- - `); - }); - - it("can return a single empty div in case where room.getMember is not falsy", () => { - mockRoom.getMember.mockReturnValueOnce(defaultMember); - const { asFragment } = renderComponent(); - expect(asFragment()).toMatchInlineSnapshot(` - -
- - `); - }); - - it("returns kick, redact messages, ban buttons if conditions met", () => { - const mockMeMember = new RoomMember(mockRoom.roomId, "arbitraryId"); - mockMeMember.powerLevel = 51; // defaults to 50 - mockRoom.getMember.mockReturnValueOnce(mockMeMember); - - const defaultMemberWithPowerLevel = { ...defaultMember, powerLevel: 0 }; - - renderComponent({ member: defaultMemberWithPowerLevel }); - - expect(screen.getByRole("button", { name: "Disinvite from room" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Ban from room" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Remove messages" })).toBeInTheDocument(); - }); - - it("should show BulkRedactDialog upon clicking the Remove messages button", async () => { - const spy = jest.spyOn(Modal, "createDialog"); - - mockClient.getRoom.mockReturnValue(mockRoom); - mockClient.getUserId.mockReturnValue("@arbitraryId:server"); - const mockMeMember = new RoomMember(mockRoom.roomId, mockClient.getUserId()!); - mockMeMember.powerLevel = 51; // defaults to 50 - const defaultMemberWithPowerLevel = { ...defaultMember, powerLevel: 0 } as RoomMember; - mockRoom.getMember.mockImplementation((userId) => - userId === mockClient.getUserId() ? mockMeMember : defaultMemberWithPowerLevel, - ); - - renderComponent({ member: defaultMemberWithPowerLevel }); - await userEvent.click(screen.getByRole("button", { name: "Remove messages" })); - - expect(spy).toHaveBeenCalledWith( - BulkRedactDialog, - expect.objectContaining({ member: defaultMemberWithPowerLevel }), - ); - }); - - it("returns mute toggle button if conditions met", () => { - const mockMeMember = new RoomMember(mockRoom.roomId, "arbitraryId"); - mockMeMember.powerLevel = 51; // defaults to 50 - mockRoom.getMember.mockReturnValueOnce(mockMeMember); - - const defaultMemberWithPowerLevelAndJoinMembership = { - ...defaultMember, - powerLevel: 0, - membership: KnownMembership.Join, - }; - - renderComponent({ - member: defaultMemberWithPowerLevelAndJoinMembership, - powerLevels: { events: { "m.room.power_levels": 1 } }, - }); - - const button = screen.getByText(/mute/i); - expect(button).toBeInTheDocument(); - fireEvent.click(button); - expect(defaultProps.startUpdating).toHaveBeenCalled(); - }); - - it("should disable buttons when isUpdating=true", () => { - const mockMeMember = new RoomMember(mockRoom.roomId, "arbitraryId"); - mockMeMember.powerLevel = 51; // defaults to 50 - mockRoom.getMember.mockReturnValueOnce(mockMeMember); - - const defaultMemberWithPowerLevelAndJoinMembership = { - ...defaultMember, - powerLevel: 0, - membership: KnownMembership.Join, - }; - - renderComponent({ - member: defaultMemberWithPowerLevelAndJoinMembership, - powerLevels: { events: { "m.room.power_levels": 1 } }, - isUpdating: true, - }); - - const button = screen.getByRole("button", { name: "Mute" }); - expect(button).toBeInTheDocument(); - expect(button).toBeDisabled(); - }); - - it("should not show mute button for one's own member", () => { - const mockMeMember = new RoomMember(mockRoom.roomId, mockClient.getSafeUserId()); - mockMeMember.powerLevel = 51; // defaults to 50 - mockRoom.getMember.mockReturnValueOnce(mockMeMember); - - renderComponent({ - member: mockMeMember, - powerLevels: { events: { "m.room.power_levels": 100 } }, - }); - - const button = screen.queryByText(/mute/i); - expect(button).not.toBeInTheDocument(); - }); -}); - describe("disambiguateDevices", () => { it("does not add ambiguous key to unique names", () => { const initialDevices = [ @@ -1217,47 +815,6 @@ describe("disambiguateDevices", () => { }); }); -describe("isMuted", () => { - // this member has a power level of 0 - const isMutedMember = new RoomMember(defaultRoomId, defaultUserId); - - it("returns false if either argument is falsy", () => { - // @ts-ignore to let us purposely pass incorrect args - expect(isMuted(isMutedMember, null)).toBe(false); - // @ts-ignore to let us purposely pass incorrect args - expect(isMuted(null, {})).toBe(false); - }); - - it("when powerLevelContent.events and .events_default are undefined, returns false", () => { - const powerLevelContents = {}; - expect(isMuted(isMutedMember, powerLevelContents)).toBe(false); - }); - - it("when powerLevelContent.events is undefined, uses .events_default", () => { - const higherPowerLevelContents = { events_default: 10 }; - expect(isMuted(isMutedMember, higherPowerLevelContents)).toBe(true); - - const lowerPowerLevelContents = { events_default: -10 }; - expect(isMuted(isMutedMember, lowerPowerLevelContents)).toBe(false); - }); - - it("when powerLevelContent.events is defined but '.m.room.message' isn't, uses .events_default", () => { - const higherPowerLevelContents = { events: {}, events_default: 10 }; - expect(isMuted(isMutedMember, higherPowerLevelContents)).toBe(true); - - const lowerPowerLevelContents = { events: {}, events_default: -10 }; - expect(isMuted(isMutedMember, lowerPowerLevelContents)).toBe(false); - }); - - it("when powerLevelContent.events and '.m.room.message' are defined, uses the value", () => { - const higherPowerLevelContents = { events: { "m.room.message": -10 }, events_default: 10 }; - expect(isMuted(isMutedMember, higherPowerLevelContents)).toBe(false); - - const lowerPowerLevelContents = { events: { "m.room.message": 10 }, events_default: -10 }; - expect(isMuted(isMutedMember, lowerPowerLevelContents)).toBe(true); - }); -}); - describe("getPowerLevels", () => { it("returns an empty object when room.currentState.getStateEvents return null", () => { mockRoom.currentState.getStateEvents.mockReturnValueOnce(null); diff --git a/test/unit-tests/components/views/right_panel/UserInfoAdminToolsContainer-test.tsx b/test/unit-tests/components/views/right_panel/UserInfoAdminToolsContainer-test.tsx new file mode 100644 index 0000000000..30a4f78842 --- /dev/null +++ b/test/unit-tests/components/views/right_panel/UserInfoAdminToolsContainer-test.tsx @@ -0,0 +1,306 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import React from "react"; +import { render, screen, fireEvent } from "jest-matrix-react"; +import { type Room, type RoomMember } from "matrix-js-sdk/src/matrix"; +import { mocked } from "jest-mock"; + +import { UserInfoAdminToolsContainer } from "../../../../../src/components/views/right_panel/user_info/UserInfoAdminToolsContainer"; +import { useUserInfoAdminToolsContainerViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel"; +import { useRoomKickButtonViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel"; +import { useBanButtonViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel"; +import { useMuteButtonViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel"; +import { useRedactMessagesButtonViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel"; +import { stubClient } from "../../../../test-utils"; +import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; + +jest.mock("../../../../../src/utils/DMRoomMap", () => { + const mock = { + getUserIdForRoomId: jest.fn(), + getDMRoomsForUserId: jest.fn(), + }; + + return { + shared: jest.fn().mockReturnValue(mock), + sharedInstance: mock, + }; +}); + +jest.mock( + "../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel", + () => ({ + useUserInfoAdminToolsContainerViewModel: jest.fn().mockReturnValue({ + isCurrentUserInTheRoom: true, + shouldShowKickButton: true, + shouldShowBanButton: true, + shouldShowMuteButton: true, + shouldShowRedactButton: true, + }), + }), +); + +jest.mock("../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel", () => ({ + useRoomKickButtonViewModel: jest.fn().mockReturnValue({ + canUserBeKicked: true, + kickLabel: "Kick", + onKickClick: jest.fn(), + }), +})); + +jest.mock("../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel", () => ({ + useBanButtonViewModel: jest.fn().mockReturnValue({ + banLabel: "Ban", + onBanOrUnbanClick: jest.fn(), + }), +})); + +jest.mock("../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel", () => ({ + useMuteButtonViewModel: jest.fn().mockReturnValue({ + isMemberInTheRoom: true, + muteLabel: "Mute", + onMuteButtonClick: jest.fn(), + }), +})); + +jest.mock("../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel", () => ({ + useRedactMessagesButtonViewModel: jest.fn().mockReturnValue({ + onRedactAllMessagesClick: jest.fn(), + }), +})); + +const defaultRoomId = "!fkfk"; + +describe("UserInfoAdminToolsContainer", () => { + // Setup it data + const mockRoom = mocked({ + roomId: defaultRoomId, + getType: jest.fn().mockReturnValue(undefined), + isSpaceRoom: jest.fn().mockReturnValue(false), + getMember: jest.fn().mockReturnValue(undefined), + getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"), + name: "test room", + on: jest.fn(), + off: jest.fn(), + currentState: { + getStateEvents: jest.fn(), + on: jest.fn(), + off: jest.fn(), + }, + getEventReadUpTo: jest.fn(), + } as unknown as Room); + + const mockMember = { + userId: "@user:example.com", + membership: "join", + powerLevel: 0, + } as unknown as RoomMember; + + const mockPowerLevels = { + users: { + "@currentuser:example.com": 100, + }, + events: {}, + state_default: 50, + ban: 50, + kick: 50, + redact: 50, + }; + + const defaultProps = { + room: mockRoom, + member: mockMember, + powerLevels: mockPowerLevels, + isUpdating: false, + startUpdating: jest.fn(), + stopUpdating: jest.fn(), + }; + + const mockMatrixClient = stubClient(); + + const renderComponent = (props = defaultProps) => { + return render( + + + , + ); + }; + + beforeEach(() => { + mocked(useUserInfoAdminToolsContainerViewModel).mockReturnValue({ + isCurrentUserInTheRoom: true, + shouldShowKickButton: true, + shouldShowBanButton: true, + shouldShowMuteButton: true, + shouldShowRedactButton: true, + }); + jest.clearAllMocks(); + }); + + it("renders all admin tools when user has permissions", () => { + renderComponent(); + + // Check that all buttons are rendered + expect(screen.getByText("Mute")).toBeInTheDocument(); + expect(screen.getByText("Kick")).toBeInTheDocument(); + expect(screen.getByText("Ban")).toBeInTheDocument(); + expect(screen.getByText("Remove messages")).toBeInTheDocument(); + }); + + it("renders no admin tools when current user is not in the room", () => { + mocked(useUserInfoAdminToolsContainerViewModel).mockReturnValue({ + isCurrentUserInTheRoom: false, + shouldShowKickButton: false, + shouldShowBanButton: false, + shouldShowMuteButton: false, + shouldShowRedactButton: false, + }); + + const { container } = renderComponent(); + + // Should render an empty div + expect(container.firstChild).toBeEmptyDOMElement(); + }); + + it("renders children when provided", () => { + render( + +
Custom Child
+
, + ); + + expect(screen.getByTestId("child-element")).toBeInTheDocument(); + expect(screen.getByText("Custom Child")).toBeInTheDocument(); + }); + + describe("Kick behavior", () => { + it("clicking kick button calls the appropriate handler", () => { + const mockedOnKickClick = jest.fn(); + mocked(useRoomKickButtonViewModel).mockReturnValue({ + canUserBeKicked: true, + kickLabel: "Kick", + onKickClick: mockedOnKickClick, + }); + renderComponent(); + + const kickButton = screen.getByText("Kick"); + fireEvent.click(kickButton); + + expect(mockedOnKickClick).toHaveBeenCalled(); + }); + + it("should not display kick buttun if user can't be kicked", () => { + mocked(useRoomKickButtonViewModel).mockReturnValue({ + canUserBeKicked: false, + kickLabel: "Kick", + onKickClick: jest.fn(), + }); + + renderComponent(); + + expect(screen.queryByText("Kick")).not.toBeInTheDocument(); + }); + + it("should display the correct label when user can be disinvited", () => { + mocked(useRoomKickButtonViewModel).mockReturnValue({ + canUserBeKicked: true, + kickLabel: "Disinvite", + onKickClick: jest.fn(), + }); + + renderComponent({ + ...defaultProps, + member: mockMember, + }); + + expect(screen.getByText("Disinvite")).toBeInTheDocument(); + }); + }); + + describe("Ban behavior", () => { + it("clicking ban button calls the appropriate handler", () => { + const mockedOnBanOrUnbanClick = jest.fn(); + mocked(useBanButtonViewModel).mockReturnValue({ + banLabel: "Ban", + onBanOrUnbanClick: mockedOnBanOrUnbanClick, + }); + renderComponent(); + + const banButton = screen.getByText("Ban"); + fireEvent.click(banButton); + + expect(mockedOnBanOrUnbanClick).toHaveBeenCalled(); + }); + + it("should display the correct label", () => { + const mockedOnBanOrUnbanClick = jest.fn(); + mocked(useBanButtonViewModel).mockReturnValue({ + banLabel: "Unban", + onBanOrUnbanClick: mockedOnBanOrUnbanClick, + }); + renderComponent(); + + // The label should be "Unban" + expect(screen.getByText("Unban")).toBeInTheDocument(); + }); + }); + + describe("Mute behavior", () => { + it("clicking mute button calls the appropriate handler", () => { + const mockedOnMuteButtonClick = jest.fn(); + mocked(useMuteButtonViewModel).mockReturnValue({ + isMemberInTheRoom: true, + muteLabel: "Mute", + onMuteButtonClick: mockedOnMuteButtonClick, + }); + renderComponent(); + + const muteButton = screen.getByText("Mute"); + fireEvent.click(muteButton); + + expect(mockedOnMuteButtonClick).toHaveBeenCalled(); + }); + + it("should not display mute button if user is not in the room", () => { + mocked(useMuteButtonViewModel).mockReturnValue({ + isMemberInTheRoom: false, + muteLabel: "Mute", + onMuteButtonClick: jest.fn(), + }); + + renderComponent(); + + expect(screen.queryByText("Mute")).not.toBeInTheDocument(); + }); + + it("should display the correct label", () => { + mocked(useMuteButtonViewModel).mockReturnValue({ + isMemberInTheRoom: true, + muteLabel: "Mute", + onMuteButtonClick: jest.fn(), + }); + renderComponent(); + + expect(screen.getByText("Mute")).toBeInTheDocument(); + }); + }); + + describe("Redact behavior", () => { + it("clicking redact button calls the appropriate handler", () => { + const mockedOnRedactAllMessagesClick = jest.fn(); + mocked(useRedactMessagesButtonViewModel).mockReturnValue({ + onRedactAllMessagesClick: mockedOnRedactAllMessagesClick, + }); + renderComponent(); + + const redactButton = screen.getByText("Remove messages"); + fireEvent.click(redactButton); + + expect(mockedOnRedactAllMessagesClick).toHaveBeenCalled(); + }); + }); +}); From 1e3fd9d3aad272c1d1010bc971da376630b93b76 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 13 Jun 2025 10:28:43 +0200 Subject: [PATCH 06/12] Update `IconButton` colors (#30124) * chore: update `@vector-im/compound-web` to 8.0.0 * refactor(IconButton): use `kind="secondary"` instead of `subtleBackground` props * test: update snapshots * fix: force color on room header toggle * fix: TAC button color * test(e2e): update release announcement screenshot --- package.json | 2 +- ...uncement-All-new-pinned-messages-linux.png | Bin 108170 -> 108181 bytes .../structures/_ThreadsActivityCentre.pcss | 4 +- res/css/views/rooms/_RoomHeader.pcss | 2 +- src/components/views/right_panel/BaseCard.tsx | 4 +- .../RoomListPanel/RoomListPrimaryFilters.tsx | 4 +- .../__snapshots__/FilePanel-test.tsx.snap | 3 +- .../__snapshots__/RoomView-test.tsx.snap | 84 ++++++++++++------ .../__snapshots__/ThreadPanel-test.tsx.snap | 6 +- .../__snapshots__/AppTile-test.tsx.snap | 3 +- .../__snapshots__/BaseCard-test.tsx.snap | 3 +- .../ExtensionsCard-test.tsx.snap | 6 +- .../PinnedMessagesCard-test.tsx.snap | 21 +++-- .../RoomSummaryCardView-test.tsx.snap | 15 ++-- .../__snapshots__/UserInfo-test.tsx.snap | 6 +- .../__snapshots__/RoomHeader-test.tsx.snap | 12 ++- .../VideoRoomChatButton-test.tsx.snap | 3 +- .../RoomListHeaderView-test.tsx.snap | 42 ++++++--- .../RoomListItemMenuView-test.tsx.snap | 12 ++- .../RoomListOptionsMenu-test.tsx.snap | 3 +- .../PinnedEventTile-test.tsx.snap | 6 +- .../ThirdPartyMemberInfo-test.tsx.snap | 6 +- .../ThemeChoicePanel-test.tsx.snap | 6 +- .../ChangeRecoveryKey-test.tsx.snap | 21 +++-- .../DeleteKeyStoragePanel-test.tsx.snap | 3 +- .../ResetIdentityPanel-test.tsx.snap | 12 ++- .../EncryptionUserSettingsTab-test.tsx.snap | 3 +- .../__snapshots__/SpacePanel-test.tsx.snap | 3 +- .../ThreadsActivityCentre-test.tsx.snap | 6 +- yarn.lock | 8 +- 30 files changed, 202 insertions(+), 107 deletions(-) diff --git a/package.json b/package.json index 4c15e5c7e7..c1a98219a3 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "@types/png-chunks-extract": "^1.0.2", "@types/react-virtualized": "^9.21.30", "@vector-im/compound-design-tokens": "^4.0.0", - "@vector-im/compound-web": "^7.11.0", + "@vector-im/compound-web": "^8.0.0", "@vector-im/matrix-wysiwyg": "2.38.3", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", diff --git a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-All-new-pinned-messages-linux.png b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-All-new-pinned-messages-linux.png index f466c17d6453f89cb5f809c9c20fae5e319f479d..78ba6e5cfd22a1833fb9d9535653de59c31bd994 100644 GIT binary patch delta 90362 zcmX_{1yG#9vZ#MN!4upe5Zom=1eXxp-3jh4KOw<2xVyW%ySux)yYo2b-utSiwzhW1 zr>A?Sr@x>P*q9O62tIHPIM?YNi6lEVg6yXi$_)yFSST-VeHaWb&w?)V}|TE?*ucY-ju zCz9&?pB0P`@rTRjUly243-O^2G^o%MaE{q{>1_~QbEh&-Zan>r*sk=ZS1{wkg)8r+ zCGYaI^h(rAP6;<}T^5gMKD-zIPr@Fo!UfEUUJeOhv&6AoXDnJc_yoJFm)y*ML~{%>u}!l?s*>RiSsFJV81 zi&&l%d0&p5NS>k}+Tey6$0bi!K00Pphv7Ij>u^9%`b7EZhAsa^`L~tM|LYCnY84h> zzJRl&a=kRaJWJJ8pa}1dJ_HU6Zpo@@5u!fWM|bX5MNG`WsdF#0 zE)zAUuqmqV^H9-`Qb5(JDD9 z1Eq=>HwE14$)uUD&;Etww6Q2xqHEay%qTJJT?kCO=K=p?k8NgszgXw@;yj>okR5v@p6?`i36$E6(qzme#j z7sIxDI;Pyj%TcLNav8z1o?+prSxGycq}ciTxY+fcd8f4h-vVF{Mi~Ak-yWPCnyDL8 zWR0zJs21m50u8@I#xfa1Q%y|{I9U9xIvTn~m#Uj@D$7rwn`_cOj;7964AQoS6hI7= z%=w_vBTDN>K64fIcKetKMr!`67pqD>#KS z#t2qNWzB-p!6NfgGs{oaY;yrX`*4_fFFR8WyVH4%E$F(S7yZ#bJ1OwLQ+%08%Er!) zSI$u&o?5%tTa#6{L*T;SG!gm>&Dhl9g)?K$7+~ygL?ndsjl1#H477iZ<8h8QH6`wcOpM9IOsxvxd?1zb7sIg)I{H%>S~fa>)4>s)Cu z&BuoNknw}>Ap$AQ<8L!czbO$#QK4O(kmP`3#ll4O#qN`o&0Az&NEJ$(uGbr3R64nS zLtLb+3C6Hb!Ciz#R%TKYrUzuE`AEQwu5VoK$ zSPx3KMQH7mRquTIjvObU0&#}%U4jkgv3US142eVBMBF5ek)NS-BR_vB7Lq@w%a1Ga z8A78<=ImmI_z>gzmi(l%RL0Ex77H4WiD~OW4MSim*OC{%mYrJ_dHG{$Rgm8xlM_L3B3c!i zda`u)r^V^`*T3GfM}@O}m8=6Rjn1YwOY2y(Di(Di3|FUv#@HJc0i^WyCq!G@PTmnoovd@8n zIy;IXA>h5h7jkT%P<_G+E`AZgJ05ck{vktAXHg*}u0@Wy5ocAp68JXn;&yrAk#5nTf7 zsvMtW*VoAH>@X-vU5KR3k)b%S9ds+5kgY`>=@3Mg66s{>{%$T*P+?oP@upGtf52JI zZqMKqQq8a*(G!66IFFWf@I8Fh@0 z-b)J2BaZa9Y^FmSWCQMs1CN=JCn34Zl_MXtq|ve0ZyuEJV(%LyS5I+pJZH6o5}i5l zFB+*m!ll)TCuWotJ8p@h50CqD@^PWrQIXjZ{{1&tDp~M18Z+ms;eD=rkwAemwUQl- zz?Xm64k5E=7zMG{{CU;vKc;TC5nEwaa^AS}S_%}Bcx!ODgYERF3p$|LNax>7k1j~$p58hXBtQBtpAmtBsqEG zQV_CVp*e^^e$9<}WpX$Svk=(>0+kB1f14}hHZcQ0I=Qupu)JIwY45w*JRCK*_WHCq zhuk8a;o&BvlTgk`qjHMAIR@4*S4i2g@NOCVeg~!RQuxdhSrj#gKc7?hap1XZ2RC0B zyOFF|N9MtFmBWzwjnAYkZ3%>1twKBnI=oaQOs6FWv@+JjO3cGdA@jnDLW|rWl8p~CXf)bBK^N=kR=Fr zGJ=OT&QnsI{ALW+@Gp^6FDpDa*( zF}Xa#0fxwelA}bRPDCnFkH6*8Kvvmf6EPN2=48DYSfx@UuQ4Wc%$hl({rRVKtifve z0~4TdIU;h$Zv9$xdTVa@zc>SM!HE&20K{HR`F8t0oR8MN4YfP zA>LRrf|cVj7*vcC4HoMkjnF{5exX9P*v1|Rva59_)o`0WRsV)Rli8Xf4K#&pd|Shk za@Gd|tWH(WciV#ZTQy!eDu?aGzhD}6Z*VYKOq9W9C~j~}ys7@;)f^k(zEf$;1ecga zoQn$2l$F-{N79&hEsEos^opjs5{MYkE=h=p{f7eDyhaiqMCN5x{t|!4B;phxTN&V0 za4Je~t!*svmHM)Pm*lE(mB;BW+OjQ{hkIED6)Jha_NYWPQ^ydkw6k19mH(cgJ`ap8 z^}eT$wb>&!EM}20?*yqY6Zs%|PkD%MY_)T35ZUUfbeAla4%6}_{z4c{ z9~ANXtBw@z{C4P%9mx3uI)l{$@ZhC5yG~f;Rxx6JxOjUGoG6deOhHo3DorfZC?gy= zvPNlKC=y#sAYeW3^8rm#U(BSPgw`n9Y5eVVJe|KBLW@S^3G~h+@9x5J@<{*&yjm%} z=l)d?&V$qpIs>MrQ*>kb1r-<30}u=i9JtNACWBTbPiJRrmLn@Vw7*c%SwDQfEaQrg ze|voNQAt6xABvPDxohm!QcybjJ(EXWhS1pN^5n5_a%BXpbAYLLv!w~U$mEYolH^eZ z=r@eZYNh$DeRTvu2vP6bZu znOe!7&H2Fi^BSJ-b6|%-!SXzCpfx>q(jgA%S<*Z-DJE- zhphNG;Vp2#89aInUGni$rt-DyaK6F8+Se8QN;cyR?<=)eA-&6MRfRgz&=+YX+cy?+ z%RHA17f{>zmfT3@6qv}~6N@}9mt=csAqQuDxz^PT(&e|Pa8c4(X0jBR%CuVP4uKbC?4@*2EXC%oPIN|4xN#$&L~k@ zzEMR2iR;%Ghaqf?V@D?#fa>ZRBjcU-L(J=?{v|t3-qS)xVU4M1=bP8-ViI1iFk)+? zcG)k6zBQ?359QT~z3ryKBOeB#yWv+Zo?oqLt2fAwz%q4Yz9O}jJzwzGlN}7D&Rt`A z|E)uw-f8HR8(GPt%fHykiGiA^-P^m>4Icl$+REByb1`*44BB(2)=KH zcxezo8+Bhgf@>~MZ@ijq4uu&g zQd6F~&vVkg8nO)n!VPPGpF{}qp#i;1E|5E~|3xITllLFJG*6jpA0Z*V`Ebyi;c7KN zks5qf?Tfcr25*Q0Kk|5tL6NF;ZUUN2ZSkvd(Y3Eo=^id=ggLlBy)P;fQyDbZ0_Vl? zGDT~}`aUHfzE5_zwZO~O20jzc@;S+CztT4Jmp9$aQVoI-g^k%LXq36|}W zj!G#kWC>|c@?J*e17xs2ltH@NAtLaenXifPC6|;mv$TTh$xYx#QU2RL7C?4F(DOA$ zXc{rZ&kcBry=GP1r{+6~KQjhnj1pfT;@0w7qAm<6`r@^sh&6-ZB>@ly)_McFl6V3>mO@1o4 z&2X>hg<2wTRYR-jEzu*hFgdHD&FgZauZaEiUPy59WEX-@scXNO^KmAYA=1n*huN^6 zb8%^wjn%ZR4Uz(}GQwXnXV&_*HI=s=YR_y}wMkUf03bG}ZdoBm1wSUPF|k9D&ao5J z=UqOv;ht&VduuiM{qzt?t9Hon=$bA5qY-7&&2a9bk6+zO9gMqJ4JD$W(E#4YdJsODAunmh*%*Z4&mnuq z8%JdQMW|5YBfLDxJZac>v^4w`UK9QjkU+Zaeq`o#R9d!4_KVB*9Cn_+wQPK?<%$=? zIR`P8aw>26LoV==< zvW{3qGyU73<)^8gnEe823u<7)$ersz-blNnC#f!I31X3 zCq`+?KNm!TGNZIME4CoirEtB{vG%%BnVYgA&Ah zPW7xeyVV1L2qWv{&yPY7o{J0*fi%Z*o7~$tD(cEPJ=!SZi#6(;f+*NA)PKavX<x zUPt7v$Wy%VZ^_qPu%?L$I!#=CB5bYYqzy7AS96r~he}@KWXzlGQDWYl~ks=2` zjFwif7vt9NHVlV12Z==;^pZKu2V+$R$0r`1&8f7K;lBT_d16qo0TwO%UROB?fT5^S zgA{9xzQ6(ibVIKkc2I1be-6b&JZ3mE()v%x6`64S-aM4y9-HN}U0IB8eYuLl_^4U@f3`;7 z+uhcB*>NQN-qWyF2A^VV&3`{dD+ZLu*K%&~i`9x;5aJzDqmfxYF-riLBp_a-6*^YT z4!I|$1;kI2I?tHveI#n=eDhzjh$%VA`xa)=-V^Z^Jhi*%-`Q?G>?a1D*ANktSr@`!(Wfdl%EGpo zAOLhaB*`S(j@QxTRxgy8pP(xi!)mO$bn0V}f5mSWZQd_21BR7iYos(kg4e0gzy-WI z`!TN?bUp^*-v&UBM!prm-~19+Ja!V8Bk6^5WWo&W^m~c3ey} zd1PeyaA9tIKVK0_*C)~fQm(bPsP#{kYL&$+=g6;U`Kgu26Ny}|{0p-e)l)v^3!5y}oAtXp0+7_vl$P;AB zS~_jE&4SJPCt>Iv24ADZ$>_?~Z^ox5jjam1_IC{Oq!CW%e5Dia58%wT5kBG`Jxq3A zc)i`bPv;_?Rm2asz=a18y=F{fuDbD-pgX5ESh*UirjOmXgH-mh!=X+jl7)Zs4KQ)Y z>=m}iPyEs-Z8FOcHr(&dbOu^K#o1Ip`4WYp!EJ(Vk)B!4Rt$AwGiKviXf6Tt)(5kn zQWJr)*RmitChumxH_4~_1kv`LPz2^y?Ny$8!>bpu2Dje!FXi#|mn#uhM^j_gFI1kB z?BPF2O^10?KyqOHr{~=X>JM0;i^}X~^4@-JZ#PxCNcE62-@|J&vuZCG;2@z{OWExP zfIZVAkcUyC|Kj>;3-bHE|C12EM?Fi5fnEm$P$4@HN-_Zn#H3U|AlI>L+)~@V0yfVk zEodVQ=sI*Wq3KoLDQ5hG)VF4Vk|9%MFm2rbK=$P<3=QiJ(i0__9)WcQo@!w>m z4LzCT>jJO&knSgxUziQ3b|lls=dV+l=wb}aMl%F>hn5$6|I)`n0zJmdG-$j(3%;}7 z@qvo+DJfa9km4HttXH<-O~(u}iRsEk0y$sgD1ivq8@IcXzw!%P+Uw`}qXl@ck}$q6 z^q62CotoohlN{4U4)A?%at8?p=fd2;uovga03W+DQpg<3D`FY=URPNaHxsS>!-b(( zJ=%@3caWmH=~`0&3RoE&>9h2yvt=L{ynMdDi;bD?hd5@#Xd%KtD=G+}p>9BLJCL7j zIExH#!}`TDt{nPkuTxr38q4&Zp8Drvb2&)2miKty;6_8=F9Nd%swvA-b0^hzBHdWD zF{0Ox#%(e;d^r;JGB3ggo|}=P{0D}Y@4dRRKB}R^`?A`#v<2^uR@WX|>-M)a_ICBX z8l1bmQI3{i)Lb_P3iL2Kp5^T{m2@hIaK4=*PiOT~Jk6i2HqVhBVZ2*#ZqqaFVDPQi z+mE}g#dXunb&OvHW6tb7!yZT2oW|G5$n2IT=8lPu?1I9$Azk={VtJ&2HGW%n|D1$^ zSnfJ!2kl0?P!n?&6J^ElaNn`26#l)D`TDgto~OBGH94+YpZ1*3wiGsO&Mu5g5npKp z39ZnSc9J>MVpKGj5Jh>M(*gp@!HdMCJ>r64Y3`_4@JX8+B7_O8b5B^t!4Se>7JtPn zeVoKbN@(=Ptke9WaRx%v5vE{ZFBrbamt(1Sh?|&$c6>-`szplt%&716{^{gU*~hH= zDqNM1qCHov&)#HY;sj1ov$Hvy^8iM${4yW}+1^5&KT+MK&UZb;2`LQ?MB7a_jjaAg zLHD&Jy}S{_M5EpnPDE77VH*G(;48xje~vZHQGdlY9_5>`Hx8-m1kM|ir~DP*Y4Dal z<_=)rMZi>1T&e-y!Gm^@3M^NHfAt|bMt|Gy{td5lQx$PCvr~UPaX*RAF&ED_Fd8&c zzJ4Et#fmxjs+N!xw8Rb4j=&MRPK-%C^#_=ScII;D>^z|mxVlg9!21fKcDJsDDXx~- z*%Gp_RNkRON#?*iJcr!yq~ImoannZ{oL(LN=^42{!h9opo+IpNG-6>x%fcyTNf~(@ zaa&f|SS$|b`QB91VUzee7USiBivQ-NQ(C_S+fbe<|Ka=I&ub5mV=%ts>Shn3AI2Bb zmZ-;6jJ&i26%rWhYB4|1E;SK%$tQZ#%Y>G5b$gzzu^vl3+}qsdq$vFW=&o19*y+iY zV7GUgMox8!+NY4a#zRq>A^4(%=h@S8pcL+u)9nMqaYGQ^Shy@RAHOHxocRh6@|3Oj z8|e@>apvPJiaUd_Pm)sy4zko_i^9d|@Q&U&)aVv>fl&krk(FhdsO_rl!pDHqH^v8f_w9DOT|C;ByU#CA0v z>VsM5JjrNjy}L?9r&2Ui1}$^wxc=e-5|KyAIe=n)pM>HE?qyMw9}XDWR^3^fD*L>D zG?P$RId5WA7@a3K3}xk=_i$D;$!J-_0?kxU)1_Gt_3qn;F+b+$k-LLr`f?Qcc+{1_ zqDYL#SuCBBq!TML>U#qdO{W&6_GF_UabgXdJ$VKwd(*u21tP3iSV6^*d&3~ha*7&w z0xD`QzN}g~4?x3+tP@o|WchC7#`Jlu;=BU}K8+Kzk z+wZZ*`pdNmljn>-TG$1_Wj>0}=czRqu37tfb;IJB!AL?;JvA#65jV~zu3j+Cfvb5}piDP@ zzSRA}->))FUxQ#%295}Ic-xa=o*B#~uD#Jm&ek8Fi`neG zm*8b6v>N+Tc$^R0qa=ugGUDA^!OzD=3?YE~#-xC{v_|>mLy)_to5$u9>lC_IPagoB zWHnJW9CiMhJ7p(^A_XIs8$kf8iK%J06RNe6Awhy5id)hDE*wpr8DbN z%aNgL3gz@{^PZR>n?1_MyILcNzar3@45JmuDVu!;P|(7D80fjtvp*;nzZ+<)e>Lyy zbVOLL2LQfQFe}iqZD*Sedfq1c3)RdbC>h}Y6*kshnX{EVC(&S>$`S)=iG^}JyboHt z+sr@kOW`L3z(rx!bN5fxeBykUdL+nOP^pbSxW|fV9P9J079JAsHa7E6u zEfz$I-vEYs&*Mv41sju%=JN!%(TFoqF>hl za4BzfajY+GH*c>eOFw5&$VbM-LZ2Q|j95aw^>|N}u0ocOJVSkI9q0HvBK5d_|7J10 z@r$=F`dlUb+*fv#T!`bMNsDQS5wvFt>oNr0!wv(qS72|;w<2$T;I{*;& zDZ=;(=x$gDz|mPH*gs=>u$*YY_T$>4@o6>eXey|89t5corcy!@;tRocDWZv4KRpK< zVaZ4q_Nh*7-^5_tz}E)PVI${6NkWKd`~*cx5{eQqzf#56`F{2utVqK?c6pOp$e{^i z$-DF?|D-(LeUS*nkJXKgw52#5m?h<#JUV_3L`UcF5-*(d$+m@d^G2FDp*5#0umZe8 z*jks_!YvERN-+h_t1+gI%f6~mcyiDk2sOr->z;Ss@o8e_+!TuS-fH$5U`(7qHG#;+ z^G{Oc{^mI|4>Ru%R!fVpD3H3aDHrD!g8pz>P=pqG=~IZz=@|iDZ02uVoR@5BxiELQ znkfW+o3}d(rP9lLBYE#vsZDC5{a0)EmBslC6aVKZB3q-;sI9b6Et5;t%G^!`-}Rj% z?Dd0#$UDP2Nq*k;#@Y^92YByLQLuuwVDxD*qow7K9yN$9Zf%4GH$ht?zJCl!>5|gELMmi7$MqiY$sx>6WwghJ(Y}1vk*m{N3L?2IZ=c( z4_+c+84-5uiCTZe#_So6a2as`jP=TxDBtv?kp1EHO|2mm>L7my>Ur~G*Moj~*cjf# zHUMfYYxP_l(X!1+#aOYn6fj0lJ8dO4xe8*;t6o`Xt$6l6aYF*;4&UqD!cV=WuNT#2B&F4j=>eT*pkJD-mmwL-C8q_bfg5 z6UcRlKWy;6l{VVLSW8~fpoEbFm*p$J@eMzUba)%28*Dp0!&MnU%gWQYu$r-=MD4uX z&bL{9NgR^1`lmJO2`L78*@sW5&-H#yp2>u~oz)}Zd3x#Tceb%4M{hXEw_c)Fr&)8f z&$*BG%#+Z2;7Z!*+ZgJPeDOs`Miu0Bbo|o}2-L{~R=`)hxX*PB?6h}F@44XjJ#hE> ziL%VR5=b)m^WI@)R?~Ot8A^i4T$Ewz03U%K@aAB@=pGSUsdH%jN&sx@SE$0qhU+jg zCqs7e60_Gao6ZHY^Ta;d_ppbbNl`-rcjtl9i2WxybXxBYV%o$ynmEFnp@S1S9>1KZ zwkVkIXeqD4u;zxK&GiSr0W$eIk-&V5x`Hx=$|~k7>fl2NN=^R(g+HE_s*1X95AGkz z;xrrcv)fJ!gdP-Yx9*Ai8mBjh@k-@7Q0xTooOI)3azyrrqNeDz0(o8P%RG&RrPx^T zD#Ea+QzR)x$=_zv2LM1bk1lDq>%>c%(dlZJ8GAN3OzR4icz$h~&kixqLCV1iOk(+L zVDNeLB+0T&N5s?W0HpEZnMv-OS5lm%w$WT(l~a~GIXq^Rr#o;&K1UsSIw?)FZ*8*# zO%S?D6Xu|1dR#pKC^H!6p(Bz!q}~g+6XlMbKV#omBJum?A-+qf`PS|IkgUF}6V*{2~Z1Cf61rWYy644a}>Cq}6AZCA+fOE>JQFnj3 z=X$z-gu6FRl|Kq4aZFFVI((UMFZ|LtSxV>X{kmV>^S$PH;%Wf zBT!a@Z4A|2SzsZFr?hl$84q@j9FKt?9EB>HD8b9Qmy>1 zLT2|jKM?=j7|oTX_U{Z*JcO<)Ih-9ou?XH??QaHs5xA=uazLOEBSc4- zWbbQk?0Pu)^F;1M6Swi{YOJ9T^F{mLs}eXpe@4pzpMeduo4)#C%Wz${EHk~^Eh>-3 zsn_MrY&He!Q@hQ6UyOI9e>@JOkRsbQES?o1L2YzLgV>m0e||LD!F!jL$OZGZa?mkE z_@??`HV4bn=K6zZ=u(vJ_W5a2On^D&gvX%O{*J&~e}6gt{Qw?2w4+kCbKS`10Upw9 zyPa z8$G~*Pwp^_jyYWciB+1k)e8yyiZQ;mwf}8;WDGN4RFg^kW29cEIb*vKu)lGdw&k1s z9#8I}aH!?eaW{@A2;1C{o9#RDp99mva+p1B3nXbXBS?FK?(o;lsQu`2xjLeuwPh=hEP-_@1j>{D8N%%*e{pWYreJOtD=j-Y8wB zdDgdt77F!fZO*!$&EObJo#JGV(rPi4UzP)2Wem#d9u<*ocj4e_rVj8S;5L0ylz7FQ z0h^GCh~dWF;ulLFc$3;~t$nu+{L0kPUSFyZs};Mz27qLVx8({?uXHrAFQ^}U1qq&h z<47ZboRyxfWiZ~FwEw0?C`J}h!C!rN9$dg)O!%Ee+RiA1gos>41iOLx@$SJ&Z=iLMA_hP44BXc;FV+OlOTwtY z2V~qnLqsb~`L$$%lP2Rg(&iNS38xDp0^Fk4Xzzlr?5Qu z)1}@VUAD< zGN9K%F8TP%HQ>XtqURXSv9hg={p9xKD7o#>=%V%bgTJ9{cRY|hxH~(T%u*qdBkySr z7ZFW%@}h4d8Hf#hSB$;vs)Lg+%m#qX$SiK>V-6h_2U9VBf6d=hH3;5Mfhr$;_Jh3E zv{}QY^XPP#$81CXkW(n@1nVC%@r%5HLkn}WA#D$0OnT!n^7#&u^Xe<7#=H_~pC%CV zjQXfkkIQ!+!FbKvjjb}Zdxm*etB%ptp$)g;RX4-i+0s3@VEsnIsRt&pMqU^o`21wS z=Y$uTf8yl4kKK}=w`pk_UC5OijZDKLV(dzM>b4IX_^p))kP+sY5Q?9Q%MVi;p8ynp#`k3q9#I!vF z^A!mwt%{RzGyt%hT$=<$JHVQsEFStNS^6>{(WFlHpiS^%y`EriuKAo-JlC!XO=+_L zG@#3Ge&zSEx1I6SnkIc(Ie-M)ej$TFroSBH&OF7kPYb<4&80{QKJQW6Zx%z8!g`gtyO3|a{oKu?3<0gXSrtQES=wc7kJ!!3kS>$ zbJ`pv+puE-Lng1E2R5P#n%aJV=k&W{_||@U#v4Uhfq$dHZp(yo-M9A|`~*~olViK1 z`kYn(Mtc-^6uT0cndcD3Br1D;-CXj#+~;#wZoNZX!cUL`?@#T>E=~0ra^t#Wy%2y+ zMfvNhXIMM{;9FZ;hcD)l1KLuE~>F@ zk~i_ht=M(^#(EBlD`oYiK)s|@OZmxc{Dbosl7L>F1Wu=K#$=xlI=I@i#Tru`SNaYc zlV)COmBTeOsbdO-3b7+O&t-`JwFmU<9}(gu2!r}~OztaWrg6wCJ;MAqPJQ|nhHj1% z*{^mx*Ru|EPRD*$w}%tX#&GX1uWhy)OYAH#Twxih7ZT$0i35%JBtxqlU$6ZkAFFjP z(NyO7aZ@K^Tkc*^*n4!)Ab|0cypo5FxSw*!iO-%l2i$XB=bm=IKkO^7l1$}_j%v65 zdIbL%EM0D~TRv~hH^~2cDZanDs3ID1FXm$P)1z@}Ej;k~R@o6{wH@ zQ>Q;embN}(eGQ#w*N_58Rg)Ow!pk>^`(3%xS#;eUC&cur5)EsC1vm^Jxz&!Kz4YSW zx>Wi7Yk44ebIcxx>n3s}`67}0<6sH6DG#5pxpP|FBhdJ7a9eHeYDpO7^YqjK>mJi{ zCN~DJw51~TS7x4R*%+@nQspxPe$BdNV#|n6_dmG#EPtKtj0%nxjzya(mC=x>;n@bw zd3L2Avg}4lkxvln6w0jr2JtH1yeg~^vX|=BJnXD+LUUyB&^aYijN!hoyg>l!lwthX{pT3) z`_surN=m!iJzXb^DgSe_WsA#+eE4q$9_{aSK{Um*Kj~`vmjo$e2vS;f@U+GTii!(U zZ+1fnvr<0#aCjY72M@&qI%yQ)AhSM`L+Ecf3}9#p<1N)WvsL)*bK};ND&3PMd9TND zw?Cxrc7xtIpbFLZ(MOhA=`bUJWq|CnuT-Wy)eDMO?>qNVbw?7ABF`y}s6<%cb$B9u z2GnOXo0Z>_?L%&J;(htNuk$N3X3yjn zWuNDqy3t@rt)Gtwmg26}=*@AZ%VfQ2lWehEG2?=Xk%~r2eB1xE~q~HWjlBk!O;n>Ah z5*Ec%SmuOb!kjC*OM#L7EW8QKv(RN;ZaJQPUz(BQhTz-b{rBKsYZtB#R6|NcJ6qTiw+JtDm;%Yxe7Au4#)2 zgx{8jysD^f?%J!zqP~spkcw zZv)Z0&#wttL+it(LuCak3jUdvvNm08HQGxUkse;Co%56>xY8|vYiVw3X{jx*tfVYg z;+FZOAMhoQDhiTZhEXal!>d;8xoa_?YNzb&`VCeBNNd5pnHVZVspLwQjbxG!f9fnN zNe*DDKub$GdNYd}$QXMd_t;FYgX8Lk80Nek1pRHf8~_Y zfs(l`n>>l;27ccvsqtreXjgKk{c0~`qIWMj7t)k9^rh}0TlBio7BiaFVZ@2d>hoOE z+H;PImLk{J>y!&Of*|5C3=J9t4H{neb3a^PsaqR+(7~}1>s#H2q{T&H{RB<=IGPWF zrWOHKE%b>og4(W}HqCG1rLcJy8$;k#O|8b&-iEr|*-+bMLvC9$AD9>%Cs3$RFpVxb zR+_!u1Cd(B@lt!spOY}tLb!u#_%@sR%fS2vgqNI&xZA+9Ur>Z^C=fBU8YDPuL^X8z zpt1N1RHTl}TKW;N3(LrI_`TaI#X@lu6JdVT>556b_ZnMr)NQ7tPQ|9cxi6G)dLg+09ytTA{py zM;Slv$_VYf*~kb|l-L{%R#L7xFa=TtVS0ALoU%ei4|pQ6D5C%1d>GN6yg7PhGX%h5 z5_6U8vw;CF`SCYwjK)uvo68j>G*H;0P|&B8fM_b-m>MAUUxui$_HNbC&R3vU@JRh^ z9ZI)A_4eVf7IUB;dC|46mGIMZFD|Tmek{v7o#k4~lYnHtpFeHpOOJq#L=M0L0$K)Q zdBYeFqA3dm~F7ur@N+K)%T0q(dFA|+uk*nur zhUv5n+1v~s+V#;kMAh@=k1o}`U30s+{v!rCyYFAZGCPQ*~Y zQpVx>)HZ_TeNQ^G{=#3F_TvRUr)OHJLJLIvZh4gmFdzmgC7r;YiyIk1zf8mTpG{JH zWRbPJgHVh%pzPL{`gdF;M9w%{P`6E;@|x9uZ^7b&o!`6i@kw_nG5<(s$y08ZV#M^n zTQ|YnDMnJvKL*{b)0af=Vt@*bgPkCy-sqfLQSw^_V6D|-SiI?#W%w2HF3iXBg^J8D z(v(OE68~fi4dIu|7jSOYcMaKtdZ;Pst<6mF7GjA5V*k=vzRRHn>FoNGSDJ861dc=E zr(m$-o4(xwEqzkYH+|d+Y(y^O`2*|!HaG#lh*?vN@A#Sg zwTxphu^VZ$q%UL~jSWl~JG@4c`eJd(uqM7sW9i+KgCGQ>4{?B%59UJ?^aA)+L-FLc zN%ysM=>r#Y43AgroOuz2Iv0}JfaE(WvhAur%*4El-Lh@CHa$shb%y$bYa3qk0zG6( zR9osDni1{VXAOC$-Hc2RA0kN~aAbNG3E;io#oNL7!0bl9j!Jl^y56kGTH|5#~S4q#JX^&hVq#VsTlX z(n@_?lBQ;gv(6%UTu@HR7aX9x9uYRFgIO;Gr|&Lh!}!dEd-CLes7%+13;+Nb?KdAj(!IVqES)UjvfEYTc$*AVr% z97Gghl<`!s)P6nQ+TmO7)3%aYX<%p2?P-+#A(clSh;V1c-<81MzglN4VLes`|t`)y(!J6(|7iCA|e-UHU?S~8#fM*%vkTD(Cbrf`V zOdlv03GDLCZ%c6f4eEy9)ozBIJTquP*f<%A_Jm6D*_5G{Vu z4WdKH-pwHKpg|ybcbI9)+kd57n_5`<_eU?cv_nq{Gn~oc4Dx?ljMzhz^qEN;&Fp57 z*(`(6cTIT6(eK563;1deX~Ec@^g}*MfdPFw=wokuI+*Yy)6;_S{c%9uo`n8HQh>nf zkju-=#UcH~0D^G!oo(A49+_-awuOQHjiqx677BGPn~>Krw_f@^)ah&-WrmxfrJV?q7B=ge!dpmiR_5w3_Lg zsZXSaXlgh4y||Pm-XB6!-lA|SA6mR4tEZbmKum`atxJRLXCsYq_ysaz_=oE%eMoVd zsBtEFjvej+6A)!EX6&$n=m$Y8@Wj`R?HRb2mbrnXnWd z!y6R~sfR^5ALx|gQ=BC>Ri;u0In_yH(fcgReroSL2K9d{q@BUp{?c_3I|vFE^d;Y| zg8YMW9yBL{G3e*LM-(zTaA{wOET4@%>(v$#3>HE;03cxK%ADW}|7zvM)~}?+?E5U3 zNGlNutF-5rdETaeC8C$JUJ;TAG-r+~On*Xd{F2J}hSw}2z7PisN=ge70N^r@+#U(F0AC?MjA5ykO z>=2ct=<4mZoj;w>2o?uKM!_M+RdmI9UU&8S>J2~Fdltl`yoUmmG}%SPHy^*etY3u0Mnx7jdP?Mj*Hp&`2nDQjvr&hr{yso?R4flehdf#GyW=raEo zOfY1{o>sXT+(T%b^@ne@*2vNO69CB4<`vK1%xLN;1kbHLS)7S|dKP2KyB3I2_Pjo- zqv=fGw5>3)auzgEJDj)@^y=1F^QiHfQ5{H#Du{{wlxC5|iE&9C z{CFTc^A%o-Br2?B%l_ZcpyXVroB+ZRMo`qa*;!8G@Fa!lO8^+XHv!L>!RK365*CIp zFW(nZZ`X~JU{b#Kp0$FU8=TzJV`0dhD53DpIvMXk>B^k$3jGU;y!V8lNKrSr(fZo1 z2N{CScUsgjn@V(Jv>pirAkZp2sIifWlZ|zZ#o&aH{Fml`%LF0L1=~g-PN6~6H|JQ?!4T`vzO`(RSHI)#N2D;A*$Co5DDOSc7$bMO zqhki~M*3jT54v;giiM^Xk{X^bnWMtk?c>ow^SMNwW=J6yzdmCyQ(%F(^3mlDCq|_? z!BLTh%tFvpwue8W6Q`LUT{56y*LX>JzKlmefvQ`I!OJkgmU+E#A3g2OOW|>~l|fTI zKO^SC^6$(*KYn}67l&0z4;L}TB=6gE1oPh2>nS09v?GL%Q46-0Qk(Cl&`i;rRAEFh z(EuOvrUh29e&b%%iuF28E<3C#75;>v0iD&64SD9Z-V(=wwXBFgl4mdwXE ze(qtm?KAy4N(Y06LWrr!8UD{a(^Ig>etBY)bP@|Y#ySY(E2)w8E|}G{^9(j&NPk_qJe|XiRocd^H>I9XdvhQ| zx{F*|fjUWWS>}F=i`D3|0mGUB+zG%p$22FU(AXgn;+nnhDW$e(1sEBcq(Js0kK9t~ z7cZC~RAueR2x*?-S~Yg#yT6A2TV29m4phdC!~kS3fLe=5Po(T=PG9xHuXJP_3dUmP zG+Vzs8bNGu_0JG`;-WbcQnqw%#IwjwG0-#TYk|f>G zY^EPo6__paJK^*qP=5CO;E#*7B|0#Kx}AFenlXx`+t69TwDFZGkS=q}P|!G4U+G8j zy}Saa3U*{Rqh#v)9%5;!1`S*T7+j2V5phZstw_UP+&%ePbQ?iUjK&RPg^5gUs@yf- z5Wd!~$w;@IFm!{%r!taab%s!W5c978l}vWlBYib_C{C*wf}*wmHaalu(hpDKh{VN6 zEk>Th_h3bGStO2TLuf8O>8IW6QaTr6__a458UnPLN<+2a+Dj1!;XBy#b#}RYiI64p z{o_{$iH&5=jNQr9t9UQFj05xFZ-TZn&ML$!-X)xNr?FJv`Y#l5LGt!UFdg)PeYZKTser`>@4pWP)A^p44-Pk(c!-_u@3FP$Fg2-%h4FEFr7CJ?)vXJW^%X;3 zerglkkK%{tfI5NUNqev9E;C!Ub?tzx6rG81lft}DuTS@**#4Z0+&o;<7ge7o05H7C zQDKzu{7WJPt_P7XKTueH zgBnM(H&8-D8myX7+df$MG9~Lw*pA4G^F5qU_yuM~-st@Lv>egf^Rdf&3%$eZm4$(p zXT`cKg$XF`q17FL@c!)a0ctOKWa2%d-a;^KN(g!3QzMm*P~8Q>4=t>Xff*!SFnai_ zMj)U<3|n6Ia3S9zH#{<5HJrDy4cX}A~C6HaL$h*RAquIIm%tMK2eU1lBWB7?G~ z4z#0K-`gb3wButkB(vWMn2j!6TpGW`f@daBKI0M!fQ@(En$E?6Y70V;ph-blzSD<> z#{lA}OyU2lrb+X&g(Xmr;QuOla&V;nB^~?cn?#C+$N~)i4DE+o5IZX?3CG>i#TKQp zL(^)IlZ%Urva+hOvZ;~QYV*lsa`Jvd5*-^W>rBg6Fi-_R23Vw(P3jcx(IQ_wL!F;0 zSAvP)FOti&&(h5X6oq&bcJ65VO(=FAefe{yttobQx4(rtc>fqx-`#zz6csVmX|gC{ zLW9P$2O$17K?)L~tEF`OuN9<=+(y(1gQz=X|IsG&sNNpFVD9+yts5e3S#9l~0sa(V zPY;i~`v*0(*LN#njHVd9L$kjSs#v<9){;w4bmL8=aJ z-}%E_`tUpaSzVQ>IoOhAUu)CM_lyC&U17T}hL4$L@l59`})u`ulf*^;Ys~93scH(WC zGWfZmJAq3h zurBx4NJ0~UCkvE&oeqWOIJNM1>K?QJ54VEvRg`Bwf5<8+!S8^%?9)(ZE5#(6#-U;n zOn7v)11>-nC;}e|LH@L(m2a-nTkR;Z+@88@%IESeIZWkrG!ZA!@zkVxYZ(>TO%tVy z`CeXc&~CG>Y=j3OY`jmIny7Vu1#9|$-#pPKjh@S?_W}jsA5pGaQY9ybYsAfU(mzpwVQ4llj$!P3S}k7 zp3%7{qgf4`Y0^dFJWs+DH8pA6z&d0=YPGBx0H^D-Vp@hfEV($LbseJVeBUiX+N`Q!4qov;562tl5D?08Lk ziV3eNrgG7g%VH~tudJvzT~PZ+A31)?S7?q45ev%&+W^ttrz7@P@YTb-^l2T9K-2zC zoN1S7*WQopfDp!DreG8bpbNCL$PpD%WV4df)6)_qC1T>r%E~j$8{w^R!rfO+qWcFZ z1@B{3v0_X|i8wu;CUX1#QvWhlirt>C)9ZNKl&0B#n^qS0iq;D7(B;9~Y1 zH!{!2X4o&*kV_BNf@D2UXH{JI_HUN5U&LSDotMs4ayum7?a(x-(z4btd1kX*2`vdn)}4}i6BEj;Ic5nHJ>)|8(R zyzecQiX!HHG3nmj;tx8RZ#OQ9%dPJI_8(4kjsbU>`kl78$x7&c?+JeBb_y9!Ad-~D zEn_xYPM$EPrw$8~4Bj6{7GY?S`!4ihq216`Xj>fyy2wb&Ci}HZ_W61DY@tmM_$+LY zzOhjDXfbA7P=h;`Y%f+mJxxYincIzm(nIxTiki{`I%wY`z$tjbDHu8D2ze4u1xOua zTJ@Fq7*E6zOW8}&DH8mpqp1d;SVTnI3Rqu7|C`D!ucCs_q;?SbWtu1_ms^N3^N}O- z(T%0W(J`UT?c&4wRKDw}5!Dy2o$&8Gnpfu=F@kWxNO(ENY7byWdSFh0L%Fuv6l$k(DZIyxSl@T7|k=%($X6D z|L37!EiNsWj2eADX(1SM$NEd#6#w{i+il)KA~VH0ozi4qvU1F55vDE-L#0HL(XWOE z%=OI73?py{a|R?3_)Zq)gt*ChX>_UK>`F|ot@oN>K(paQ$p!Nti|nWWEWK|bih60~ zQaN3)H8k$UDpct^cHz2F?cxtBg`t9{=sf$b+wMb#-eGG{bo|vLX2L3Wk6wsgx5IoZ z`G`IC;LC#-BdK6TjuZKkKL#^4zQ0%5EnvzlFD_oF^R$hEo6+=oJaKJgP*`GNnYJvo z#8hqcmkgClw-~i;I{UrF{c#Xt#|!vU){klP5R2pfzADM*GL=(s!S71v zFpQ8Ocf1Sj(9sELgc2VTq_o;nDY|U5D3#1nMT58qFc(QIal++gs-$zB^C0p^_Kn}V z9j{wBhn(*1bq*iDoy3A6tvV-f?}c=C6)O}A|Fm(ryZ@CkV~Wi$nxtu*!?20jtvJPY zxC4JtQ2$AsNyk#_TE}>tEj%D4Qk@Z>I3VTwxD(rv?y%WICFQ&Hbz(AJtOV@`h^^8 zxzzL~B)uc04-4uUDzi?LjqVra-<$g{t$N^#%%NsL-1z7DfpnTe9*8c8Ypbg4kO zsFKovHUCQ8clQ8Erb|o9!|yg5_Ij~0j?(R-RcvQNQ34ki7blVZz7~yJmcWvt!sjnR zdE^y3SzDhlZ(RPaMsGza7F%T~p71RNyvj>m*Gt0wZf%qH(_ya$AGdJIX7a{)Ry_wdH*Sec3oX_0ce8NWV*Q+T*LDEf8XFJh$e5S*%_8goj4 zjC}n_80nNY5Do5|j9qt!G2CB2O)}Jbfxr-p@l8tdvKRf^yU+R9edB?z@Y`;&IqL*N z>~g`1^GBC~j8+r(9|1Fl5skvfAZJ{|qR4Z1PQWbON`Lomu37unXPTEq-rq~GaIvld zG>0xJyeAr6XD=^^@kGLB!eCSl0^ZIfzc_;)RWwPP_2O~y!kuQqx~bMAfe$8)WdJ|Lpmh)u3*etm8xo}7}}#r z@2d6d$v8)|AAW<8FazVEve5-r^(+wWV1Dq^-9=WByY*LX?TGGyB%6-c_T#3cZONy< zGj@IBr-=fY=QFZ}UimArl2h-!+duCoITKj(acuM#9433xvL#6tfxfb)@2+8-NnmuR z2-5UarjmEMR-d^vEQ2LZXU1tmjtB@JN<7_f6Nm(C+097nV&nhh)7RpIf^x*kKRdiD zvyRG)zy3-q){7T0u55>#M|xJ_`|?}pW=Wi`NGd$k3SN>PRT|ak-Tv+n?W)Y804uKL zNI_4BcRuN(fDwL!l3a3v4|5uvl_iK&ifC_%aMgh?eb2&l@)70lx&!$zcSO9N9d(zvdl~KaFOaT%9s#Y+HjeaR z1t;PBj7z9O>M`J!fDg8-Ll|N3_)Ck<1Ht7lY)+rsTIaXI-F90^N&YI#?|hl;w7xhc za{PX`ULNApGWJnwdG-`V!>ST)yNy(~;bRa+1EtUx6GfW827(>@@zEnYp?;%Gu0K1> zAOD7WfXZhkl~YqwbCN34PB7!CjS}7cdFh!<#)Kw;&d%5Ox_MJgRX> z>Bwzsq9T%LGVeH9Lwo2rW#LIe@Gz@?=8KS!t7Ty=Eh)9Mu&}f|ve3LLvQ2AFO>J%l zHZEbwntm_^xKCEBDD&gdE2W^ZPS0MrixENNNHW6&Ri}4>snvPnmjlIM46ow@0{WG0 z&d4^jF;+V*(~ooFvKO}~WYfXy27lL=ncD!10iDn5Vyo4W03F+Mswz(EY%+8#<_6<9 zkKidb7O?%O%pUOkbDZz~T*SswPKIC{pJ2*MGK0~@6P81$4u4Uq;eyT)!qa@3i^P^1 zT&8+!d445dR7K_beSY5T8*|mA^?r0(ZOZy3P|%i(Ebm6;*0k0|k7AEZA(N)}6}#_D zizIgT%fyGXboGwI?Gc{IPV>=znO&nB+qKGNr|ib+HVv+*EIF}icM2*wvrW_eCi@d; z>L~CKEew%hzw%Pj(l`V>M#n25@=cZL-`LzSU9pb~r#_kiY#dB$k$ky}!#X=+n0b_W z?~>V=lR}+NQ=6ChL1PI-VxqiTiRo${*(wXnO1UOnF+KPMQPnjyBY*xt`znG9 zKl1P_i~i7auE#61KD4li3MY9cB&PD_Fwc1w`}mPFaScB-FD!@r%-sW?D-MJj3Y|lN z$3TS|2HKx$hkqFJQlwVGbQFf3l6(L4;XZ&*-|&tKteCUe$JG#obE)@|e}2uzl~b0r zz#N6SI)tqxW3?P6nTQ<@`iy~dhN2EOFNeyRgoI!~zABPi)S%VoBM3_rQkbzaWsF+8 zM$a#rO{-X4n?C=wUKVvb`ydi5VKd)#|82pbYkey^8A_)B0=t|yeaXck=FueWbT|FYa;FsmfHJ4cJ zZRuU2ps0B1yv|Ne&J&gxtYs3Y0+nTEe(oOC@#LUw4W#R!LK{EUkNo|tDGumLXwrQt zkGOha-bZyAD^zkTXUCf>%{p?Xbea$N-X2*sNO}FhdVdgC+{iQp$lnGmMhW^8X5u}w z&&I4h@=jdioRISNo`xOhahNaZVx_Q7p;-N-YXydo`UTF14`UKw8KG6LIX${famxM= zFhM*(mgxaB1Sy)GJLE%rrO(5Oh{E59q8)4{&;chD=&SK+9gF#85GglbI$S49tsYx5 z2$RbbG7UkmdH-omYZYurcXozY`UO6V2|1Y~h)SR+GY8-8gtxgnmEh{Wkk3)oE8(l| z->nH{HYd<{m733IuLcZz$9~hy2QQy6Cr!SopRk~;y+->pCd5@Lu<2tcVifk$msE3U z&s$Z6{$)!F%}#P!uL4}XF+RYi6AK*<<4-O*{@ncGMepz=*%Le%3E4K)WHTHb4Ex*D z)pouX3q;K4#bW!?l|Yk7cXeA@T%HhlbY$iD3*XmCE3+W@;UM#4B=KW$NlAMOBZt}8 zceHLzy0%<+04~TYkpN1IBhszr#^(wS{xj5Mdam$P39zV#0{)p zN;a$L@v6t^)ge`FT90vebvldQ`2dev4(YwfjkXSx_pX6#_o&im+SU(8(~;c`_{fjs z;WaIM@W}(mp-qEm0!0Bu*x{qEZAAlgM2>6GmhEIrDB#(e6cR53AG_|Y-kN+}=Od2t zC?}2AQ;v>>2wfnfVx@DUUxzrHfHY9f6Tya;ujMqXI(NT0Q?(z3ScbXoEHtCMNIRXl zpWD0HbKw$a?G>n90XU40?+Ke7$X9>sPlk|a%&SGs%$zI>naXDJjf^}$>a0Gef%EK| z&YD@Qz;_hmz3vvKhDS^Y!r}2s=88K>priuP;EWx}Uh`c{D2q5w5o&~3TO-7SS>59+ zvMi>vo~{M%$*HYN==K}!A`zu%7v^x$Dz#rTuH;JCx@tRZe0OfRh{1PMw?hi_ z47H>s>U;(<=>Fn2LrGh7ij|gMZ6eau#bA&f`N`-<)l8B}2hm)I(PD{*sqSUIrfPcU zn{)+y!{FXsM7`EN_23r9k*KoOjf zb>l;I*}cXm@6n;^IeCJLitov#$6w`=(Dguf0Gvkncyv$1efyM>NY`&hAziyxu^BD#-+uSV^aJ?9j&G>Wmx;rIXx8poTR+AaU9xxM!(*EV@$*jqnmUh^KO%5hbsEiw#pm~SCYSkC zR%PWF=ssr-DIlNO~78fIHi1;fOAR_uf z>{gQO_%GJrd7!B6MK^%icDz!#WGuH1Ct6q@*%X*^OIMrUiX}MnUPn%SB4DkfP|UDz z3W`+JPQX#JjdAAI2I|~yh!?x^d5y_QOG`hnm9S2p%Qa7&+ipdeEER3RBH?BoqXOX9 zH4Wf9Y^2WcZt?5zzh3TrG!`Xe-&I_&%)$B0jR01jTdDK1?jQQ2@kr?GT&M5Ryn{s# zcZ8(i@}+Ge9AVW|4VwU4<7V^AU)%{ z{dU-WOBto$LP}lkI7E7F`0>iADaxKzb)hjzLIf#}ccQ9e?TPq(p{shekvv@F8%PF5 z6-dV*aME5N`?F^I7@I)kUvgxFXh)w{P%soHb8~N%K8@DFgXW}%wKXK+c@^c~x$H3I&9}vm5+}5?RN`0z==8^kK#O|6y zI7jSpbBh)g&bfkkn8{DFBKhT)9hfmeQ>iHK7vQSgh^Y+J*hG9kO_In!Soo-3&Rd$D zjfQ(?JSH+7Zne<*ZGr3`#Ca{4D+A03p?Vi99CIP|GN=v2Hl>gW2LBA@Xfr;F{QjpVEL7%x_j!n&!z?N|I9N-Ohs0pR*Y=hGUslT&?PssxW7}r!*@+_nHD6DtBey;jUq@W*0oz;q2Et2ArCr zlx;|D#XJCA;BNYfgSBasIQ!UEgt{`i6VHQDEqdrm_Lwj^J`TUw!u{dtQ=0XMGf;xn z)&A{R`a7Tol^lDGuJBsJfB4@!HO%R~O3c)8(1QY^-53GO*QLSbT7j$TZS{o}&sP96 zL^4I0nV$ai8CZatpFI)M!BJF$HxafY`HVdrr|Ory^Do#la3vzOTL{La;ttD*&Q!P(Q7V=$ zbXiyXB-_Rdk+@LWbTJxyV&Zr>SW3?V+8>8I7>7$q!GD*Tr21VqRHvOnJXAsoKU9!3dE0 z+-sys$p05!Zv$%tQC}md6vv(LKBz%1mrY1L{4$Qy=!Q+mA@>MfD(=cY0w=TkHd-hUft-sc=s>UazT$TCdtESN^i zd*3T%%&-EsgxIl@OUa2Gz6iy0vn;l66O+fg&t+Tyo}mFQX*M-{3nR&m$rtPV7WfG* ziT7ZEXa{BPX}hJeX7V0S(Cq!aKk#Ufaj_V~g)2>5I!p|yR*F_7mpi*9F1)JuE-Yy* zftNHS8)P%@VWvs-UI>lhg5J6l_|VEX4!JDV{}{=}!$Me*=Gl=|2q{z)`m+_X8?AZr z#6|RelW^i~ZWWBVJObj01b|Mwkx&P6xgv4I5+DEYENZeNCh@veEAMze8o^zMt~n<0 z86qt|yCU+W?H^a1P)M*g7X!Gd!?y_OpCa)KJhhq1#b0&F#8cKaCLaP_MU}>)eH)5 z92Oau#SZjsB34yTEFmVgB&kv-T(=#&!$VtJn^`W60YAGG(2GnH*0kx}FJnxsNU$Pr zGtg0}(B`jvXyh4{A?CZ*Dt{*+#KgR6@+$G3K!AiJTB4PV+;&wdv8~AMxuCk_`rZWX zkb_n_0%CC*0<0vEmQ!5L?3+_eRkP&Sf2sebJjs*==Lm}amxE1B{A^mP8n(CrDFiPN zE-(S@+uZC28n-j5NvfSVz%QkhE>#X5@YAQM817v50&E*TQP<*r(2sS}A+Y zNC=x*_+>jI0&Y#udAAM&8Te}vA}I;*lGn@JHps8Fn5)hrSKm;%%a zVvtWy_u(hpE*&1v8V}Lk18}~wNneuq2wCjbBTqDgQHVZN9%`iBNtod5n=IH42{ml| zF{!JqC%N`KVVkcfY+16ASU&X-`#0g#-;Eii7Ratpk!hU;htL3Ji$w)4fKfwUZFQeo zjI-vMktwc5*G_@^Gwi2X|6Csf%N|@zZ_!X(kH~3PI9vTTGE@bafRHJXgTUZhdS{CT*MMQ%)e0H>Yh z0zKR7Kgk`DZi8JzDZU{8`DX$gMd#o|Kl*y2#;6B5Gn~!xYZFW-^A$`I>8`2IlHE0|+5peEXG1i8{MnIziQ(Xc%MB z%-^!L@M1JkcHmPb#)7lRkg{PxTftmLBASx84^} zb0feU-s1V#&SJd#l|igHp+I^iY7_sMKos=U#%}(} zq+@WV!PL)&4OeaVd_9pjMbpK~rlHhv2&luPq$JUj)S{@{4|ei;|0eOG2mdZ|T!-Kl z0$2ZZw{-!yp`^l=va@`rlc})$&}}vRfX+fy6c);0@%)&?b zWTghgKDiO5={0=w7f)jsMJi2F9Qh1o{JBcEEMZ*Fsow5J#$x#>dQT-xfA_%{Zjx5; z>VTP@3%CmUxe!LJB%*JLE!72Fb&uoxP1Gi>{_Jz>!rMiGi^8TTT4;Q#k}dBWmdxc@ zRfR_L8DXipQK6qpU>f~~$$m#3t&p-Dm8owobs%Z)4XB`ou6E~H2-D96xAG?MA?HidDR!>or0DQ2e#U1fzv$- z!4#beL}M%(E~dShFm(k(u86Cu{|K(VTi1Kv9VL}OB)q5A_^Yjcy{n^5lK&WTMN0I6 zr{P&3uo=qs(?2?&Dyph#dReLb(wwjHXEN@eBm=lqGIC5swLi+gO%iMaVFR1yA}`wo z{x5agYY(QeHXMst1Qz^iqa;462#wAZC?*iWTgU{VufxvI;88*Tw;N$>PrM>>qe+RU zXEfZ=^*`*r=|ArcZ4@9G!}%M@^+Gq|5rTejA)ev=n*-yC0yLVg*BQ&b9tUks`<)c` zMepns4c6;d#!l^kdoa=)$pK^_E*0Mkz&+Qr=)I(^q?OC7yNe5jXPy8u{=95T>W0Ss zC$Pw-Z>5r%gY{YN?h5yS><%TDFJI>fYAGz*e}{Cx^%l;>@1K|i{7!Kz?(P#R8h#B< zc|l2Y_lV%c2T_`1*Sw0J;|5td8k!@5sqd~}=mhFl^Vt#!TM}-&{H*4eYzpNN=~7X#Qh+PS}ZwdfC-5{@9HB|c5yyzcEX2}j+Zj;R!9ec`((IHM%NfEKsYo$j4GNM4vmiZMA3OBE*51 zLGn(}c~LU>{BF!L5rzRz5aKJC^b5D!lc~Rex#1?w3ECD9|9vzHXftN!wdrmSsBc>$ zUuK~{8_~199O6~A`|Fn9>FLjeOeO+InI0i_OM#jAJi`)&tuA@}Wnx~Q#Vyfi_c8e| z<0*;CT9dafkK3@Q4GHqbi_0~eC`fRPPN^z$5Hj<|DFB&%7t(etr_Zzr@*ri+nE2FTTH@48oM) z24|J_bag14Vk-Hwfu&VjN_dXuGP~n4Ez(-miauN1VqePfr{ZCe~)(r7@qeH zb>X1wM-xc8r|z#aZ>e9JKTf9|GpWBC-|U=|&|d~i+g-2}@`0m0ID=HLcQS1|zfUAwrbaZ$<+1j_R)#KaT8cgn`r*0w2mIK_~xthjsD zRosTo>$VUTr4-19L*+Hi#GXY*gQ2>*2=nPxi!RtE9o`AlYz!!~*_ApsXHcTDuWmf8 z1qC9KijE_HeSdRZvFZG!INPj8#yKKkD985j5d)&SW38&G6jRsD|kTJGegf=r>{~BYR(*_iMT;g!gknf>24kw%NKZ&eO=A%DM<2pxI?ZHuMxy zo=L^8s{cx}Dna?~X6Xy!7O0TO+{pDx21HMv94}uDw@jWCj!8hQ_it?g0lpKfU))Z* zpNdP0%X^8(;EQvBk&7!MC&)F4;N)o?;#(Blh!lWtL~2~Ulr-*m9{)ZAsFrYpdpePK zZb5~Kg{{%SO?$$l8E;&67y`~UDb`4}QwYkvL2u4@HzhE;{JPD*PL6RvEXb6@uV#67 zGEbFFGqW<0hF2`5BuSwqexpWamJlGb_jn?6t(53WtDgC;Rds=k4(fuIKYE@{6Pp7F;m(-4++d&;#Le8QHBLjsY2e`?cRr zh|7cxQzF@>h!xylf9D^sDW0sp&c~OOmcR4zlT%`q&_wKke7Us1wF}=f-?r>tbhy_>SyJjDCx5? z#|?go84IFYP8+=|J%nUPjLs}1YTC(9M`z%*j-;2PnJ0FjSRh9qL)^Prsr9w64zG|) zRgb4iS85jIerYzV4zdRDVecCM>)v_ARux{9u8OLvAgkFF?1s=3e6~*(Y{)BF-rw^x zkU&wA+zR$OU3R2XGSBX4eusi0Vp*#d-#t5$xYTF_Z=!^8id6F1saav+JBGtJ*}eA> zk}sDH`%2?P#;o4$k7|k7JeHMax1=AT!XRnJT-RS>^b= z3l#WM3|yg;Om;V0X&yOZ7u|L_#P0|vH%uz+O{Ve_UwNO8@h7Lq*B%?2S-?RD;DfLbRn#pv+Da;{?9ksUZWZZ0_tex@H2YU9DkFUS|K zs>|k*!ruo!hT>bxc?KQG+*~u{ws{M&yXc`gASaU4IPLB#1)mjPE}iT<&^C2gJVZ{6 zJh59DV>14<##^CF(Qh$YVq+#7>a7k}1(xZ8vgAM0ZV3UPiL5^44?sf?lr#omt)1DP zEHsx`{2fCAw!y}#_$tFis&Eq??}4OM2N{nTFd^C(?lp232kN07_X=@#9T^5QOc3N5 zHXy3W=3(#w%XqSw)w9gg>UBI|ZK&K$fBBHg@QcYwSm-^ST}E78ZzEOBY#_VSS1fx2 z|G8mDB2()#>|f0FMG>pDIm3FgwqzOnnlakRTZ_F|yw&*WZ1lj<@z*joi|bnr;v z(2d#^5>=&mH-oo-&te-MYwtm46p%ti~h6T_%t=K%>T?WHtuDq`I zs*sfGYEl|R-AS71b~6Pc_XF1xW3)TQ?8P%;-`GI>w!h8|ro!|GAq0s00k>AKv|Duj z_%01$qgd9jHs1mReTpD!wOpnCVYkNy!LGW2c#q3g`VUr$Ys7a)aux=?KFlM=S;b{Y zHer_d<K^K2=!p-Bl{g+wqf%UvsrcLc(d zlVguFivGc-mH5Cr#0VO5a2>S3+QOP?H6KV$#PhB!-Ex35km~nPk~dBo6{E|!Lwycb zZ;&-I{0}-SXXru(Mz{Oa@p&xh3N`E}8zc+yWmhuQKqO{KLsXpnjAiE! z!q8v;Zn@BEl3*txnkc4QJBNRGAxNOlHH*bSYqW9|`jDxAbh^`p4!r+vjqqI9P0)aR zfEUw?1MaGP*eo{oX@31|6JemCauFX*RjB6g-`o06%pd1QG<|Z&X1+AzV}G1>y;0xq zBtF`(68C$GFP(G>-!|>b|NhOjwE;@f!%BU!ef`s9&+ipK`9%CS+l9k!PNN~$vVoR~ z(H6?f*Z~oToQAS+u@NxkHBsv_(eR(;##h+WAMV_|i5x(XasNYV%EIT=#!t+f+z0SR z{ySACYTG)th5~&FC#T97JAp72Ku|`o>hk9DmI0e0 z`j>O&X_z@%63#%>LbcP%K%+p{VE0fjw1dGdttL?_+2OWyI4ThTqdJC+09^UGVt08I zi;=V-ZXqwv%i4=H0^-P*tFERxlS%8ag~0$yoK!SYsRg`WA5!Du<+n|x`h@bszJU)L z`cb4zzdb7oPvU&%P#x7Bz>>(TEZFBBzOGAaY5c^)DxIvd&E1J(kmM#L7lDUBLwd@}?ov&+Ws zC@&*rRG>Du)tN$~r3NIwEil=tTM6Br94tjZ0A|Q9@W${rTkPm3nl$g(-W`fvgf?k2 zPR5uNgI`*n+gIo`z*}C>@d*`S4K9|9tfn`iOxt54L&*<_RbrY_#O;4#sklJ%ld~xz z^|iOxBl!w|K;yR$H&?_su)d#}Io7AXFV7cwy|ziOF_MPkf1p5&)9}8P(r7a>F=6uj z5+~4(3-&c=u{&yzEZZkV$4-P0%=`ut#ZSG=nDR`=`-&|>o7r~cB4bc7-kb7`L8>fI zFh+f(!YVEYvBhqNuxz-Mjc|N#%36CE7u1F6Fj_tTgc)gP^E?Nmcl`KVRcUqb^{-)j zavLWrBZYZ>h;6P$_-0N;(nOyvx3^QpI|$N2c(D4f&&OUuv6Tt-1aG<{+ePn9dIQ|x zt+P6l#FhaaF2HWDhYB(P-Zr(LCbo2K_JtW1CQL{m785Z%5Y((f>@ry!sDGeeN}~M< z8bsR5$r#eDKyE8Jo7CvGP?f4gK{vSl{oCB{1&QWNn_UBL>MytYbcH`yHhR*(|DXX| zy48)_G&8q-j{68JembzgMoECxO^-gam&P({21^9;9c40{Al3bXEh$!ABm{}6iKBTi zZql3*5`HnTZ1V2CdTg2^B*@-!c?E8)B_4^0)ZY(Tqta7z6pTWdY5kOOe`c6D*l8e;3d%n%eNCJI)ThRt14!; zADm+O78hjrKXl~f$w_g^X|yO=w|kcBFo9CuF|15>yE{o4nG|L2C0w|uB-W#h(tR=X zePB;CF3<)4?fU||j2$jalxd>AoKOn;)pVcP3~yqMb_era5c$2Z0SIBij{P_IGhEVy z6A0vQd}JdoDZR6o^gUbOghAoYz-LI*wN+Nbr;5HhgkAxSqkpq=^KW*<2BwRqO*}0! zc&y!%o9kU3jq6z7sZj25eNc0ZT1#2-s%*@qH+6fmo7xH~cntgs>3h0O-#}lL*R~|B zpE7ZzNljqM>S)#ddCeBwaWM{dEr4E0{>Bp3#B3|HW%w?!JSo7<hkF%5*E4@Llhu zXY));pTaxwK~L=h86k;=$K9)-0detSJkF*RM#`-m*>)uI=xfYx6v~3?4}}+#+PEQXU%2xX!;(P ze2=OZpPy1O>=oWuZ6Kc|_1PeKVQ~T2@G!b^(CwEN@P0GQ7W#f-XRPtrB$v@L7<#FI z?oJkl`i!9n{&8|_1#IQGM&f9?QrWfY_3ilyd|!PGB%b#*^f4cL^8P28KtxS%y}7}W zr>?X5+GRW_C%))AZe*ar&WY7>k(0fQf{gd~dvQE+Q1b%?3#3N6NmI?4PMS~O6 zMDMV)$snuMIp?k?svMCTEGHbRcOLqPfZ0}J7;Ta zYi*+GS=LM!7_#Z1;?m;cVn1N1P+~zrFsx6|oUUAP>RC-Gi5-Cp(rOi&#nGTE44D0R zf{9%**C|U}Ny}-c2aLE@4pnBmw{bM?kt2}oK9=s~qk1?qQaXUi9h@AOIwd2kkjfVT zD;MkFrYjaYCSZF@-ae6{ugYFY!F+=IK}Nxf4%f5%rzN>3nR?f}dxfBo&E$YF#67V~ zU#D!CI5ioCE$@+@aNIp4!1O!o;G-NcH=XlsYNr+)Hl|75xiMkiuj_04bX>d!8$X8L zg&%Z4DR0&WskPVm04T}63EPSCCsnskHW7%=2I|=%E5p&KbatdW67E@bOY>2b=g)Gg{Ya)`{l@o1A93zMYF3wzd?&F8ieiI3+=Ubxr(+Th)S5IKo?fb$=?=53- zyUhP_JQuhS!1YMAeil?sjczQM5Tb6`zD!_;CMnYr3FA3!d} zNrGmdAo%0<+2vJ&JNSoS8s=%+$-IrOq_~Ndk(yBK`0&}%lxT+qxgQ*;IVI`R34m4yzCq6%r{# zP#@JL=5DOMzKpgvDeqfIC>3eYtTrvLX=k8vm`im=^BN*=&rRi##pCs!G1$r}MVt6P zRDA_dTuZR_20{oD2<{LT3m)7df@^Sx;O?%6kPsXK1X(<2@ZhpoaCi6M?ympg-uqtt zZ;GOJ&yMuznd$kud!{EZYkI7GG3|C@w#LHa>vYRqZqvh9Z&+VYNLWzAi@5z+-^&gl zU_9Uv!VU>MYMf-eg?8yxgklrvRaNI_Bq+2E&(Ao(O}U5_^xNAuKJh%Cn*GSbo_Bww z^qr4tO)b&8prw>rS4I>*BE_y(UU2xyLK&J7*FPQGeJJE=3jg5a%G@)>?xk2b<#7@4 zWx=MBhiW|M!;T0LP9Z|i0ebUx0npH<`;$R8okn&1aR@w&O^i$B=jVy4)1tj^=>Pux zRS}g)aAZn;L9xYDUQV`{c6oBulz!e%N_qov6;Y*uJw_PzjIjtX>pBw$I){JiNz!10 z;$S8}KM{lUIiy*Np)Yg;wPsMi>6f>z9*X6gPj6hBUowmu{|bO_ZRP3cJP72t@7!qDkO4a35Azuz&-5-BV|q zyT0xe9_*s z4S%_9fXf`tP5ikOtpXTv`HjX29j#aQj7yP&(-Fhw8IU~_IFmA zyxj5wotllBmI>Ph!WSXmbg0ChyNJy;tvsstN^v8XT()U<3PYc%U>xV26vtybpA)`Y zDhJDdsjMuDxTk4fg{{S}G(D74f#*J@OR0VJx35Z;r5w!g2?5dP^v^I3CBTY4a>Q!h zX#^*y;WLxZ%EIe&%Mas*5hKl-derW(R%%Rmrwky#*%I= zAD+A5$U6jrzKb(?=cmARvrazWcMvOkeWgWC z03pQp-QnRg7zp)Dly?4Xn^%>8KDVJ>2fLGw6)Ft6h89<$2EO|DDDRk@f1!*7&!rcC z#7%Q^kj>%WrxGP%vx8>w+#<3n_A6uOCHPmeDL^x~VL4sxHIt^>rjn}#4FrO#e|YoV zX4{8J!ISACd62N`GuA9!nD%r=V~>}FS$!w>8$*(sg8_!Fh^bQZjx0>{u#Vz-6%ohU zGF~Bm^YF-{^wOXSjfk2IB|t{M^>pd6}MM%!J_(&;em@W zI@k7VJew!?CmXxV$+^c2dk*I@BEW#F2D`i-(!Z~t<;ZXhZPcq^dv|FYDcRNE*xldg zL1m*qwJ@DCVxqxNJFGwWuw5+<1RkM|x(8UG5MXK!3Y@M?rGuw*ShQZl+K+#>~*cj{Dr?5VAVeZkNv%R_cvTWV*YXuz-QeaV<53+ zj>lBBe&VZozP?^6aLjxXx#FgW;*f#~4sZOjHkgJRDB(TV)(_l)H49vrTwukX3H5DZLZWl@`qcu+xwJE59J z?DbBqd@_}00-+C0u6iRtB1scehEY;7=(|O0n^uf=<#5q~q@30IwYI5HpD2-KuR$~U z(C4GixeQwxw_PW2j#PB%4#k1I{o!M4WbR1s)Dv;Yf#s#UGZ|$jKYcXXeWKX3n_w_B z@jhqHDyrM?72(9FDf7K^W6TppkAvNgZ@clIG!$;8%sMoxI5BB}s|_AzOewFM8>J}n zrLF_VQyuy}hGJ>%snLz%7x2v1c-ADmL3^?I8? zc>a1N{9(G<=+9Hyu>%n-*K!SVJ2s_Z{G}|!YU&ab)^mZi#g+AkLkE(QJAh=__Nz(N zoI+#1G=BnkLwF2u+wy3$a`+m25r6NXPa4r(s%0n!ImE$~cuvN>>+Y|Jg;77)0jVhCuqxk5150 z8kh_dXJw6XRC4lOj6!TD9!zq|nRdzOwks$Q=Y4foBYSPOCk?T6bX`urakg@$5nwf% znZJlf zqY<7&+(5DQ`;UF#i4g>dDSu+gu^t60B%01xe%9x{^D0LLLZLtxD#pPxkNxC%S&Ro> z>Nhf8q%oFQt0%>{QEo;Dj2WxRGRe}40*j+L+1cR~c9X}Qe+)vP??Lk;9ta;xTyoay%d)H7rZw;C_5DmL46Qr zcu-ZYXa(Lvov10(Bv9(~x!{g>($7Va=so0B5-Af?2wG^@>WOkyUt;Bs4M)-&Hv5kH z@1$7teb4oI23^8H8>?oLA*vX)i0Rd>Zr+u{4%uG3K9>KlH!Q|y$@CK|Ive8z_Ytq@ zsDV%NOTh=_o z8~9yKZgf!hX#)=f1!6oLn-Z$C_h>j{ej&9SGeW=}ftpn!%U5cfy2I-=DfWlX6{#

1UwNmM2(M;P4-D8P$kuI?7YFrVgPQDJ%;tCUJ-Ilm3w8^56U zO5&g87Wak@zps_&gn79oju0Zu?e3+lBYhk199qRbRCXPJRdX=8=Os1lB&V?P@v&*I z*FF+U^(KVMUu~co#X-D_bg;SR8tqjNdMcNZ+35OMoO(Xu@Hblgki zJpqppi;UMycdm6zmtzkRidq_qux&xjdgosuH0~|$m$U_EHE{0RxDgnEK-{ZsB{}pn z?ltw!17tK2EP5Hn?PqenBmMzM;&1p%;R$ru6ooU5T{#9c%Ol$HBiT)bVKA$vgqh#^ zopW}rP$!F_>3BPPsb8zvDJ6DvrmR!T7sVy)PRl*az9XjQ?~b#b^wTe_1{;e_52G_y1vFfk99Km`j3x%QzNec8Talli^tR5+)%h$n18{qma$%UydLN9X z{w27Lgr+X`fdSdz^6wPmUkN3`dq!Oikl*vSi_l&>c{&~bC56!0N}8Or!82?FthCBw z=l8egIsF|iV^ioWtbhVt8^!6jU%K$(!z41<81jza&ei^N)Dh1iZxe{rCEZ}Q=zLgM z^j|Nk)nkC0g`d<+oW)=N`kd}ob!Q5l)8ta5$V>6nGsS%>gRu4Q?D^)mrea?=IO0g9Bzc?qc`t^3Qd2_w zvVJ`mYz$Wr-U9}gDwuwE6NU2SiP17*qzc!K6uxGF1}h+R2hWivXEm*wzi)x5n$rJC z!jyq;ayL9R^u+=#8(YVidbb}K{rUEh!AJwY|AgT<@CIO(4O9~)!g`Ih43r{XX8DY9 zG*7ffAMJZY?J|z5RdmO5C#*d~zUTQcc1d<=z0Em|#|6Oqu`NOs((cD5Ms_r2!W%HY z7Fx;ScUVuux-=Y_?vG2`fBc}1s5x7FQASN8wp)6kbX;p!==cEVdvD5WHnU3Md$nLR zwrUeUqHvS+3FpnWMaD8yDj3D{pbG(Q>$+hi?SiaH`uoBnlw3gt#n~$D3TF!fF8FF&iMJkH8XYk={ru3k7*F2YepV$tUoxN zIFJm+;i<2-t;qnXg1^%2_B7vVw#xs_tipd`3kU2W={uWYK&bS`v(s-<)i&1Fb~~1!pn7PJoaR=uq|hJC355e%&HTT{>B{J z7l``pu-7tIz=vBxL$3B%xZRGQs!5p5Q)063PuRPmerq}sjHnH=DE=Xz{$|&mk`XJQ~{1t%i02M$cNO{>;iWQPT&q+G>hrGA`Bs zy&QfqT15DCNR!h+F&I1=90Kb!v}1fm32&w)`1H$ha zQNA4QM_2l%PqXN-&4;}+d}IbA-&^#`?pz;G zAbC_T18_q8dY9np%=WRh;R~S78{oNOhLS)tMiV1ZUu}Ax0Z_@Ppr_H^BmybZ%O6v! zrMNU_?ujzU(;bcc($;&EUuCG{-Y@+T0YlvM_jQd8jS-62>b!mHH!Mj7S;Du+yuV!e za*LcLYG`6o%kt*qhYvp2og9!?oX&vx=DkcQ+cneOH^;9CKjEFH{Ye0iZjJrlGyt`j zrVW#ncj=b3iJI==&)+#74h||*nJlCM*0%2Y7j*d^+q_I)=BHmFuK)X=Qa9O8-Wip?Wx|aq!^}zIn|*JN zE%A$0p2?7xiJR7Ml`g<`0Z&YJgZ?DTnnrmx{>_C1sNDQ>xcL zIwBWrq}ym{u7-Pa#Cdkv&atD=XKD27o(Arr2R-XBM?PqmsV~w1Fc{TMdxZh;T9dL^WCl!%mV2Cq8z*7GNtf!8=eA!=j^gmywMA zZqR4=5{kXxi(Vx1-t@EFK*IZ<4MM7@PaxRAA0GrArn@0&xF3o{pxnue%1tBiI*2>>zN@5VZ1NEIDfexbBL?n8yxhnq#$qp? z+?vTq`o9eM4vGFK_fmSfC4=S{6)#ddmc5uBp0L2}47 zgDsNw$5NaGhBwSIm0v@swSw@lzhi%V{`2e(QN{Jibjaf1XfEcQm(#$3p8xs|--Qlj zGYPer;LI7Nf2T44bS40A(*hyrkRcw6aXca2_a~k<1`yB-v_T0$gq+HmG;o=n&$NI~ z(g#0)QSdjDJonyOnP{pi#DmWugY-{7LHru?CZBx@1M@@PD!ltA6MBOC5E=OUTg2zh z5Qt9<{a{V5jJaM3GdlS2TJWDj!A8?EPNpTm|9Lj5jjZMMe<)~h&2jiY9*h~Kip5IvVfe2q&|`bKzv{+W3FEnN^168zeax9RDGwQ z{`l)4m+WVd;VZYT9s2Nuo5RS!`B5^r->@Cr;LN}w>sOs)BMCu0sEqr0JW3@yRs>G)Up|@f8omc!o_!Qo{!jJ`K|1^v(9%@EPHXgKJx!wEbAc>YFcR~SbfNGa zi>wedgq)&*bv2L*7lKS72$R*cBTH@nr&;ufWLnbp17ME(@onp#BfNn$JFSkxTGaruxy7(~`l9fJ)urW)DSElb49$D}%U2FZ`^}kR9yEhfKNg7x<6L#)z^$RQ`$yH~X4jDvJl8~Ua4njD3-he|y6`JshXBuUzw;{z8DMMhyP*FD1#QRfsz}yryTH!( zM(*b3po!7;Ze-Jacqv}r`=~u$R?bvDr>UA)7t#e|EIt+Ydjd(7 zV8U4+q53RBI~NprIk?xSFEHdkx~OMSvF7c)3; z73p-3eE`9Vs%<{4q#2ovlccx>)MC&22N^sRH5CJ@e%)5L9G7K;%(Q_=8Bsb%+Q2c! zTD2O=Oj3Dx_%~+C`fxc^hG2k$)A(gn!RT*QgNNIdNRe%f%-mcnuztT{KX9%VfI;*cb&`Co?5^=%_+I5KtIZn_J^>9=e&qfeAT4h%>>P7uQkB-AiAheKq0^{ z)fzQlbJ+&|ku!pIL()+mr;gviQ45+)UF}7H?_~ip_}KHAZ~!QiHVCQ6&cL2LGty&U zy9G>6f9>c0!=2B*G&D3qx*u{4N;QOZ!EWXk7Z-p006C?i4Qcg={{Ttmzc^ zA89{__$bmKL#@KoXBJJqcWT8N3!?!L=a1)u9sy#B?_C^z)?I>U_M3L?+Nzc{HIA4H znLMJ^MRnDv9wjU&e<7#8A8t2#It7F&q3Q(jrA87L&U}!)%z|QJ2kwQL=w8$EIq`_! z%bJ82c8j<0kQ+?Pn3EK^7;Lf(F;~v;4WX_IbIx1Dw-kZ&Lm(lw{644^4?l>AV49-T zE?#FUeMj=vMCk?d*J3Wq*4|4MQkWF?4m4+haItN;+qfuTY|~F-Zo)Uyugcq1rTc=x zmLQpzM8s@;^&LV|@oPem!PmUtNQG?bq>%WP&8;;=oaR#bK$RI@_fQB3i6ldoy9Sh?R*e%hD%3sTopq_1MJB{FcBM0nGm z9lA&>;tgE>9)1E2+N8^w8u}Qz`pf0eJY?88CJ_QJ zn`Uz*MtGf5XJCU)h~5uZ!*pWX%chYYmxqACd&3ch2~2OR^GCL9=j&3^(jHbKCqXupCG?4*eW4J}LPW;&xtJffnWYjBBQ&+L zVk8wz=;qx0MB!cSQ7{2da&=rIYjMGx-dGqFdz)L3a&_zDsV1{^!8Tsv)}$~9SgJ#4 zGn(^ax|Y2fDeTTs?df^kvR?J+8n_{(rt+!qeV36;h&h4o=UK|So; za)i+1&>xcxk|`CI6_SDn&QG?EF$P|~IMTzbW%*P=wdm5VZV9qF`LdW18#j-n!;1;7 zA)i=65RNO=8r>6=kE#^;73n72brsX$hrGv#@Tudp$cCo2%^7sk8t1K~N|Jb1 z?U54wCV{4VEtRZ+2bzcKvNCu;fEyYbUXM1_H%zbfdzWd6_@_Pz?Pm#Ob*(U%5C3xc z8Nn(pd{N+&KOnc8H%fr(v`Z}uEfPF;&KWQ@@cW@oy9npy6^z*ubXpT9u#t9FSv^(> z*uPzX+>h2Nb%;A5;M-zgq0x{^=gpf#^(u@APFlyKP#vZs#`g_NEi~5`*8cn@-UvYB zVO`l~H@qOzAur4rjk;H`qVUu*i2BxVntjWYx#iQ;4|rZ48jQH*z56^mGme(jUZ$(38c$lwu2mw}VQzj-Rl6Y(i@2(4`YO2-0-XC>R z6f9^CN1Yb%lE2yQ_8(c!&NXaJrg6$N#;G8EviKwDHd*p7Nlq8j(163y1wNV3t!lLQ zPoNE^O8dYd)$QzS6_SuS5XK*^$*f0=M@Gv*QEcde@ z4N=j22@*a80y>me+SoT$jak3256va*hUzVab3+0Mf8 z@EqUstNG_k&( zSsKgZIQUvjo8bcJ!&i&i$+m(8Y**s0u;u4_)JYo?#6+2Bm1FS9YV~L*e^}r;s&XB< zh#E7Tr>uGNLeJ~Hi_0Z_^=NsVrYfJ#XnnWnvm4_ zb)cy2J&re`&ZP&kx&$y0F_0J{be96H(z})x?&RRs{NtdvPMI&0E}KJta&$c`Yr4To zV}tYCWRyvuQ(1&whHNjAD)q~@^+8!_rRetH1h?_fa%y;=`)qjNA;Bk6NC^GU*4tzIo zYQ7$TnVC((&>mi5vDG3J|CIN=mP{TB)J`RX+u2!i2_^RQm5~rE8I0|Nsi(@&OVkVQ1Q$NMf;@pIb%ask3zdrhz1w?Q22_N?k8el}N#Ya)x* zD>Bwsh_kaY28yNN<@7fNRxkcX8qM}6@dS4r;UPow3k$=hY6T28_IrAyp^o*;_Od-DJiLqs;Ms;OmhGeCM2AQhzQgO!8w?_y2xBw$qjUfud`|-CtU6|b}1a- z8UmhW z)u@+hG&_DTx5m0?rjy1odO>7q{S3q@k8{SkdTuUB^mHNdcSs0;3MJ+ni4F3-r1WX+ zkn8B^5dGnmS;Qgp#8VUmE3+4(MY^?{fqhm;`TE*?N}r7GCLJi+w`~qZ z!I9_>%plM_6OdVS1ymdTbL$O$+$aAF$i#IYi zpCY0`bPk?H0^Ex4c^I1kL!H}fa4Dh?_MY1G^S!&4yWHV@X!777+#~T2a^1cq%g8%TBeqZ;Q45&{#KBUvXIBj zIw-5qzGyhqJ3r9mw0n95uJzYvnFSVGP5Aip4L|`c)XFNglEyB~Im`aL4V<_>?{=+i z`qJa`y>65*7TDTBs{{yNLHy;2`Q~lf&&kPTUV=d`icf8(n%Y_#nvUT&)d!>M?RUs- zp_~C??U)P&()+Q`V~AWh`ClR z4j-||y#yW0RWtmn*zd=F0<~XFzL`+cJ%O9Qa%7L8M2#_?X;TKH-5IWo)KjHi(=bgo9k67VR_W;(`(9tg{aE-Eg_m#(D<3&275mad>@iu;dRTQ%NMXNe5`&Xy%) zNuhYp;sh{os%v};$|bUU6X5cY@^O-1zm70!&aZB>)Sa*9Pl0Y-A|YX6b$U4Z{pj^} zPS5*9nl^370V2|D<}`CGhbQT3cOoyeh@j$+`gEpjyE1-AuHcp4$#~fb0q&Y;Tl}-Y z0KRh1GG0*UHW!*cx90AcVt;ElB;8|kz7H&FB_OS7 z==D;1M!poFI{gUiE~}u8gE?XKeN-f0?!*O-ZBpJFa=9SmW~HL*V=b-Fl0%ng0e)$) zp=rXigZgz_D;DKj0Z-*FCcCYVSc_`ZiYtl!w z&g9$-o`UOUkYo&Yt>fQ`3OeAgl6tX{ah|ZY2PM~8bL!a&Qe{k5n zNB+uF&|f#!P1yRXhwv)qyn?|bL>h+ew%g^e8)ZfJoEXE^3NN=6AE0PT&$m*Gx<2dZ z6R2b5IWmf45^&iVKKs}s@52%*qnr|?wcfHdr1|8nLO~2+bFp(S#ZAA{?QPsL`ILx(MHI2DTMkf5Iy0FD{KpM3Z6jU>?aH7rx z6Poyzr#LsHGWtbe(PY8xA+5Q)Bpk4Dzq*Kz)q*ZZ%+^@7mZf{(WM_Q7BYD8m&Ag&q ze<;SkmsiQJKbPTRV@}pTf$dWIxcpFYK3MHRf-75%vW~U@SCvNq*2N?qe%yADFKaxV zZXOZW8EYl1p2=l)#N?swUH}V=C?pFwi#~?UzyoF$gMC^1>ZvIX)d7OyCwIlM0G`iL zM1*9)rPRBYV6-^qB$ljM%@C!GMYJY`24~93Uri^gL*tQlVr2$DZCyA206K~x?EO>d8R3q!)Ul<7$`{^7(Dt0I8dR`r1ZPaD)~aP zjubY#zhRZ1(K7I|WWxEQ`Bpx-MW{t3>yk*PEQSb|inl;Bh|1Roj^^rgb8zZvB3GPh z5C~Y(qIWuE`M9z3B(%wYw?!34ER?qesNbZ}8UWLcb;!KIW)xSBCzmsOCdUn-W?ysF zV&pySH?}kf{ZUW-_Qs%gzH#7Q=Hc?-bSXsl_h&_hzQMT1Ij>V z+I8TEy~#(s=e}Tp$5;78F}`kk)YqxJ(_F>Pd9^R*88Rd$AO7opvb98HhJnu62rEjpQ9y@Q+v=bAlaW z{CAn&@{MdSju*eSXhBGDJ{so?gXaiFOsjz~OQl}FEN%H89$6?^6vh!jdasbn$772`X8bPww$&0ls}}(7mTZ7!T1CB`Ea@4 zX$XwuE3b92_>%dUdu{Icvb5Guc5s)EIn8pqqnFC(<}n5twOE|`8PW!JdMIISIY&xw z*e8)%UcXk^8B-8>2ic_GlWsbTG0S|HnuGX`j6@m1Zy|lJsR<~u&U@HfiDN?myFA&5 zoiBOjkNkb#pdl6Qp_Wi0&7p>Cvyo;jg8&z^SR8e)tV}o41Xh$h(cK@<;F|q~88gP9 zL8IvR5?Rc4$vQ4tF7tImq%46_vKZAYm{weJ7|f%BsGap4e+eO;!Bx47;%t|BogywP z*TyS)xKD8Jk{B%3=w^8gz0bQj(jv55(tVdF{nf@kFNG-6Rym1A+&#Vhj5jt&T`hw^N@(IiRV6 zhUzB=zxjR=xOkV&7OGmU*S;w?suF9)fBiTe@|}+w9vcQml-;S>wH_JA{dP#|IB2hjwd0+lTjt z?-do)K1a-v>Ux=RsBa(i(X>#T`|0JLoo$ z?BMt&x%yih(6WEHVQOt{&0)X9STfag)FBAuVzp9qE?s*(TH2Os&9>{!W8HR-Wr!If zeI|JXCfwqhsuPr%LKocLY++zothS%p-E!L7kMfagr_;E66)aqTay|$@JI*qucIbJGJ(~8}0q<%u%980muVg34HQxl;O8Z)+1z4D7!PJ=HKb- zLR-itiVf*I8}2(x&i1G@pL>8#e%pCmSCa~I+pVIaGS}7L5S4VJy6f@N{&0-`{@ypn z#s=vu(Ekzk`OzOP;^vJ6Sj$ZSA1k3-vhE`=Isra;1Fx$7G9&D$WuDNxmq{U$hwhOl~QP~7ztL$Pvs`A8BPHPEdLwYh0^){N%+>!m{3i)m&V ztYX0yII_K0fihLbI(Da+%iAzA$H$Z8tjj_OXa&6_bQll z^(uu75qMXe0f?sZ5|BsT3LKf~>K@yNcI8_a+v7iY{0SaFxHGTE3OPl3_Usw*#lcOl ze#fUr)_}J1debjQD-RU+?8G1y{DnC=>s`Y}HOT*1OFU&qE7M6{Ac*Ll^RZz?K1c8a z%%T%6Z4{%3!A8JTCncjGV(EIb5+2V&PR7*Xketme>IbIcqRKoZ7ebpC~h8oHDKB_2sahx z5x^pbEKUDwzJD9Pqmd}~?3v#{z`tfJq0HE;K?PkZ&^w6uuWppC&E*x(n!yUXu>P&( zQ7`((5d(R9;19g}1lfc>N?uTZW#lAri~9T|9p}HwOTNsZL3E$|$C?S{Kv%X6B1j$b zzi)La{=5~8p8Q`cofWB$QJ-fcsLnACotD|X>M;BkUi?>bP?^J~`l}w!Mx{f(cL0Na zl-oWtTkZmRj$AWvFRR)JH45a3M{h@qSYKmQSuA$is2I^2G&-gW1ZwRz7L1~t$l!bk zPIT5X-Ri%JDNtV$x!Mgv$Q~V|U}{j0Up0v6>inQ{LnE`hN6)@>%TgCFuU$B@8N`W1 zY@;xVFjVm03muG%e`zKyt1(Nmw(cxb&){f0Hqhu|JlX0t{<) z?_-nbZyZzTF7o0xiyDsdJbDrKbitTaz8|)~zu2g0ENxXb- zkB;hGC_b-4>TV*b@3NsdNNq_MJQWvi*2r<8C`Fo^PL``w+^bp{f8ACEIm)E;$r%D# zf1;_g@5mcJ)cJYlPA`ed7or``QujXg-wRLYGO|KsPo6KSERW+KoB<>bqQc}*sFF!e zc9#A4J7}tVa-_Mwsi_A%i5-u1W|{JnWp9uMf?3*kAB6Q}Kg`hz1nUdo(m$iBB z3(x4&GL5ie*4XL1Bm0?h0^x79)x?-mPk#LNajmC$2T;&j1@R8pk=;6m!|AcM@`Jj| zol+|uD3k^0PJbwMI%rp!-3X$@NUG;duz7eBJe&>dm<}xua=75*@a)bIH`8S>REl^0 zMV&L-y&dttx{fyI8}%744hk^D$BG5_EnP(i{}B*MuKD~Y3bq!m&?e+gr9Ql5Gg9jL z2C-qX6?pV=f`$|cUPC(tu$kB>sCst{`4vCcpjz$nNITw$!9a8rM^%=2uq6AR z$p9JID7(#k6oe5IQk`WsStcSf?^hpWa>#qtu0ovi`(*L3j@sqDY1wKc_A4_xn4&_1 zWQ?Kq4y#zI^wAtDt$ zTL{o)kG_1KX?S*nPGqdX6KK9tGWr=Frjui8<=&#DVeQkv0cYdrpXM#vPq1mD7z|cO zG1Yk@J!aVxywwWgB?$<24G);kH7^d>4W~V7b|R2!>!if5iyP7@4W$L^_i98*JN%S}~hd(V+q&SH$e!>LKY&IKB25BEPJsObbr zsci*SXqFClTSAwo?D;yp⋙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 delta 90429 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 diff --git a/res/css/structures/_ThreadsActivityCentre.pcss b/res/css/structures/_ThreadsActivityCentre.pcss index a1472108ac..f26139da64 100644 --- a/res/css/structures/_ThreadsActivityCentre.pcss +++ b/res/css/structures/_ThreadsActivityCentre.pcss @@ -49,12 +49,12 @@ &:hover, &:hover .mx_ThreadsActivityCentreButton_Icon { background-color: $quaternary-content; - color: $primary-content; + fill: $primary-content; } } & .mx_ThreadsActivityCentreButton_Icon { - color: $secondary-content; + fill: $secondary-content; } } diff --git a/res/css/views/rooms/_RoomHeader.pcss b/res/css/views/rooms/_RoomHeader.pcss index afde8f3464..0e2b40e0bd 100644 --- a/res/css/views/rooms/_RoomHeader.pcss +++ b/res/css/views/rooms/_RoomHeader.pcss @@ -103,5 +103,5 @@ Please see LICENSE files in the repository root for full details. } .mx_RoomHeader .mx_RoomHeader_toggled { - color: var(--cpd-color-icon-accent-primary); + fill: var(--cpd-color-icon-accent-primary); } diff --git a/src/components/views/right_panel/BaseCard.tsx b/src/components/views/right_panel/BaseCard.tsx index a564a60695..b308e1d973 100644 --- a/src/components/views/right_panel/BaseCard.tsx +++ b/src/components/views/right_panel/BaseCard.tsx @@ -76,7 +76,7 @@ const BaseCard: React.FC = ({ data-testid="base-card-back-button" onClick={onBackClick} tooltip={label} - subtleBackground + kind="secondary" > @@ -92,7 +92,7 @@ const BaseCard: React.FC = ({ onClick={onClose ?? closeRightPanel} ref={closeButtonRef} tooltip={closeLabel ?? _t("action|close")} - subtleBackground + kind="secondary" > diff --git a/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx b/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx index 892f2b56b7..676501a3ea 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx @@ -39,7 +39,7 @@ export function RoomListPrimaryFilters({ vm }: RoomListPrimaryFiltersProps): JSX > {displayChevron && ( setIsExpanded((_expanded) => !_expanded)} > - + )}

+ {shouldShowComponent(UIComponent.AddIntegrations) && ( + + )} {body} ); From 28a232eea849643450e7d07ef79f58248f2229e0 Mon Sep 17 00:00:00 2001 From: ElementRobot Date: Mon, 16 Jun 2025 01:23:43 -0500 Subject: [PATCH 08/12] [create-pull-request] automated change (#30144) Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com> --- src/i18n/strings/cs.json | 11 +++- src/i18n/strings/el.json | 93 +++++++++++++++++++++++++-- src/i18n/strings/nb_NO.json | 4 ++ src/i18n/strings/ru.json | 121 +++++++++++++++++++++++++++++------- 4 files changed, 199 insertions(+), 30 deletions(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index d736aa3d6f..7c9773881e 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -788,6 +788,7 @@ "cross_signing_status": "Stav křížového podepisování:", "cross_signing_untrusted": "Váš účet má v bezpečném úložišti identitu pro křížový podpis, ale v této relaci jí zatím nevěříte.", "crypto_not_available": "Kryptografický modul není k dispozici", + "device_id": "ID zařízení", "key_backup_active_version": "Verze aktivní zálohy:", "key_backup_active_version_none": "Žádné", "key_backup_inactive_warning": "Vaše klíče nejsou z této relace zálohovány.", @@ -1958,6 +1959,7 @@ }, "face_pile_tooltip_shortcut": "Včetně %(commaSeparatedMembers)s", "face_pile_tooltip_shortcut_joined": "Včetně vás, %(commaSeparatedMembers)s", + "failed_determine_user": "Nelze určit, kterého uživatele ignorovat, protože se změnila událost člena.", "failed_reject_invite": "Nepodařilo se odmítnout pozvánku", "forget_room": "Zapomenout na tuto místnost", "forget_space": "Zapomenout tento prostor", @@ -2050,6 +2052,7 @@ "read_topic": "Klikněte pro přečtení tématu", "rejecting": "Odmítání pozvánky…", "rejoin_button": "Znovu vstoupit", + "room_is_low_priority": "Toto je místnost s nízkou prioritou", "search": { "all_rooms_button": "Vyhledávat ve všech místnostech", "placeholder": "Hledat zprávy…", @@ -2690,6 +2693,9 @@ "inline_url_previews_room": "Povolit náhledy URL adres pro členy této místnosti jako výchozí", "inline_url_previews_room_account": "Povolit náhledy URL adres pro tuto místnost (ovlivňuje pouze vás)", "insert_trailing_colon_mentions": "Vložit dvojtečku za zmínku o uživateli na začátku zprávy", + "invite_controls": { + "default_label": "Povolit uživatelům pozvat vás do místností" + }, "jump_to_bottom_on_send": "Po odeslání zprávy přejít na konec časové osy", "key_backup": { "backup_in_progress": "Klíče se zálohují (první záloha může trvat pár minut).", @@ -2756,6 +2762,7 @@ "show_in_private": "V soukromých místnostech", "show_media": "Vždy zobrazit" }, + "not_supported": "Váš server tuto funkci neimplementuje.", "notifications": { "default_setting_description": "Toto nastavení se ve výchozím stavu použije pro všechny vaše místnosti.", "default_setting_section": "Chci být upozorňován na (Výchozí nastavení)", @@ -2813,6 +2820,7 @@ "voip": "Hlasové a video hovory" }, "preferences": { + "Electron.enableContentProtection": "Zabraňte zachycení obsahu okna jinými aplikacemi", "Electron.enableHardwareAcceleration": "Povolit hardwarovou akceleraci (restaurtujte %(appName)s, aby se změna projevila)", "always_show_menu_bar": "Vždy zobrazovat horní lištu okna", "autocomplete_delay": "Zpožnění našeptávače (ms)", @@ -2986,6 +2994,7 @@ "show_chat_effects": "Zobrazit efekty chatu (animace např. při přijetí konfet)", "show_displayname_changes": "Zobrazovat změny zobrazovaného jména", "show_join_leave": "Zobrazit zprávy o vstupu/odchodu (pozvánky/odebrání/vykázání nejsou ovlivněny)", + "show_message_previews": "Zobrazit náhledy zpráv", "show_nsfw_content": "Zobrazit NSFW obsah", "show_read_receipts": "Zobrazovat potvrzení o přečtení", "show_redaction_placeholder": "Zobrazovat smazané zprávy", @@ -3132,7 +3141,7 @@ "upgraderoom": "Aktualizuje místnost na novou verzi", "upgraderoom_permission_error": "Na provedení tohoto příkazu nemáte dostatečná oprávnění.", "usage": "Použití", - "verify": "Ověří uživatele, relaci a veřejné klíče", + "verify": "Ruční ověření jednoho ze svých vlastních zařízení", "view": "Zobrazí místnost s danou adresou", "whois": "Zobrazuje informace o uživateli" }, diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index 3a25563601..70efbb20e9 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -2021,6 +2021,7 @@ "jump_to_bottom_on_send": "Μεταβείτε στο τέλος του χρονολογίου όταν στέλνετε ένα μήνυμα", "key_backup": { "backup_in_progress": "Δημιουργούνται αντίγραφα ασφαλείας των κλειδιών σας (το πρώτο αντίγραφο ασφαλείας μπορεί να διαρκέσει μερικά λεπτά).", + "backup_starting": "Έναρξη δημιουργίας αντιγράφων ασφαλείας...", "backup_success": "Επιτυχία!", "cannot_create_backup": "Δεν είναι δυνατή η δημιουργία αντιγράφου ασφαλείας κλειδιού", "create_title": "Δημιουργία αντιγράφου ασφαλείας κλειδιού", @@ -2030,8 +2031,8 @@ "description": "Προστατευτείτε από την απώλεια πρόσβασης σε κρυπτογραφημένα μηνύματα και δεδομένα, δημιουργώντας αντίγραφα ασφαλείας των κλειδιών κρυπτογράφησης στον διακομιστή σας.", "enter_phrase_title": "Εισαγάγετε τη Φράση Ασφαλείας", "enter_phrase_to_confirm": "Εισαγάγετε τη Φράση Ασφαλείας σας για δεύτερη φορά για να την επιβεβαιώσετε.", - "generate_security_key_description": "Θα δημιουργήσουμε ένα κλειδί ασφαλείας για να το αποθηκεύσετε σε ασφαλές μέρος, όπως έναν διαχειριστή κωδικών πρόσβασης ή ένα χρηματοκιβώτιο.", - "generate_security_key_title": "Δημιουργήστε ένα κλειδί ασφαλείας", + "generate_security_key_description": "Θα δημιουργήσουμε ένα κλειδί ανάκτησης για να το αποθηκεύσετε σε ασφαλές μέρος, όπως έναν διαχειριστή κωδικών πρόσβασης ή ένα χρηματοκιβώτιο.", + "generate_security_key_title": "Δημιουργήστε ένα Κλειδί Ανάκτησης", "pass_phrase_match_failed": "Αυτό δεν ταιριάζει.", "pass_phrase_match_success": "Ταιριάζει!", "phrase_strong_enough": "Τέλεια! Αυτή η Φράση Ασφαλείας φαίνεται αρκετά ισχυρή.", @@ -2044,7 +2045,7 @@ "title_set_phrase": "Ορίστε μια Φράση Ασφαλείας", "unable_to_setup": "Δεν είναι δυνατή η ρύθμιση του μυστικού χώρου αποθήκευσης", "use_different_passphrase": "Να χρησιμοποιηθεί διαφορετική φράση;", - "use_phrase_only_you_know": "Χρησιμοποιήστε μια μυστική φράση που γνωρίζετε μόνο εσείς και προαιρετικά αποθηκεύστε ένα κλειδί ασφαλείας για να το χρησιμοποιήσετε για τη δημιουργία αντιγράφων ασφαλείας." + "use_phrase_only_you_know": "Χρησιμοποιήστε μια μυστική φράση που γνωρίζετε μόνο εσείς και, προαιρετικά, αποθηκεύστε ένα Κλειδί Ανάκτησης για να το χρησιμοποιήσετε ως αντίγραφο ασφαλείας." } }, "key_export_import": { @@ -2145,6 +2146,7 @@ "prompt_invite": "Ερώτηση πριν από την αποστολή προσκλήσεων σε δυνητικά μη έγκυρα αναγνωριστικά matrix", "replace_plain_emoji": "Αυτόματη αντικατάσταση απλού κειμένου Emoji", "security": { + "analytics_description": "Μοιραστείτε ανώνυμα δεδομένα για να μας βοηθήσετε να εντοπίσουμε προβλήματα. Τίποτα προσωπικό. Χωρίς τρίτους.", "bulk_options_accept_all_invites": "Αποδεχτείτε όλες τις %(invitedRooms)sπροσκλήσεις", "bulk_options_reject_all_invites": "Απόρριψη όλων των προσκλήσεων %(invitedRooms)s", "bulk_options_section": "Μαζικές επιλογές", @@ -2175,12 +2177,14 @@ "message_search_unsupported_web": "Το %(brand)s δεν μπορεί να αποθηκεύσει με ασφάλεια κρυπτογραφημένα μηνύματα τοπικά ενώ εκτελείται σε πρόγραμμα περιήγησης ιστού. Χρησιμοποιήστε την %(brand)s Επιφάνεια εργασίας για να εμφανίζονται κρυπτογραφημένα μηνύματα στα αποτελέσματα αναζήτησης.", "record_session_details": "Κατέγραψε το όνομα του πελάτη, την έκδοση και τη διεύθυνση URL για να αναγνωρίζεις τις συνεδρίες πιο εύκολα στον διαχειριστή συνεδρίας", "send_analytics": "Αποστολή δεδομένων αναλυτικών στοιχείων", - "strict_encryption": "Μη στέλνετε ποτέ κρυπτογραφημένα μηνύματα σε μη επαληθευμένες συνεδρίες από αυτήν τη συνεδρία" + "strict_encryption": "Αποστολή μηνυμάτων μόνο σε επαληθευμένους χρήστες" }, "send_read_receipts": "Αποστολή αποδείξεων ανάγνωσης", "send_read_receipts_unsupported": "Ο διακομιστής σου δεν υποστηρίζει την απενεργοποίηση αποστολής αποδείξεων ανάγνωσης.", "send_typing_notifications": "Αποστολή ειδοποιήσεων πληκτρολόγησης", "sessions": { + "best_security_note": "Για τη βέλτιστη ασφάλεια, επαληθεύστε τις συνεδρίες σας και αποσυνδεθείτε από οποιαδήποτε συνεδρία που δεν αναγνωρίζετε ή χρησιμοποιείτε πλέον.", + "browser": "Πρόγραμμα περιήγησης", "confirm_sign_out": { "one": "Επιβεβαιώστε την αποσύνδεση αυτής της συσκευής", "other": "Επιβεβαιώστε την αποσύνδεση αυτών των συσκευών" @@ -2197,8 +2201,83 @@ "one": "Επιβεβαιώστε ότι αποσυνδέεστε από αυτήν τη συσκευή χρησιμοποιώντας Single Sign On για να αποδείξετε την ταυτότητά σας.", "other": "Επιβεβαιώστε την αποσύνδεση από αυτές τις συσκευές χρησιμοποιώντας Single Sign On για να αποδείξετε την ταυτότητά σας." }, + "current_session": "Τρέχουσα συνεδρία", + "desktop_session": "Συνεδρία εφαρμογής υπολογιστή", + "details_heading": "Λεπτομέρειες συνεδρίας", + "device_unverified_description": "Επαληθεύστε ή αποσυνδεθείτε από αυτήν τη συνεδρία για βέλτιστη ασφάλεια και αξιοπιστία.", + "device_unverified_description_current": "Επαληθεύστε την τρέχουσα συνεδρία σας για βελτιωμένα ασφαλή μηνύματα.", + "device_verified_description": "Αυτή η συνεδρία είναι έτοιμη για ασφαλή ανταλλαγή μηνυμάτων.", + "device_verified_description_current": "Η τρέχουσα συνεδρία σας είναι έτοιμη για ασφαλή ανταλλαγή μηνυμάτων.", + "error_pusher_state": "Αποτυχία ορισμού κατάστασης pusher", + "error_set_name": "Αποτυχία ορισμού ονόματος συνεδρίας", + "filter_all": "Όλα", + "filter_inactive": "Ανενεργό", + "filter_inactive_description": "Ανενεργό για%(inactiveAgeDays)s ημέρες ή και περισσότερο", + "filter_label": "Φιλτράρισμα συσκευών", + "filter_unverified_description": "Δεν είναι έτοιμο για ασφαλή ανταλλαγή μηνυμάτων", + "filter_verified_description": "Έτοιμο για ασφαλή ανταλλαγή μηνυμάτων", + "hide_details": "Απόκρυψη λεπτομερειών", + "inactive_days": "Ανενεργό για %(inactiveAgeDays)s+ ημέρες", + "inactive_sessions": "Ανενεργές συνεδρίες", + "inactive_sessions_explainer_1": "Οι ανενεργές συνεδρίες είναι συνεδρίες που δεν έχετε χρησιμοποιήσει για κάποιο χρονικό διάστημα, αλλά συνεχίζουν να λαμβάνουν κλειδιά κρυπτογράφησης.", + "inactive_sessions_explainer_2": "Η αφαίρεση ανενεργών συνεδριών βελτιώνει την ασφάλεια και την απόδοση και σας διευκολύνει να εντοπίσετε αν μια νέα συνεδρία είναι ύποπτη.", + "inactive_sessions_list_description": "Εξετάστε το ενδεχόμενο να αποσυνδεθείτε από παλιές συνεδρίες (%(inactiveAgeDays)s ημέρες ή παλαιότερες) που δεν χρησιμοποιείτε πλέον.", + "ip": "Διεύθυνση IP", + "last_activity": "Τελευταία δραστηριότητα", + "mobile_session": "Συνεδρία κινητού", + "n_sessions_selected": { + "one": "%(count)s επιλεγμένη συνεδρία", + "other": "%(count)s επιλεγμένες συνεδρίες" + }, + "no_inactive_sessions": "Δεν βρέθηκαν ανενεργές συνεδρίες.", + "no_sessions": "Δεν βρέθηκαν συνεδρίες.", + "no_unverified_sessions": "Δεν βρέθηκαν μη επαληθευμένες συνεδρίες.", + "no_verified_sessions": "Δεν βρέθηκαν επαληθευμένες συνεδρίες.", + "os": "Λειτουργικό σύστημα", + "other_sessions_heading": "Άλλες συνεδρίες", + "push_heading": "Ειδοποιήσεις push", + "push_subheading": "Λάβετε ειδοποιήσεις push σε αυτήν τη συνεδρία.", + "push_toggle": "Ενεργοποίηση/απενεργοποίηση ειδοποιήσεων push σε αυτήν τη συνεδρία.", + "rename_form_caption": "Λάβετε υπόψη ότι τα ονόματα των συνεδριών είναι επίσης ορατά στα άτομα με τα οποία επικοινωνείτε.", + "rename_form_heading": "Μετονομασία συνεδρίας", + "rename_form_learn_more": "Μετονομασία συνεδριών", + "rename_form_learn_more_description_1": "Άλλοι χρήστες σε απευθείας μηνύματα και αίθουσες στις οποίες συμμετέχετε μπορούν να δουν μια πλήρη λίστα των συνεδριών σας.", + "rename_form_learn_more_description_2": "Αυτό τους παρέχει την εμπιστοσύνη ότι πραγματικά μιλούν με εσάς, αλλά σημαίνει επίσης ότι μπορούν να δουν το όνομα της συνεδρίας που εισάγετε εδώ.", + "security_recommendations": "Συστάσεις ασφαλείας", + "security_recommendations_description": "Βελτιώστε την ασφάλεια του λογαριασμού σας ακολουθώντας αυτές τις συστάσεις.", "session_id": "Αναγνωριστικό συνεδρίας", - "verify_session": "Επαλήθευση συνεδρίας" + "show_details": "Εμφάνιση λεπτομερειών", + "sign_in_with_qr": "Σύνδεση νέας συσκευής", + "sign_in_with_qr_button": "Εμφάνιση κωδικού QR", + "sign_in_with_qr_description": "Χρησιμοποιήστε έναν κωδικό QR για να συνδεθείτε σε άλλη συσκευή και να ρυθμίσετε την ασφαλή ανταλλαγή μηνυμάτων.", + "sign_out": "Αποσυνδεθείτε από αυτήν τη συνεδρία", + "sign_out_all_other_sessions": "Αποσύνδεση από όλες τις άλλες συνεδρίες (%(otherSessionsCount)s)", + "sign_out_confirm_description": { + "one": "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε από τη %(count)s συνεδρία;", + "other": "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε από τις %(count)s συνεδρίες;" + }, + "sign_out_n_sessions": { + "one": "Αποσύνδεση από %(count)s συνεδρία", + "other": "Αποσύνδεση από %(count)s συνεδρίες" + }, + "title": "Συνεδρίες", + "unknown_session": "Άγνωστος τύπος συνεδρίας", + "unverified_session": "Μη επαληθευμένη συνεδρία", + "unverified_session_explainer_1": "Αυτή η συνεδρία δεν υποστηρίζει κρυπτογράφηση και συνεπώς δεν μπορεί να επαληθευτεί.", + "unverified_session_explainer_2": "Δεν θα μπορείτε να συμμετάσχετε σε αίθουσες όπου είναι ενεργοποιημένη η κρυπτογράφηση κατά τη χρήση αυτής της συνεδρίας.", + "unverified_session_explainer_3": "Για καλύτερη ασφάλεια και ιδιωτικότητα, συνιστάται να χρησιμοποιείτε εφαρμογές Matrix που υποστηρίζουν κρυπτογράφηση.", + "unverified_sessions": "Μη επαληθευμένες συνεδρίες", + "unverified_sessions_explainer_1": "Οι μη επαληθευμένες συνεδρίες είναι συνεδρίες στις οποίες έχετε συνδεθεί με τα διαπιστευτήριά σας, αλλά δεν έχουν επαληθευτεί.", + "unverified_sessions_explainer_2": "Θα πρέπει να βεβαιωθείτε ιδιαίτερα ότι αναγνωρίζετε αυτές τις συνεδρίες, καθώς ενδέχεται να αποτελούν μη εξουσιοδοτημένη χρήση του λογαριασμού σας.", + "unverified_sessions_list_description": "Επαληθεύστε τις συνεδρίες σας για βελτιωμένα ασφαλή μηνύματα ή αποσυνδεθείτε από αυτές που δεν αναγνωρίζετε ή χρησιμοποιείτε πλέον.", + "url": "URL", + "verified_session": "Επαληθευμένη συνεδρία", + "verified_sessions": "Επαληθευμένες συνεδρίες", + "verified_sessions_explainer_1": "Οι επαληθευμένες συνεδρίες είναι οπουδήποτε χρησιμοποιείτε αυτόν τον λογαριασμό αφού εισαγάγετε τη φράση πρόσβασής σας ή επιβεβαιώσετε την ταυτότητά σας με άλλη επαληθευμένη συνεδρία.", + "verified_sessions_explainer_2": "Αυτό σημαίνει ότι έχετε όλα τα κλειδιά που απαιτούνται για να ξεκλειδώσετε τα κρυπτογραφημένα μηνύματά σας και να επιβεβαιώσετε σε άλλους χρήστες ότι εμπιστεύεστε αυτήν τη συνεδρία.", + "verified_sessions_list_description": "Για βέλτιστη ασφάλεια, αποσυνδεθείτε από οποιαδήποτε συνεδρία που δεν αναγνωρίζετε ή χρησιμοποιείτε πλέον.", + "verify_session": "Επαλήθευση συνεδρίας", + "web_session": "Συνεδρία web" }, "show_avatar_changes": "Εμφάνιση αλλαγών εικόνας προφίλ", "show_breadcrumbs": "Εμφάνιση συντομεύσεων σε δωμάτια που προβλήθηκαν πρόσφατα πάνω από τη λίστα δωματίων", @@ -2219,6 +2298,7 @@ "metaspaces_orphans_description": "Ομαδοποιήστε σε ένα μέρος όλα τα δωμάτιά σας που δεν αποτελούν μέρος ενός χώρου.", "metaspaces_people_description": "Ομαδοποιήστε όλα τα άτομα σας σε ένα μέρος.", "metaspaces_subsection": "Χώροι για εμφάνιση", + "spaces_explainer": "Οι χώροι είναι τρόποι ομαδοποίησης αιθουσών και ανθρώπων. Παράλληλα με τους χώρους στους οποίους βρίσκεστε, μπορείτε να χρησιμοποιήσετε και κάποιους προκατασκευασμένους χώρους.", "title": "Πλαϊνή μπάρα" }, "start_automatically": "Αυτόματη έναρξη μετά τη σύνδεση", @@ -2249,7 +2329,8 @@ "voice_processing": "Επεξεργασία φωνής", "voice_section": "Ρυθμίσεις φωνής" }, - "warn_quit": "Προειδοποιήστε πριν την παραίτηση" + "warn_quit": "Προειδοποιήστε πριν την παραίτηση", + "warning": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: " }, "share": { "permalink_message": "Σύνδεσμος στο επιλεγμένο μήνυμα", diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json index e0f08b7ff8..dd8b57a1b8 100644 --- a/src/i18n/strings/nb_NO.json +++ b/src/i18n/strings/nb_NO.json @@ -1023,6 +1023,7 @@ "no_userid": "Kan ikke verifisere enheten - finner ikke bruker-ID", "success_description": "Enheten (%(deviceId)s) er nå krysssignert", "success_title": "Verifiseringen var vellykket", + "text": "Oppgi ID-en og fingeravtrykket til en av dine egne enheter for å verifisere det. MERK at dette lar den andre enheten sende og motta meldinger som deg. HVIS NOEN HAR FORTALT DEG BARE Å LIME INN NOE HER, ER DET SANNSYNLIGVIS AT DU BLIR SVINDLET!", "wrong_fingerprint": "Kan ikke verifisere enheten %(deviceId)s '- det medfølgende fingeravtrykket'%(fingerprint)s «samsvarer ikke med enhetens fingeravtrykk»%(fprint)s '" }, "no_key_or_device": "Det ser ut til at du ikke har en gjenopprettingsnøkkel eller andre enheter du kan verifisere mot. Denne enheten vil ikke kunne få tilgang til gamle krypterte meldinger. For å bekrefte identiteten din på denne enheten, må du tilbakestille verifiseringsnøklene dine.", @@ -2065,6 +2066,7 @@ "read_topic": "Klikk for å lese emnet", "rejecting": "Avviser invitasjon...", "rejoin_button": "Bli med igjen", + "room_content": "Rominnhold", "room_is_low_priority": "Dette er et lavt prioritert rom", "search": { "all_rooms_button": "Søk i alle rom", @@ -2114,6 +2116,7 @@ "add_space_label": "Legg til område", "breadcrumbs_empty": "Ingen nylig besøkte rom", "breadcrumbs_label": "Nylig besøkte rom", + "collapse_filters": "Skjul filterlisten", "empty": { "no_chats": "Ingen chatter ennå", "no_chats_description": "Kom i gang ved å sende meldinger til noen eller ved å opprette et rom", @@ -3112,6 +3115,7 @@ "jumptodate": "Gå til den gitte datoen i tidslinjen", "jumptodate_invalid_input": "Vi klarte ikke å forstå den gitte datoen (%(inputDate)s). Prøv å bruke formatet ÅÅÅÅ-MM-DD.", "lenny": "Legger til ( ͡° ͜ʖ ͡°) foran en ren tekstmelding", + "manual_device_verification_confirm_description": "Dette vil tillate en annen enhet å sende og motta meldinger som deg. HVIS NOEN BA DEG LIME INN NOE HER, ER DET SANNSYNLIG AT DU BLIR LURT! Er du sikker på at du vil bekrefte denne andre enheten?", "manual_device_verification_confirm_title": "Forsiktig: manuell enhetsverifisering", "me": "Viser handling", "msg": "Sender en melding til den angitte brukeren", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 44eeefdf92..126a915ca1 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -762,6 +762,7 @@ "backup_key_not_stored": "не сохранено", "backup_key_stored": "в секретном хранилище", "backup_key_stored_status": "Сохраненный резервный ключ:", + "backup_key_unexpected_type": "непредвиденный тип", "backup_key_well_formed": "корректный", "cross_signing": "Кросс-подпись", "cross_signing_cached": "сохранено локально", @@ -775,6 +776,7 @@ "cross_signing_status": "Статус кросс-подписи:", "cross_signing_untrusted": "У вашей учётной записи есть кросс-подпись в секретное хранилище, но она пока не является доверенной в этом сеансе.", "crypto_not_available": "Криптографический модуль недоступен", + "device_id": "Идентификатор устройства", "key_backup_active_version": "Активная резервная версия:", "key_backup_active_version_none": "Нет", "key_backup_inactive_warning": "Резервное копирование ваших ключей из этого сеанса не выполняется.", @@ -787,6 +789,8 @@ "secret_storage_ready": "готово", "secret_storage_status": "Секретное хранилище:", "self_signing_private_key_cached_status": "Самоподписанный закрытый ключ:", + "session": "Сессия", + "session_fingerprint": "Отпечаток пальца (ключ сессии)", "title": "Сквозное шифрование", "user_signing_private_key_cached_status": "Закрытый ключ подписи пользователей:" }, @@ -812,6 +816,7 @@ "low_bandwidth_mode": "Режим низкой пропускной способности", "low_bandwidth_mode_description": "Требуется совместимый сервер.", "main_timeline": "Основная хронология", + "manual_device_verification": "Ручная проверка устройства", "no_receipt_found": "Квитанция не найдена", "notification_state": "Состояние уведомления %(notificationState)s", "notifications_debug": "Отладка уведомлений", @@ -996,6 +1001,15 @@ "incoming_sas_dialog_waiting": "Ожидаем подтверждения от партнера…", "incoming_sas_user_dialog_text_1": "Проверить этого пользователя, чтобы отметить его, как доверенного. Доверенные пользователи дают вам больше уверенности при использовании шифрованных сообщений.", "incoming_sas_user_dialog_text_2": "Подтверждение этого пользователя сделает его сеанс доверенным у вас, а также сделает ваш сеанс доверенным у него.", + "manual": { + "already_verified": "Это устройство уже проверено", + "already_verified_and_wrong_fingerprint": "Предоставленный отпечаток пальца не совпадает, но устройство уже проверено!", + "device_id": "Идентификатор устройства", + "failure_description": "Не удалось проверить '%(deviceId)s': %(error)s", + "failure_title": "Сбой проверки", + "fingerprint": "Отпечаток пальца (ключ сессии)", + "success_title": "Проверка прошла успешно" + }, "no_key_or_device": "Похоже, у вас нет Ключа Восстановления, или других сеансов, с которыми вы могли бы свериться. В этом сеансе вы не сможете получить доступ к старым зашифрованным сообщениям. Чтобы подтвердить свою личность в этом сеансе, вам нужно будет сбросить свои ключи шифрования.", "no_support_qr_emoji": "Устройство, которое вы пытаетесь проверить, не поддерживает сканирование QR-кода или проверку смайликов, которые поддерживает %(brand)s. Попробуйте использовать другой клиент.", "other_party_cancelled": "Другая сторона отменила проверку.", @@ -1035,7 +1049,7 @@ "unverified_sessions_toast_description": "Проверьте, чтобы убедиться, что ваша учётная запись в безопасности", "unverified_sessions_toast_reject": "Позже", "unverified_sessions_toast_title": "У вас есть незаверенные сеансы", - "verification_description": "Подтвердите свою личность, чтобы получить доступ к зашифрованным сообщениям и доказать свою личность другим.", + "verification_description": "Подтвердите свою личность, чтобы получить доступ к зашифрованным сообщениям и подтвердить свою личность другим. Если вы также используете мобильное устройство, откройте приложение там, прежде чем продолжить.", "verification_dialog_title_device": "Проверить другое устройство", "verification_dialog_title_user": "Запрос на сверку", "verification_skip_warning": "Без проверки вы не сможете получить доступ ко всем своим сообщениям и можете показаться другим людям недоверенным.", @@ -1054,8 +1068,10 @@ "waiting_other_user": "Ожидание %(displayName)s для проверки…" }, "verification_requested_toast_title": "Запрошено подтверждение", + "verified_identity_changed": "Подтвержденная личность %(displayName)s (%(userId)s) изменилась. Узнайте больше", "verify_toast_description": "Другие пользователи могут не доверять этому сеансу", - "verify_toast_title": "Заверьте этот сеанс" + "verify_toast_title": "Заверьте этот сеанс", + "withdraw_verification_action": "Подтверждение верификации" }, "error": { "admin_contact": "Пожалуйста, обратитесь к вашему администратору, чтобы продолжить использовать этот сервис.", @@ -1096,13 +1112,14 @@ "unknown_error_code": "неизвестный код ошибки", "update_power_level": "Не удалось изменить уровень прав" }, - "error_app_open_in_another_tab": "%(brand)s был открыт в другой вкладке.", + "error_app_open_in_another_tab": "Переключитесь на другую вкладку, чтобы подключиться к %(brand)s . Теперь эту вкладку можно закрыть.", + "error_app_open_in_another_tab_title": "%(brand)s подключен в другой вкладке", "error_app_opened_in_another_window": "%(brand)s открыт в другом окне. Нажмите \"%(label)s\" чтобы использовать %(brand)s в данном окне и отключить другое.", "error_database_closed_description": { "for_desktop": "Возможно, ваш диск переполнен. Освободите место и перезагрузите компьютер.", "for_web": "Если вы очистили данные браузера, то это сообщение ожидаемо. %(brand)s также может быть открыт в другой вкладке, или ваш диск заполнен. Пожалуйста, освободите место и перезагрузите" }, - "error_database_closed_title": "База данных неожиданно закрылась", + "error_database_closed_title": "%(brand)s перестал работать", "error_dialog": { "copy_room_link_failed": { "description": "Не удалось скопировать ссылку на комнату в буфер обмена.", @@ -1141,7 +1158,8 @@ "image": "Изображение", "poll": "Опрос", "video": "Видео" - } + }, + "preview": "%(prefix)s: %(preview)s" }, "export_chat": { "cancelled": "Экспорт отменён", @@ -1266,12 +1284,16 @@ }, "incompatible_browser": { "continue": "Продолжить в любом случае", + "description": "%(brand)s использует некоторые функции браузера, которые недоступны в вашем текущем браузере. %(detail)s", + "detail_can_continue": "Если вы продолжите, некоторые функции могут перестать работать, и существует риск потери данных в будущем.", "detail_no_continue": "Попробуйте обновить этот браузер, если вы используете не последнюю версию, и повторите попытку.", "learn_more": "Подробнее", "linux": "Linux", "macos": "Mac", + "supported_browsers": "Для наилучшего впечатления используйте Chrome, Firefox, Edge или Safari.", "title": "Неподдерживаемый браузер", "use_desktop_heading": "Вместо этого используйте %(brand)s Desktop", + "use_mobile_heading": "Для этого используйте %(brand)s на мобильном телефоне", "use_mobile_heading_after_desktop": "Или воспользуйтесь нашим мобильным приложением", "windows_64bit": "Windows (64-бит)", "windows_arm_64bit": "Windows (ARM 64-бит)" @@ -1284,8 +1306,8 @@ "explainer": "Менеджеры по интеграции получают данные конфигурации и могут изменять виджеты, отправлять приглашения в комнаты и устанавливать уровни доступа от вашего имени.", "manage_title": "Управление интеграциями", "toggle_label": "Включить менеджер интеграции", - "use_im": "Используйте менеджер интеграций для управления ботами, виджетами и наклейками.", - "use_im_default": "Используйте менеджер интеграций %(serverName)s для управления ботами, виджетами и наклейками." + "use_im": "Используйте менеджер интеграций для управления ботами, виджетами и наборами стикеров.", + "use_im_default": "Используйте менеджер интеграций %(serverName)s для управления ботами, виджетами и наборами стикеров." }, "integrations": { "disabled_dialog_description": "Включите '%(manageIntegrations)s' в Настройках.", @@ -1327,7 +1349,7 @@ "name_email_mxid_share_room": "Пригласите кого-нибудь, используя его имя, адрес электронной почты, имя пользователя (например, ) или поделитесь этой комнатой.", "name_email_mxid_share_space": "Пригласите кого-нибудь, используя их имя, адрес электронной почты, имя пользователя (например, ) или поделитесь этим пространством.", "name_mxid_share_room": "Пригласите кого-нибудь, используя его имя, имя пользователя (например, ) или поделитесь этой комнатой.", - "name_mxid_share_space": "Пригласите кого-нибудь, используя их имя, учётную запись (как ) или поделитесь этим пространством.", + "name_mxid_share_space": "Пригласите кого-нибудь, используя их отображаемое имя или имя учётной записи (например, ) или поделитесь этим пространством.", "recents_section": "Недавние Диалоги", "room_failed_partial": "Мы отправили остальных, но нижеперечисленные люди не могут быть приглашены в ", "room_failed_partial_title": "Некоторые приглашения не могут быть отправлены", @@ -2018,7 +2040,10 @@ "pinned_message_badge": "Закреплённое сообщение", "pinned_message_banner": { "button_close_list": "Закрыть список", - "button_view_all": "Посмотреть все" + "button_view_all": "Посмотреть все", + "description": "В этой комнате есть закрепленные сообщения. Нажмите, чтобы просмотреть их.", + "go_to_message": "Показать прикрепленное сообщение на временной шкале.", + "title": "%(index)s из %(length)s Закрепленные сообщения" }, "read_topic": "Нажмите, чтобы увидеть тему", "rejecting": "Отклонение приглашения…", @@ -2115,7 +2140,8 @@ "other": "Удаляются сообщения в %(count)s комнатах" }, "room": { - "more_options": "Дополнительные параметры" + "more_options": "Дополнительные параметры", + "open_room": "Открыть комнату %(roomName)s" }, "show_less": "Показать меньше", "show_n_more": { @@ -2198,6 +2224,8 @@ "error_deleting_alias_description": "Произошла ошибка при удалении этого адреса. Возможно, он больше не существует или произошла временная ошибка.", "error_deleting_alias_description_forbidden": "У вас нет прав для удаления этого адреса.", "error_deleting_alias_title": "Ошибка при удалении адреса", + "error_publishing": "Невозможно опубликовать комнату", + "error_publishing_detail": "Произошла ошибка при публикации этой комнаты", "error_save_space_settings": "Не удалось сохранить настройки пространства.", "error_updating_alias_description": "Произошла ошибка при обновлении альтернативных адресов комнаты. Это может быть запрещено сервером или произошел временный сбой.", "error_updating_canonical_alias_description": "При обновлении основного адреса комнаты произошла ошибка. Возможно, это не разрешено сервером или произошел временный сбой.", @@ -2438,20 +2466,26 @@ }, "settings": { "account": { + "dialog_title": "Настройки: Учетная запись", "title": "Учетная запись" }, "all_rooms_home": "Показывать все комнаты на Главной", "all_rooms_home_description": "Все комнаты, в которых вы находитесь, будут отображаться на Главной.", "always_show_message_timestamps": "Всегда показывать время отправки сообщений", "appearance": { + "bundled_emoji_font": "Использовать встроенный шрифт эмодзи", + "compact_layout": "Показывать компактный текст и сообщения", + "compact_layout_description": "Для использования этой функции необходимо выбрать современный макет.", "custom_font": "Использовать системный шрифт", "custom_font_description": "Установите имя шрифта, установленного в вашей системе, и %(brand)s попытается его использовать.", "custom_font_name": "Название системного шрифта", "custom_font_size": "Использовать другой размер", "custom_theme_add": "Добавить пользовательскую тему", "custom_theme_downloading": "Загрузка пользовательской темы…", - "custom_theme_error_downloading": "Ошибка при загрузке информации темы.", + "custom_theme_error_downloading": "Ошибка при загрузке темы", + "custom_theme_help": "Введите URL-адрес пользовательской темы, которую вы хотите применить.", "custom_theme_invalid": "Неверная схема темы.", + "dialog_title": "Настройки: Внешний вид", "font_size": "Размер шрифта", "font_size_default": "%(fontSize)s (по умолчанию)", "high_contrast": "Высокая контрастность", @@ -2506,6 +2540,7 @@ "title": "Вы уверены, что хотите отключить хранение ключей и удалить их?" }, "device_not_verified_button": "Проверить это устройство", + "device_not_verified_description": "Для просмотра настроек шифрования необходимо подтвердить это устройство.", "device_not_verified_title": "Устройство не проверено", "dialog_title": "Настройки: Шифрование", "key_storage": { @@ -2515,19 +2550,26 @@ }, "recovery": { "change_recovery_confirm_button": "Подтвердите новый ключ восстановления", + "change_recovery_confirm_description": "Чтобы завершить, введите новый Ключ Восстановления ниже. Ваш старый ключ больше работать не будет.", "change_recovery_confirm_title": "Введите новый ключ восстановления", "change_recovery_key": "Изменить ключ восстановления", + "change_recovery_key_description": "Запишите новый ключ восстановления в безопасном месте. Затем нажмите «Продолжить», чтобы подтвердить изменение.", "change_recovery_key_title": "Изменить ключ восстановления?", + "description": "Восстановите свою идентификацию и историю сообщений с помощью ключа восстановления, если вы потеряли все существующие устройства.", "enter_key_error": "Ключ восстановления, который вы ввел, неверный.", "enter_recovery_key": "Введите ключ восстановления", "forgot_recovery_key": "Забыли ключ восстановления?", + "key_storage_warning": "Хранилище ключей не синхронизировано. Нажмите кнопку ниже, чтобы устранить проблему.", "save_key_description": "Не сообщайте эту информацию никому!", "save_key_title": "Ключ восстановления", "set_up_recovery": "Настройка восстановления", "set_up_recovery_confirm_button": "Завершить настройку", + "set_up_recovery_confirm_description": "Введите ключ восстановления, показанный на предыдущем экране, чтобы завершить настройку восстановления.", "set_up_recovery_confirm_title": "Для подтверждения введите ключ восстановления", + "set_up_recovery_description": "Хранилище ключей защищено ключом восстановления. Если после установки вам понадобится новый ключ восстановления, вы можете создать его заново, выбрав '%(changeRecoveryKeyButton)s'.", "set_up_recovery_save_key_description": "Запишите ключ восстановления в безопасном месте, например в диспетчере паролей, зашифрованной заметке или физическом сейфе.", "set_up_recovery_save_key_title": "Сохраните ключ восстановления в безопасном месте", + "set_up_recovery_secondary_description": "После нажатия кнопки «Продолжить» мы сгенерируем для вас ключ восстановления.", "title": "Восстановление" }, "title": "Шифрование" @@ -2609,6 +2651,7 @@ "password_change_success": "Ваш пароль успешно изменён.", "personal_info": "Личная информация", "profile_subtitle": "Так вас видят другие пользователи приложения.", + "profile_subtitle_oidc": "Ваша учетная запись управляется отдельным поставщиком идентификационных данных, поэтому некоторые ваши личные данные изменить нельзя.", "remove_email_prompt": "Удалить %(email)s?", "remove_msisdn_prompt": "Удалить %(phone)s?", "spell_check_locale_placeholder": "Выберите регион", @@ -2638,7 +2681,7 @@ "enter_phrase_title": "Введите секретную фразу", "enter_phrase_to_confirm": "Введите секретную фразу второй раз, чтобы подтвердить ее.", "generate_security_key_description": "Мы создадим ключ восстановления, который вы сможете хранить в безопасном месте, например в менеджере паролей или сейфе.", - "generate_security_key_title": "Создание ключа безопасности", + "generate_security_key_title": "Создание Ключа Восстановления", "pass_phrase_match_failed": "Они не совпадают.", "pass_phrase_match_success": "Они совпадают!", "phrase_strong_enough": "Отлично! Эта контрольная фраза выглядит достаточно сильной.", @@ -2647,11 +2690,11 @@ "set_phrase_again": "Задать другой пароль.", "settings_reminder": "Вы также можете настроить безопасное резервное копирование и управлять своими ключами в настройках.", "title_confirm_phrase": "Подтвердите секретную фразу", - "title_save_key": "Сохраните свой ключ безопасности", + "title_save_key": "Сохраните ключ восстановления", "title_set_phrase": "Задайте секретную фразу", "unable_to_setup": "Невозможно настроить секретное хранилище", "use_different_passphrase": "Использовать другую кодовую фразу?", - "use_phrase_only_you_know": "Используйте секретную фразу, известную только вам, и при необходимости сохраните ключ безопасности для резервного копирования." + "use_phrase_only_you_know": "Используйте секретную фразу, известную только вам, и при необходимости сохраните Ключ Восстановления от резервной копии." } }, "key_export_import": { @@ -2741,6 +2784,7 @@ "code_blocks_heading": "Блоки кода", "compact_modern": "Использовать более компактный \"Современный\" макет", "composer_heading": "Редактор", + "default_timezone": "Браузер по умолчанию (%(timezone)s)", "dialog_title": "Настройки: Параметры", "enable_hardware_acceleration": "Включить аппаратное ускорение", "enable_tray_icon": "Показывать значок в трее и сворачивать в него окно при закрытии", @@ -2766,6 +2810,7 @@ "bulk_options_accept_all_invites": "Принять все приглашения (%(invitedRooms)s)", "bulk_options_reject_all_invites": "Отклонить все %(invitedRooms)s приглашения", "bulk_options_section": "Основные опции", + "dehydrated_device_description": "Функция автономного устройства позволяет вам получать зашифрованные сообщения, даже если вы не вошли в систему ни на одном устройстве.", "dehydrated_device_enabled": "Устройство в автономном режиме", "dialog_title": "Настройки: Безопасность и конфиденциальность", "e2ee_default_disabled_warning": "Администратор вашего сервера отключил сквозное шифрование по умолчанию в приватных комнатах и диалогах.", @@ -3281,6 +3326,7 @@ "download_action_decrypting": "Расшифровка", "download_action_downloading": "Загрузка", "download_failed": "Загрузка не удалась", + "download_failed_description": "Произошла ошибка при загрузке этого файла", "e2e_state": "Состояние сквозного шифрования", "edits": { "tooltip_label": "Изменено %(date)s. Нажмите для посмотра истории изменений.", @@ -3436,6 +3482,7 @@ "left_reason": "%(targetName)s покинул(а) комнату: %(reason)s", "no_change": "%(senderName)s не сделал(а) изменений", "reject_invite": "%(targetName)s отклонил(а) приглашение", + "reject_invite_reason": "%(targetName)s отклонил приглашение: %(reason)s", "remove_avatar": "%(senderName)s удалил(а) аватар", "remove_name": "%(senderName)s удалил(а) отображаемое имя (%(oldDisplayName)s)", "set_avatar": "%(senderName)s установил(а) аватар", @@ -3472,9 +3519,10 @@ }, "m.room.tombstone": "%(senderDisplayName)s обновил(а) эту комнату.", "m.room.topic": { - "changed": "%(senderDisplayName)s изменил(а) тему комнаты на \"%(topic)s\"." + "changed": "%(senderDisplayName)s изменил(а) тему комнаты на \"%(topic)s\".", + "removed": "%(senderDisplayName)s удалил тему." }, - "m.sticker": "%(senderDisplayName)s отправил(а) наклейку.", + "m.sticker": "%(senderDisplayName)s отправил(а) стикер.", "m.video": { "error_decrypting": "Ошибка расшифровки видео" }, @@ -3524,7 +3572,8 @@ "reactions": { "add_reaction_prompt": "Отреагировать", "custom_reaction_fallback_label": "Пользовательская реакция", - "label": "%(reactors)s отреагировали %(content)s" + "label": "%(reactors)s отреагировали %(content)s", + "tooltip_caption": "отреагировал с %(shortName)s" }, "read_receipt_title": { "one": "Просмотрел %(count)s человек", @@ -3713,6 +3762,10 @@ "few": "И еще %(count)s...", "many": "И еще %(count)s..." }, + "unsupported_browser": { + "description": "Если вы продолжите, некоторые функции могут перестать работать, и существует риск потери данных в будущем. Обновите браузер, чтобы продолжить использование %(brand)s.", + "title": "%(brand)s не поддерживает этот браузер" + }, "unsupported_server_description": "На этом сервере используется старая версия Matrix. Перейдите на Matrix%(version)s, чтобы использовать %(brand)s ее без ошибок.", "unsupported_server_title": "Ваш сервер не поддерживается", "update": { @@ -3730,6 +3783,12 @@ "toast_title": "Обновление %(brand)s", "unavailable": "Недоступен" }, + "update_room_access_modal": { + "description": "Чтобы создать ссылку для совместного доступа, сделайте эту комнату общедоступной и разрешите пользователям запрашивать присоединение. Это позволит гостям присоединиться без приглашения.", + "dont_change_description": "Кроме того, вы можете провести звонок в отдельной комнате.", + "no_change": "Я не хочу менять уровень доступа.", + "title": "Изменить уровень доступа в комнату" + }, "upload_failed_generic": "Файл '%(fileName)s' не был загружен.", "upload_failed_size": "Размер файла '%(fileName)s' превышает допустимый предел загрузки, установленный на этом сервере", "upload_failed_title": "Сбой отправки файла", @@ -3739,6 +3798,7 @@ "error_files_too_large": "Эти файлы слишком большие для загрузки. Лимит размера файла составляет %(limit)s.", "error_some_files_too_large": "Некоторые файлы имеют слишком большой размер, чтобы их можно было загрузить. Лимит размера файла составляет %(limit)s.", "error_title": "Ошибка загрузки", + "not_image": "Выбранный вами файл не является изображением.", "title": "Загрузка файлов", "title_progress": "Загрузка файлов (%(current)s из %(total)s)", "upload_all_button": "Загрузить всё", @@ -3758,7 +3818,7 @@ "deactivate_confirm_description": "Деактивация этого пользователя приведет к его выходу из системы и запрету повторного входа. Кроме того, они оставит все комнаты, в которых он участник. Это действие безповоротно. Вы уверены, что хотите деактивировать этого пользователя?", "deactivate_confirm_title": "Деактивировать пользователя?", "demote_button": "Понижение", - "demote_self_confirm_description_space": "Вы не сможете отменить это изменение, поскольку вы понижаете свои права, если вы являетесь последним привилегированным пользователем в пространстве, будет невозможно восстановить привилегии вбудущем.", + "demote_self_confirm_description_space": "Вы не сможете отменить это изменение, поскольку вы понижаете свои права, если вы являетесь последним привилегированным пользователем в пространстве, будет невозможно восстановить привилегии в будущем.", "demote_self_confirm_room": "После понижения своих привилегий вы не сможете это отменить. Если вы являетесь последним привилегированным пользователем в этой комнате, выдать права кому-либо заново будет невозможно.", "demote_self_confirm_title": "Понизить самого себя?", "disinvite_button_room": "Отозвать приглашение в комнату", @@ -3770,10 +3830,11 @@ "error_mute_user": "Не удалось заглушить пользователя", "error_revoke_3pid_invite_description": "Не удалось отозвать приглашение. Возможно, на сервере возникла вре́менная проблема или у вас недостаточно прав для отзыва приглашения.", "error_revoke_3pid_invite_title": "Не удалось отменить приглашение", + "ignore_button": "Игнорировать", "ignore_confirm_description": "Все сообщения и приглашения от этого пользователя будут скрыты. Вы действительно хотите их игнорировать?", "ignore_confirm_title": "Игнорировать %(user)s", "invited_by": "Приглашен %(sender)s", - "jump_to_rr_button": "Перейти к последнему прочтённому", + "jump_to_rr_button": "Перейти к последнему прочитанному сообщению", "kick_button_room": "Удалить из комнаты", "kick_button_room_name": "Удалить из %(roomName)s", "kick_button_space": "Исключить из пространства", @@ -3797,19 +3858,22 @@ "no_recent_messages_description": "Попробуйте пролистать ленту сообщений вверх, чтобы увидеть, есть ли более ранние.", "no_recent_messages_title": "Последние сообщения от %(user)s не найдены" }, - "redact_button": "Удалить последние сообщения", + "redact_button": "Удалить сообщения", "revoke_invite": "Отозвать приглашение", "room_encrypted": "Сообщения в этой комнате защищены сквозным шифрованием.", "room_encrypted_detail": "Ваши сообщения в безопасности, ключи для расшифровки есть только у вас и получателя.", "room_unencrypted": "Сообщения в этой комнате не защищены сквозным шифрованием.", "room_unencrypted_detail": "В зашифрованных комнатах ваши сообщения в безопасности: только у вас и у получателя есть ключи для расшифровки.", - "share_button": "Поделиться ссылкой на пользователя", + "send_message": "Отправить сообщение", + "share_button": "Поделиться профилем", "unban_button_room": "Разблокировать в комнате", "unban_button_space": "Разблокировать в пространстве", "unban_room_confirm_title": "Разблокировать в %(roomName)s", "unban_space_everything": "Разблокировать их везде, где я могу это сделать", "unban_space_specific": "Разблокировать их из определённых мест, где я могу это сделать", "unban_space_warning": "Они не смогут получить доступ к тем местам, где вы не являетесь администратором.", + "unignore_button": "Не игнорировать", + "verification_unavailable": "Проверка пользователя недоступна", "verify_button": "Подтвердить пользователя", "verify_explainer": "Для дополнительной безопасности подтвердите этого пользователя, сравнив одноразовый код на ваших устройствах." }, @@ -3838,6 +3902,7 @@ "camera_disabled": "Ваша камера выключена", "camera_enabled": "Ваша камера всё ещё включена", "cannot_call_yourself_description": "Вы не можете позвонить самому себе.", + "close_lobby": "Закрыть лобби", "connecting": "Подключение", "connection_lost": "Соединение с сервером потеряно", "connection_lost_description": "Вы не можете совершать вызовы без подключения к серверу.", @@ -3851,14 +3916,23 @@ "disabled_no_perms_start_video_call": "У вас нет разрешения для запуска видеозвонка", "disabled_no_perms_start_voice_call": "У вас нет разрешения для запуска звонка", "disabled_ongoing_call": "Текущий звонок", + "element_call": "Element Call", "enable_camera": "Включить камеру", "enable_microphone": "Включить микрофон", "expand": "Вернуться к звонку", + "get_call_link": "Поделиться ссылкой на звонок", "hangup": "Повесить трубку", "hide_sidebar_button": "Скрыть боковую панель", "input_devices": "Устройства ввода", + "jitsi_call": "Конференция Jitsi", "join_button_tooltip_call_full": "Извините — этот вызов в настоящее время заполнен", + "legacy_call": "Звонок (устаревший)", "maximise": "Заполнить экран", + "maximise_call": "Развернуть звонок", + "metaspace_video_rooms": { + "conference_room_section": "Конференции" + }, + "minimise_call": "Свернуть звонок", "misconfigured_server": "Вызов не состоялся из-за неправильно настроенного сервера", "misconfigured_server_description": "Попросите администратора вашего домашнего сервера (%(homeserverDomain)s) настроить сервер TURN для надежной работы звонков.", "misconfigured_server_fallback": "В качестве альтернативы вы можете попробовать использовать общедоступный сервер по адресу , но он не будет таким надежным, и ваш IP-адрес будет передаваться на сервер. Вы также можете управлять этим в настройках.", @@ -3906,6 +3980,7 @@ "user_is_presenting": "%(sharerName)s показывает", "video_call": "Видеовызов", "video_call_started": "Начался видеозвонок", + "video_call_using": "Видеозвонок с использованием:", "voice_call": "Голосовой вызов", "you_are_presenting": "Вы представляете" }, @@ -4005,7 +4080,7 @@ "error_need_to_be_logged_in": "Вы должны войти в систему.", "error_unable_start_audio_stream_description": "Невозможно запустить аудио трансляцию.", "error_unable_start_audio_stream_title": "Не удалось запустить прямую трансляцию", - "modal_data_warning": "Данные на этом экране используются %(widgetDomain)s", + "modal_data_warning": "Приведенные ниже данные передаются %(widgetDomain)s", "modal_title_default": "Модальный виджет", "no_name": "Неизвестное приложение", "open_id_permissions_dialog": { @@ -4014,7 +4089,7 @@ "title": "Разрешите этому виджету проверить ваш идентификатор" }, "popout": "Всплывающий виджет", - "set_room_layout": "Установить мой макет комнаты для всех", + "set_room_layout": "Установить макет для всех", "shared_data_avatar": "URL-адрес изображения вашего профиля", "shared_data_device_id": "Идентификатор вашего устройства", "shared_data_lang": "Ваш язык", From 9d1455e4dd939bce46ab4b1c9302b03cf680ca61 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 17 Jun 2025 11:31:08 +0100 Subject: [PATCH 09/12] Prevent skipping forced verification after logging in with OIDC (#30141) Pass the freshLogin parameter along to doSetLoggedIn when restoring a session, instead of hard-coding it to always be false. --- playwright/e2e/oidc/index.ts | 17 ++ playwright/e2e/oidc/oidc-native.spec.ts | 156 +++++++++++++++++- src/Lifecycle.ts | 2 +- .../components/structures/MatrixChat-test.tsx | 47 +++++- 4 files changed, 218 insertions(+), 4 deletions(-) diff --git a/playwright/e2e/oidc/index.ts b/playwright/e2e/oidc/index.ts index 02de6e2f03..602a4368c7 100644 --- a/playwright/e2e/oidc/index.ts +++ b/playwright/e2e/oidc/index.ts @@ -11,6 +11,9 @@ import { type Page } from "@playwright/test"; import { expect } from "../../element-web-test"; +/** + * Click through registering a new user in the MAS UI. + */ export async function registerAccountMas( page: Page, mailpit: MailpitClient, @@ -42,3 +45,17 @@ export async function registerAccountMas( await expect(page.getByText("Allow access to your account?")).toBeVisible(); await page.getByRole("button", { name: "Continue" }).click(); } + +/** + * Click through entering username and password into the MAS login prompt. + */ +export async function logInAccountMas(page: Page, username: string, password: string): Promise { + await expect(page.getByText("Please sign in to continue:")).toBeVisible(); + + await page.getByRole("textbox", { name: "Username" }).fill(username); + await page.getByRole("textbox", { name: "Password", exact: true }).fill(password); + await page.getByRole("button", { name: "Continue" }).click(); + + await expect(page.getByText("Allow access to your account?")).toBeVisible(); + await page.getByRole("button", { name: "Continue" }).click(); +} diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts index a6fbf231ce..3268c2b65a 100644 --- a/playwright/e2e/oidc/oidc-native.spec.ts +++ b/playwright/e2e/oidc/oidc-native.spec.ts @@ -6,8 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ +import { type Config, CONFIG_JSON } from "@element-hq/element-web-playwright-common"; +import { type Browser, type Page } from "@playwright/test"; +import { type StartedHomeserverContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers/HomeserverContainer"; + import { test, expect } from "../../element-web-test.ts"; -import { registerAccountMas } from "."; +import { logInAccountMas, registerAccountMas } from "."; import { ElementAppPage } from "../../pages/ElementAppPage.ts"; import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts"; @@ -101,4 +105,154 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { expect(localStorageKeys).toHaveLength(0); }, ); + + test("can log in to an existing MAS account", { tag: "@screenshot" }, async ({ page, mailpitClient }, testInfo) => { + // Register an account with MAS + await page.goto("/#/login"); + await page.getByRole("button", { name: "Continue" }).click(); + + const userId = `alice_${testInfo.testId}`; + await registerAccountMas(page, mailpitClient, userId, `${userId}@email.com`, "Pa$sW0rD!"); + await expect(page.getByText("Welcome")).toBeVisible(); + + // Log out + await page.getByRole("button", { name: "User menu" }).click(); + await expect(page.getByText(userId, { exact: true })).toBeVisible(); + + // Allow the outstanding requests queue to settle before logging out + await page.waitForTimeout(2000); + await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click(); + await expect(page).toHaveURL(/\/#\/login$/); + + // Log in again + await page.goto("/#/login"); + await page.getByRole("button", { name: "Continue" }).click(); + await page.getByRole("button", { name: "Continue" }).click(); + + // We should be in (we see an error because we have no recovery key). + await expect(page.getByText("Unable to verify this device")).toBeVisible(); + }); + + test.describe("with force_verification on", () => { + test.use({ + config: { + force_verification: true, + }, + }); + + test("verify dialog cannot be dismissed", { tag: "@screenshot" }, async ({ page, mailpitClient }, testInfo) => { + // Register an account with MAS + await page.goto("/#/login"); + await page.getByRole("button", { name: "Continue" }).click(); + + const userId = `alice_${testInfo.testId}`; + await registerAccountMas(page, mailpitClient, userId, `${userId}@email.com`, "Pa$sW0rD!"); + await expect(page.getByText("Welcome")).toBeVisible(); + + // Log out + await page.getByRole("button", { name: "User menu" }).click(); + await expect(page.getByText(userId, { exact: true })).toBeVisible(); + await page.waitForTimeout(2000); + await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click(); + await expect(page).toHaveURL(/\/#\/login$/); + + // Log in again + await page.goto("/#/login"); + await page.getByRole("button", { name: "Continue" }).click(); + await page.getByRole("button", { name: "Continue" }).click(); + + // We should be being warned that we need to verify (but we can't) + await expect(page.getByText("Unable to verify this device")).toBeVisible(); + + // And there should be no way to close this prompt + await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible(); + }); + + test( + "continues to show verification prompt after cancelling device verification", + { tag: "@screenshot" }, + async ({ browser, config, homeserver, page, mailpitClient }, testInfo) => { + // Register an account with MAS + await page.goto("/#/login"); + await page.getByRole("button", { name: "Continue" }).click(); + + const userId = `alice_${testInfo.testId}`; + const password = "Pa$sW0rD!"; + await registerAccountMas(page, mailpitClient, userId, `${userId}@email.com`, password); + await expect(page.getByText("Welcome")).toBeVisible(); + + // Log in an additional account, and verify it. + // + // This means that when we log out and in again, we are offered + // to verify using another device. + const otherContext = await newContext(browser, config, homeserver); + const otherDevicePage = await otherContext.newPage(); + await otherDevicePage.goto("/#/login"); + await otherDevicePage.getByRole("button", { name: "Continue" }).click(); + await logInAccountMas(otherDevicePage, userId, password); + await verifyUsingOtherDevice(otherDevicePage, page); + await otherDevicePage.close(); + + // Log out + await page.getByRole("button", { name: "User menu" }).click(); + await expect(page.getByText(userId, { exact: true })).toBeVisible(); + await page.waitForTimeout(2000); + await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click(); + await expect(page).toHaveURL(/\/#\/login$/); + + // Log in again + await page.goto("/#/login"); + await page.getByRole("button", { name: "Continue" }).click(); + await page.getByRole("button", { name: "Continue" }).click(); + + // We should be in, and not able to dismiss the verify dialog + await expect(page.getByText("Verify this device")).toBeVisible(); + await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible(); + + // When we start verifying with another device + await page.getByRole("button", { name: "Verify with another device" }).click(); + + // And then cancel it + await page.getByRole("button", { name: "Close dialog" }).click(); + + // Then we should still be at the unskippable verify prompt + await expect(page.getByText("Verify this device")).toBeVisible(); + await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible(); + }, + ); + }); }); + +/** + * Perform interactive emoji verification for a new device. + */ +async function verifyUsingOtherDevice(deviceToVerifyPage: Page, alreadyVerifiedDevicePage: Page) { + await deviceToVerifyPage.getByRole("button", { name: "Verify with another device" }).click(); + await alreadyVerifiedDevicePage.getByRole("button", { name: "Verify session" }).click(); + await alreadyVerifiedDevicePage.getByRole("button", { name: "Start" }).click(); + await alreadyVerifiedDevicePage.getByRole("button", { name: "They match" }).click(); + await deviceToVerifyPage.getByRole("button", { name: "They match" }).click(); + await alreadyVerifiedDevicePage.getByRole("button", { name: "Got it" }).click(); + await deviceToVerifyPage.getByRole("button", { name: "Got it" }).click(); +} + +/** + * Create a new browser context which serves up the default config plus what you supplied, and sets m.homeserver to the + * supplied homeserver's URL. + */ +async function newContext(browser: Browser, config: Partial>, homeserver: StartedHomeserverContainer) { + const otherContext = await browser.newContext(); + await otherContext.route(`http://localhost:8080/config.json*`, async (route) => { + const json = { + ...CONFIG_JSON, + ...config, + default_server_config: { + "m.homeserver": { + base_url: homeserver.baseUrl, + }, + }, + }; + await route.fulfill({ json }); + }); + return otherContext; +} diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 5641f936ae..20e7435599 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -657,7 +657,7 @@ export async function restoreSessionFromStorage(opts?: { ignoreGuest?: boolean } freshLogin: freshLogin, }, false, - false, + freshLogin, ); return true; } else { diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index 94fc9a925a..1fb705d98c 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -1040,7 +1040,7 @@ describe("", () => { localStorage.removeItem("must_verify_device"); }); - it("should show the complete security screen if unskippable verification is enabled", async () => { + it("should show the Complete Security screen if unskippable verification is enabled", async () => { // Given we have force verification on, and an existing logged-in session // that is not verified (see beforeEach()) @@ -1053,7 +1053,6 @@ describe("", () => { // Sanity: we are not racing with another screen update, so this heading stays visible await screen.findByRole("heading", { name: "Verify this device", level: 1 }); }); - it("should not open app after cancelling device verify if unskippable verification is on", async () => { // See https://github.com/element-hq/element-web/issues/29230 // We used to allow bypassing force verification by choosing "Verify with @@ -1081,6 +1080,50 @@ describe("", () => { await screen.findByRole("heading", { name: "Verify this device", level: 1 }); }); + describe("when query params have a loginToken", () => { + const loginToken = "test-login-token"; + const realQueryParams = { + loginToken, + }; + + let loginClient!: ReturnType; + const deviceId = "test-device-id"; + const accessToken = "test-access-token"; + const clientLoginResponse = { + user_id: userId, + device_id: deviceId, + access_token: accessToken, + }; + + beforeEach(() => { + localStorage.setItem("mx_sso_hs_url", serverConfig.hsUrl); + localStorage.setItem("mx_sso_is_url", serverConfig.isUrl); + loginClient = getMockClientWithEventEmitter(getMockClientMethods()); + // this is used to create a temporary client during login + jest.spyOn(MatrixJs, "createClient").mockReturnValue(loginClient); + + loginClient.login.mockClear().mockResolvedValue(clientLoginResponse); + }); + + it("should show the Complete Security screen after OIDC login if unskippable ver. is on", async () => { + // Given force_verification is on (outer describe) + // And we just logged in via OIDC (inner describe) + + // When we load the page + getComponent({ realQueryParams }); + + defaultDispatcher.dispatch({ + action: "will_start_client", + }); + await waitFor(() => + expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "client_started" }), + ); + + // Then we are not allowed in - we are being asked to verify + await screen.findByRole("heading", { name: "Verify this device", level: 1 }); + }); + }); + function createMockCrypto(): CryptoApi { return { getVersion: jest.fn().mockReturnValue("Version 0"), From ba3b9840ca2fc8fbdbb1915f96aa1abead56fbde Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 17 Jun 2025 13:11:17 +0000 Subject: [PATCH 10/12] Upgrade dependency to matrix-js-sdk@37.9.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a433f22aea..67b58714d5 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "maplibre-gl": "^5.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "37.9.0-rc.0", + "matrix-js-sdk": "37.9.0", "matrix-widget-api": "^1.10.0", "memoize-one": "^6.0.0", "mime": "^4.0.4", diff --git a/yarn.lock b/yarn.lock index 069a5ebd52..5eaa6648e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9219,10 +9219,10 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -matrix-js-sdk@37.9.0-rc.0: - version "37.9.0-rc.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-37.9.0-rc.0.tgz#7a0b536abe110096ed3d86f05a33cae0d4258fe4" - integrity sha512-LHezGwAJwABI5IYkwinBqnte8yosVGTa8kPQBmnrhioDP9EZQtQuGtjhHXMR+oWo257UbjhWKRVDJijhDQuxNA== +matrix-js-sdk@37.9.0: + version "37.9.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-37.9.0.tgz#f56dcb47ae5324a719d279a29278f28e43d2b234" + integrity sha512-a4p/2XM7kOQsguOjzsnbmBajxLRJewlOG1KB6HfErUKWR4GKwZEfJ3oHeWUHRdE/IYLdUQzIAwFdvCFK08pOww== dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm" "^14.2.0" From 01519f7fd57c917f1e4950f7f733a0fb8c21aaf9 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 17 Jun 2025 13:15:36 +0000 Subject: [PATCH 11/12] v1.11.104 --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a18c5396..a138735650 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +Changes in [1.11.104](https://github.com/element-hq/element-web/releases/tag/v1.11.104) (2025-06-17) +==================================================================================================== +## ✨ Features + +* Update the mobile\_guide page to the new design. ([#30006](https://github.com/element-hq/element-web/pull/30006)). Contributed by @pixlwave. +* Provide a devtool for manually verifying other devices ([#30094](https://github.com/element-hq/element-web/pull/30094)). Contributed by @andybalaam. +* Implement MSC4155: Invite filtering ([#29603](https://github.com/element-hq/element-web/pull/29603)). Contributed by @Half-Shot. +* Add low priority avatar decoration to room tile ([#30065](https://github.com/element-hq/element-web/pull/30065)). Contributed by @MidhunSureshR. +* Add ability to prevent window content being captured by other apps (Desktop) ([#30098](https://github.com/element-hq/element-web/pull/30098)). Contributed by @t3chguy. +* New room list: move message preview in user settings ([#30023](https://github.com/element-hq/element-web/pull/30023)). Contributed by @florianduros. +* New room list: change room options icon ([#30029](https://github.com/element-hq/element-web/pull/30029)). Contributed by @florianduros. +* RoomListStore: Sort low priority rooms to the bottom of the list ([#30070](https://github.com/element-hq/element-web/pull/30070)). Contributed by @MidhunSureshR. +* Add low priority filter pill to the room list UI ([#30060](https://github.com/element-hq/element-web/pull/30060)). Contributed by @MidhunSureshR. +* New room list: remove color gradient in space panel ([#29721](https://github.com/element-hq/element-web/pull/29721)). Contributed by @florianduros. +* /share?msg=foo endpoint using forward message dialog ([#29874](https://github.com/element-hq/element-web/pull/29874)). Contributed by @ara4n. + +## 🐛 Bug Fixes + +* Do not send empty auth when setting up cross-signing keys ([#29914](https://github.com/element-hq/element-web/pull/29914)). Contributed by @gnieto. +* Settings: flip local video feed by default ([#29501](https://github.com/element-hq/element-web/pull/29501)). Contributed by @jbtrystram. +* AccessSecretStorageDialog: various fixes ([#30093](https://github.com/element-hq/element-web/pull/30093)). Contributed by @richvdh. +* AccessSecretStorageDialog: fix inability to enter recovery key ([#30090](https://github.com/element-hq/element-web/pull/30090)). Contributed by @richvdh. +* Fix failure to upload thumbnail causing image to send as file ([#30086](https://github.com/element-hq/element-web/pull/30086)). Contributed by @t3chguy. +* Low priority menu item should be a toggle ([#30071](https://github.com/element-hq/element-web/pull/30071)). Contributed by @MidhunSureshR. +* Add sanity checks to prevent users from ignoring themselves ([#30079](https://github.com/element-hq/element-web/pull/30079)). Contributed by @MidhunSureshR. +* Fix issue with duplicate images ([#30073](https://github.com/element-hq/element-web/pull/30073)). Contributed by @fatlewis. +* Handle errors returned from Seshat ([#30083](https://github.com/element-hq/element-web/pull/30083)). Contributed by @richvdh. + + Changes in [1.11.103](https://github.com/element-hq/element-web/releases/tag/v1.11.103) (2025-06-10) ==================================================================================================== ## 🐛 Bug Fixes diff --git a/package.json b/package.json index 67b58714d5..2245d033c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.104-rc.0", + "version": "1.11.104", "description": "Element: the future of secure communication", "author": "New Vector Ltd.", "repository": { From a7a8428d1cea199b781a6323105081ff994d6b5f Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 17 Jun 2025 13:19:00 +0000 Subject: [PATCH 12/12] Reset matrix-js-sdk back to develop branch --- package.json | 2 +- yarn.lock | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 6a67b7e468..36c501e13d 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "maplibre-gl": "^5.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "37.9.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^1.10.0", "memoize-one": "^6.0.0", "mime": "^4.0.4", diff --git a/yarn.lock b/yarn.lock index 8826c7a46a..807587985b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2255,11 +2255,6 @@ resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-14.2.0.tgz#1d6bddb2f777ac1674546467aab6f8584a7f2e71" integrity sha512-xYbH1Yg8YwfXxGsCVDypiRvSVYPnCybsoRqlBDuAvIOs9tOfmdeeJqN+3VxvLWH28g3CtJs+9Afw8dYSHViTFg== -"@matrix-org/olm@3.2.15": - version "3.2.15" - resolved "https://registry.yarnpkg.com/@matrix-org/olm/-/olm-3.2.15.tgz#55f3c1b70a21bbee3f9195cecd6846b1083451ec" - integrity sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q== - "@matrix-org/react-sdk-module-api@^2.4.0": version "2.5.0" resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-2.5.0.tgz#df774d0ae0c327fbd40f8994bbb13ed35e26c337" @@ -3853,6 +3848,7 @@ "@vector-im/matrix-wysiwyg-wasm@link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.3-cc54d8b3e9472bcd8e622126ba364ee31952cd8a-integrity/node_modules/bindings/wysiwyg-wasm": version "0.0.0" + uid "" "@vector-im/matrix-wysiwyg@2.38.3": version "2.38.3" @@ -9224,14 +9220,12 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -matrix-js-sdk@37.9.0: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "37.9.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-37.9.0.tgz#f56dcb47ae5324a719d279a29278f28e43d2b234" - integrity sha512-a4p/2XM7kOQsguOjzsnbmBajxLRJewlOG1KB6HfErUKWR4GKwZEfJ3oHeWUHRdE/IYLdUQzIAwFdvCFK08pOww== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4efb27354f466d4e50f09e95f5853780326a2b6c" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm" "^14.2.0" - "@matrix-org/olm" "3.2.15" another-json "^0.2.0" bs58 "^6.0.0" content-type "^1.0.4"