From 4dbf739fb8210bc851e3526f9c0c843452df488f Mon Sep 17 00:00:00 2001
From: libexus
Date: Fri, 18 Jun 2021 16:48:23 +0000
Subject: [PATCH 01/52] Translated using Weblate (German)
Currently translated at 99.4% (2977 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/
---
src/i18n/strings/de_DE.json | 28 ++++++++++++++++++++++------
1 file changed, 22 insertions(+), 6 deletions(-)
diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 9d1c1fa071..3eb0996792 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -1321,7 +1321,7 @@
"Disconnect from the identity server ?": "Verbindung zum Identitätsserver trennen?",
"Add Email Address": "E-Mail-Adresse hinzufügen",
"Add Phone Number": "Telefonnummer hinzufügen",
- "Changes the avatar of the current room": "Ändert den Avatar für diesen Raum",
+ "Changes the avatar of the current room": "Ändert den Avatar des Raums",
"Deactivate account": "Benutzerkonto deaktivieren",
"Show previews/thumbnails for images": "Vorschauen für Bilder",
"View": "Vorschau",
@@ -1388,7 +1388,7 @@
"Backup key stored: ": "Backup Schlüssel gespeichert: ",
"Clear notifications": "Benachrichtigungen löschen",
"Disconnect from the identity server and connect to instead?": "Vom Identitätsserver trennen, und stattdessen eine Verbindung zu aufbauen?",
- "The identity server you have chosen does not have any terms of service.": "Der von dir gewählte Identitätsserver hat keine Nutzungsbedingungen.",
+ "The identity server you have chosen does not have any terms of service.": "Der von dir gewählte Identitätsserver gibt keine Nutzungsbedingungen an.",
"Disconnect identity server": "Verbindung zum Identitätsserver trennen",
"contact the administrators of identity server ": "Administration des Identitätsservers kontaktieren",
"wait and try again later": "warte und versuche es später erneut",
@@ -1634,7 +1634,7 @@
"Show rooms with unread notifications first": "Räume mit ungelesenen Benachrichtigungen zuerst zeigen",
"Show shortcuts to recently viewed rooms above the room list": "Kürzlich besuchte Räume anzeigen",
"Use Single Sign On to continue": "Einmalanmeldung zum Fortfahren nutzen",
- "Confirm adding this email address by using Single Sign On to prove your identity.": "Bestätige das Hinzufügen dieser E-Mail-Adresse durch Single Sign-on, um deine Identität nachzuweisen.",
+ "Confirm adding this email address by using Single Sign On to prove your identity.": "Bestätige die neue E-Mail-Adresse mit Single-Sign-On, um deine Identität nachzuweisen.",
"Single Sign On": "Einmalanmeldung",
"Confirm adding email": "Hinzugefügte E-Mail-Addresse bestätigen",
"Confirm adding this phone number by using Single Sign On to prove your identity.": "Bestätige die hinzugefügte Telefonnummer, indem du deine Identität mittels der Einmalanmeldung nachweist.",
@@ -3102,7 +3102,7 @@
"Your public space": "Dein öffentlicher Space",
"You can change this later": "Du kannst die Sichtbarkeit später ändern",
"Invite only, best for yourself or teams": "Nur Eingeladene können beitreten - am besten für dich selbst oder Teams",
- "Open space for anyone, best for communities": "Öffne den Space für alle - am Besten für Communities",
+ "Open space for anyone, best for communities": "Öffne den Space für alle - am besten für Communities",
"Private": "Privat",
"Public": "Öffentlich",
"Spaces are new ways to group rooms and people. To join an existing space you’ll need an invite": "Spaces sind ein neuer Weg Räume und Leute zu gruppieren. Um einen bestehenden Space zu betreten brauchst du eine Einladung",
@@ -3302,7 +3302,7 @@
"Join the beta": "Beta beitreten",
"Leave the beta": "Beta verlassen",
"Beta": "Beta",
- "Tap for more info": "Klicke für mehr Infos",
+ "Tap for more info": "Für mehr Infos hier klicken",
"Spaces is a beta feature": "Spaces sind noch in der Entwicklung und möglicherweise instabil",
"Want to add a new room instead?": "Willst du einen neuen Raum hinzufügen?",
"Adding rooms... (%(progress)s out of %(count)s)|one": "Raum wird hinzugefügt...",
@@ -3352,5 +3352,21 @@
"See when people join, leave, or are invited to this room": "Anzeigen, wenn Leute eingeladen werden oder den Raum betreten und verlassen",
"The user you called is busy.": "Der angerufene Benutzer ist momentan beschäftigt.",
"User Busy": "Benutzer beschäftigt",
- "No results for \"%(query)s\"": "Keine Ergebnisse für \"%(query)s\""
+ "No results for \"%(query)s\"": "Keine Ergebnisse für \"%(query)s\"",
+ "Some suggestions may be hidden for privacy.": "Um deine Privatsphäre zu schützen, werden einige Vorschläge möglicherweise ausgeblendet.",
+ "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Dieser Raum ist offenbar nicht verschlüsselt. Das kann an einem veralteten Gerät oder an manchen Verfahren wie E-Mail-Einladungen liegen. Verschlüsselung aktivieren.",
+ "We're working on this as part of the beta, but just want to let you know.": "Dies ist noch Teil der Beta, wir wollten dir es aber schon jetzt zeigen.",
+ "Or send invite link": "Oder versende einen Einladungslink",
+ "If you can't see who you’re looking for, send them your invite link below.": "Niemanden gefunden? Sende ihnen stattdessen einen Einladungslink.",
+ "If you have permissions, open the menu on any message and select Pin to stick them here.": "Du kannst, sofern du die Berechtigung dazu hast, Nachrichten in ihrem Menü hier anpinnen.",
+ "Search for rooms or people": "Räume oder Leute suchen",
+ "Forward message": "Nachricht weiterleiten",
+ "Open link": "Link öffnen",
+ "Sent": "Gesendet",
+ "You don't have permission to do this": "Du bist dazu nicht berechtigt",
+ "Error loading Widget": "Fehler beim Laden des Widgets",
+ "Pinned messages": "Angeheftete Nachrichten",
+ "Nothing pinned, yet": "Es ist nichts angepinnt. Noch nicht.",
+ "End-to-end encryption isn't enabled": "Ende-zu-Ende-Verschlüsselung ist deaktiviert",
+ "See when people join, leave, or are invited to your active room": "Anzeigen, wenn Leute den aktuellen Raum betreten, verlassen oder in ihn eingeladen werden"
}
From c4b252d941fda74b6b027d5d10e49d04c3fd1e2d Mon Sep 17 00:00:00 2001
From: Bamstam
Date: Wed, 16 Jun 2021 15:22:07 +0000
Subject: [PATCH 02/52] Translated using Weblate (German)
Currently translated at 99.4% (2977 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/
---
src/i18n/strings/de_DE.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 3eb0996792..e86025dc83 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -709,7 +709,7 @@
"Error saving email notification preferences": "Fehler beim Speichern der E-Mail-Benachrichtigungseinstellungen",
"Tuesday": "Dienstag",
"Enter keywords separated by a comma:": "Gib die Schlüsselwörter durch einen Beistrich getrennt ein:",
- "Forward Message": "Nachricht weiterleiten",
+ "Forward Message": "Weiterleiten",
"You have successfully set a password and an email address!": "Du hast erfolgreich ein Passwort und eine E-Mail-Adresse gesetzt!",
"Remove %(name)s from the directory?": "Soll der Raum %(name)s aus dem Verzeichnis entfernt werden?",
"%(brand)s uses many advanced browser features, some of which are not available or experimental in your current browser.": "%(brand)s nutzt zahlreiche fortgeschrittene Browser-Funktionen, die teilweise in deinem aktuell verwendeten Browser noch nicht verfügbar sind oder sich noch im experimentellen Status befinden.",
From 3d34e8e8075c0211abd8565892ad71edb6de5a9f Mon Sep 17 00:00:00 2001
From: Michael Sasser
Date: Wed, 16 Jun 2021 15:14:41 +0000
Subject: [PATCH 03/52] Translated using Weblate (German)
Currently translated at 99.4% (2977 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/
---
src/i18n/strings/de_DE.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index e86025dc83..90530b5b07 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -1635,7 +1635,7 @@
"Show shortcuts to recently viewed rooms above the room list": "Kürzlich besuchte Räume anzeigen",
"Use Single Sign On to continue": "Einmalanmeldung zum Fortfahren nutzen",
"Confirm adding this email address by using Single Sign On to prove your identity.": "Bestätige die neue E-Mail-Adresse mit Single-Sign-On, um deine Identität nachzuweisen.",
- "Single Sign On": "Einmalanmeldung",
+ "Single Sign On": "Single Sign-on",
"Confirm adding email": "Hinzugefügte E-Mail-Addresse bestätigen",
"Confirm adding this phone number by using Single Sign On to prove your identity.": "Bestätige die hinzugefügte Telefonnummer, indem du deine Identität mittels der Einmalanmeldung nachweist.",
"Click the button below to confirm adding this phone number.": "Klicke unten die Schaltfläche, um die hinzugefügte Telefonnummer zu bestätigen.",
From 3b39eca70ff7b5ac4ead3f1a34ceb7cbf4f40874 Mon Sep 17 00:00:00 2001
From: iaiz
Date: Wed, 16 Jun 2021 13:19:42 +0000
Subject: [PATCH 04/52] Translated using Weblate (Spanish)
Currently translated at 100.0% (2993 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/es/
---
src/i18n/strings/es.json | 26 ++++++++++++++++++++++----
1 file changed, 22 insertions(+), 4 deletions(-)
diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json
index 5e8e57bacd..c1fb8e6542 100644
--- a/src/i18n/strings/es.json
+++ b/src/i18n/strings/es.json
@@ -82,7 +82,7 @@
"Fill screen": "Llenar pantalla",
"Filter room members": "Filtrar miembros de la sala",
"Forget room": "Olvidar sala",
- "For security, this session has been signed out. Please sign in again.": "Por seguridad, esta sesión ha sido cerrada. Por favor inicia sesión nuevamente.",
+ "For security, this session has been signed out. Please sign in again.": "Esta sesión ha sido cerrada. Por favor, inicia sesión de nuevo.",
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s de %(fromPowerLevel)s a %(toPowerLevel)s",
"Guests cannot join this room even if explicitly invited.": "Los invitados no pueden unirse a esta sala incluso si se les invita explícitamente.",
"Hangup": "Colgar",
@@ -168,7 +168,7 @@
"Search": "Buscar",
"Search failed": "Falló la búsqueda",
"Seen by %(userName)s at %(dateTime)s": "Visto por %(userName)s el %(dateTime)s",
- "Send Reset Email": "Enviar correo electrónico de restauración",
+ "Send Reset Email": "Enviar correo de restauración",
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s envió una imagen.",
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s invitó a %(targetDisplayName)s a unirse a la sala.",
"Server error": "Error del servidor",
@@ -2021,7 +2021,7 @@
"Your Matrix account on ": "Su cuenta de Matrix en ",
"No identity server is configured: add one in server settings to reset your password.": "No hay ningún servidor de identidad configurado: añada uno en la configuración del servidor para poder restablecer su contraseña.",
"Sign in instead": "Iniciar sesión",
- "A verification email will be sent to your inbox to confirm setting your new password.": "Se enviará un correo electrónico de verificación a su bandeja de entrada para confirmar la configuración de su nueva contraseña.",
+ "A verification email will be sent to your inbox to confirm setting your new password.": "Te enviaremos un correo electrónico de verificación para cambiar tu contraseña.",
"Your password has been reset.": "Su contraseña ha sido restablecida.",
"You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "Ha cerrado todas las sesiones y ya no recibirá más notificaciones push. Para volver a activar las notificaciones, inicie sesión de nuevo en cada dispositivo.",
"Set a new password": "Elegir una nueva contraseña",
@@ -3321,5 +3321,23 @@
"Currently joining %(count)s rooms|other": "Entrando en %(count)s salas",
"No results for \"%(query)s\"": "Ningún resultado para «%(query)s»",
"The user you called is busy.": "La persona a la que has llamado está ocupada.",
- "User Busy": "Persona ocupada"
+ "User Busy": "Persona ocupada",
+ "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Normalmente tus mensajes privados están cifrados, pero esta sala no lo está. Esto puede ser debido a un dispositivo o método no soportado, como las invitaciones por correo. Activa el cifrado en ajustes.",
+ "End-to-end encryption isn't enabled": "El cifrado de extremo a extremo no está activado",
+ "If you can't see who you’re looking for, send them your invite link below.": "Si no encuentras abajo a quien buscas, envíale tu enlace de invitación.",
+ "Teammates might not be able to view or join any private rooms you make.": "Las personas de tu equipo no podrán ver o unirse a ninguna sala privada que crees.",
+ "We're working on this as part of the beta, but just want to let you know.": "Estamos trabajando en ello como parte de la beta, pero te lo queríamos contar.",
+ "Or send invite link": "O envía un enlace de invitación",
+ "Some suggestions may be hidden for privacy.": "Puede que se hayan ocultado algunas sugerencias por motivos de privacidad.",
+ "Search for rooms or people": "Busca salas o gente",
+ "Message preview": "Vista previa del mensaje",
+ "Forward message": "Reenviar mensaje",
+ "Open link": "Abrir enlace",
+ "Sent": "Enviado",
+ "You don't have permission to do this": "No tienes permisos para hacer eso",
+ "Error - Mixed content": "Error - Contenido mezclado",
+ "Error loading Widget": "Error al cargar el widget",
+ "Pinned messages": "Mensajes fijados",
+ "If you have permissions, open the menu on any message and select Pin to stick them here.": "Si tienes permisos, abre el menú de cualquier mensaje y selecciona Fijar para colocarlo aquí.",
+ "Nothing pinned, yet": "Nada fijado, todavía"
}
From 103577f0e6fe5eaa6835314fdaba9693c5b07dd3 Mon Sep 17 00:00:00 2001
From: Szimszon
Date: Tue, 15 Jun 2021 19:52:03 +0000
Subject: [PATCH 05/52] Translated using Weblate (Hungarian)
Currently translated at 100.0% (2993 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hu/
---
src/i18n/strings/hu.json | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index b290f20076..cb749f12a5 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -3376,5 +3376,22 @@
"No results for \"%(query)s\"": "Nincs találat ehhez: %(query)s",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Próbáljon ki más szavakat vagy keressen elgépelést. Néhány találat azért nem látszik, mert privát és meghívóra van szüksége, hogy csatlakozhasson.",
"The user you called is busy.": "A hívott felhasználó foglalt.",
- "User Busy": "Felhasználó foglalt"
+ "User Busy": "Felhasználó foglalt",
+ "If you can't see who you’re looking for, send them your invite link below.": "Ha nem található a keresett személy, küldje el az alábbi hivatkozást neki.",
+ "Some suggestions may be hidden for privacy.": "Adatvédelem miatt néhány javaslat esetleg rejtve van.",
+ "If you have permissions, open the menu on any message and select Pin to stick them here.": "Ha van hozzá jogosultsága, nyissa meg a menüt bármelyik üzenetben és válassza a Kitűz menüpontot a kitűzéshez.",
+ "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "A személyes beszélgetések általában titkosítottak, de ez a szoba nem az. Ez azért lehet, mert nem támogatott eszköz vagy mód használt mint például e-mail meghívók. Titkosítás engedélyezése a beállításokban.",
+ "We're working on this as part of the beta, but just want to let you know.": "A béta funkció ezen részén dolgozunk, csak tudatni szerettük volna.",
+ "Teammates might not be able to view or join any private rooms you make.": "Csapattagok lehet, hogy nem láthatják vagy léphetnek be az ön által készített privát szobákba.",
+ "Or send invite link": "Vagy meghívó link küldése",
+ "Search for rooms or people": "Szobák vagy emberek keresése",
+ "Forward message": "Üzenet továbbítása",
+ "Open link": "Hivatkozás megnyitása",
+ "Sent": "Elküldve",
+ "You don't have permission to do this": "Nincs jogosultsága ehhez",
+ "Error - Mixed content": "Hiba - Vegyes tartalom",
+ "Error loading Widget": "Kisalkalmazás betöltési hiba",
+ "Pinned messages": "Kitűzött üzenetek",
+ "Nothing pinned, yet": "Semmi sincs kitűzve egyenlőre",
+ "End-to-end encryption isn't enabled": "Végpontok közötti titkosítás nincs engedélyezve"
}
From ed306d74562f3be4454d8cfc6a7a8efa54ea7673 Mon Sep 17 00:00:00 2001
From: jelv
Date: Wed, 16 Jun 2021 11:39:17 +0000
Subject: [PATCH 06/52] Translated using Weblate (Dutch)
Currently translated at 100.0% (2993 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nl/
---
src/i18n/strings/nl.json | 26 ++++++++++++++++++++++----
1 file changed, 22 insertions(+), 4 deletions(-)
diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json
index 7299e9d161..1818a64e54 100644
--- a/src/i18n/strings/nl.json
+++ b/src/i18n/strings/nl.json
@@ -1177,7 +1177,7 @@
"Homeserver URL": "Thuisserver-URL",
"Identity Server URL": "Identiteitsserver-URL",
"Free": "Gratis",
- "Join millions for free on the largest public server": "Doe mee met miljoenen anderen op de grootste openbare server",
+ "Join millions for free on the largest public server": "Neem deel aan de grootste openbare server met miljoenen anderen",
"Premium": "Premium",
"Premium hosting for organisations Learn more": "Premium hosting voor organisaties Lees meer",
"Other": "Overige",
@@ -1513,7 +1513,7 @@
"View": "Bekijken",
"Find a room…": "Zoek een gesprek…",
"Find a room… (e.g. %(exampleRoom)s)": "Zoek een gesprek… (bv. %(exampleRoom)s)",
- "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Als u het gesprek dat u zoekt niet kunt vinden, vraag dan een uitnodiging, of Maak een nieuw gesprek aan.",
+ "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Als u het gesprek dat u zoekt niet kunt vinden, vraag dan een uitnodiging of maak een nieuw gesprek aan.",
"Explore rooms": "Gesprekken ontdekken",
"Show previews/thumbnails for images": "Miniaturen voor afbeeldingen tonen",
"Clear cache and reload": "Cache wissen en herladen",
@@ -1932,7 +1932,7 @@
"Jump to first unread room.": "Ga naar het eerste ongelezen gesprek.",
"Jump to first invite.": "Ga naar de eerste uitnodiging.",
"Session verified": "Sessie geverifieerd",
- "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Uw nieuwe sessie is nu geverifieerd. U heeft nu toegang tot uw versleutelde berichten, en deze sessie zal voor andere gebruikers als vertrouwd gemarkeerd worden.",
+ "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Uw nieuwe sessie is nu geverifieerd. U heeft nu toegang tot uw versleutelde berichten en deze sessie zal voor andere gebruikers als vertrouwd gemarkeerd worden.",
"Your new session is now verified. Other users will see it as trusted.": "Uw nieuwe sessie is nu geverifieerd. Ze zal voor andere gebruikers als vertrouwd gemarkeerd worden.",
"Without completing security on this session, it won’t have access to encrypted messages.": "Als u de beveiliging van deze sessie niet vervolledigt, zal ze geen toegang hebben tot uw versleutelde berichten.",
"Go Back": "Terugkeren",
@@ -3267,5 +3267,23 @@
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Probeer andere woorden of controleer op typefouten. Sommige resultaten zijn mogelijk niet zichtbaar omdat ze privé zijn of u een uitnodiging nodig heeft om deel te nemen.",
"No results for \"%(query)s\"": "Geen resultaten voor \"%(query)s\"",
"The user you called is busy.": "De gebruiker die u belde is bezet.",
- "User Busy": "Gebruiker Bezet"
+ "User Busy": "Gebruiker Bezet",
+ "We're working on this as part of the beta, but just want to let you know.": "We werken hieraan als onderdeel van de beta, maar we willen het u gewoon laten weten.",
+ "Teammates might not be able to view or join any private rooms you make.": "Teamgenoten zijn mogelijk niet in staat zijn om privégesprekken die u maakt te bekijken of er lid van te worden.",
+ "Or send invite link": "Of verstuur uw uitnodigingslink",
+ "If you can't see who you’re looking for, send them your invite link below.": "Als u niet kunt vinden wie u zoekt, stuur ze dan uw uitnodigingslink hieronder.",
+ "Some suggestions may be hidden for privacy.": "Sommige suggesties kunnen om privacyredenen verborgen zijn.",
+ "Search for rooms or people": "Zoek naar gesprekken of personen",
+ "Message preview": "Voorbeeld van bericht",
+ "Forward message": "Bericht doorsturen",
+ "Open link": "Koppeling openen",
+ "Sent": "Verstuurd",
+ "You don't have permission to do this": "U heeft geen rechten om dit te doen",
+ "Error - Mixed content": "Fout - Gemengde inhoud",
+ "Error loading Widget": "Fout bij laden Widget",
+ "Pinned messages": "Vastgeprikte berichten",
+ "If you have permissions, open the menu on any message and select Pin to stick them here.": "Als u de rechten heeft, open dan het menu op elk bericht en selecteer Vastprikken om ze hier te zetten.",
+ "Nothing pinned, yet": "Nog niks vastgeprikt",
+ "End-to-end encryption isn't enabled": "Eind-tot-eind-versleuteling is uitgeschakeld",
+ "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Uw privéberichten zijn normaal gesproken versleuteld, maar dit gesprek niet. Meestal is dit te wijten aan een niet-ondersteund apparaat of methode die wordt gebruikt, zoals e-mailuitnodigingen. Versleuting inschakelen in instellingen."
}
From 3ddb103dd6e2a494f87e57a7547a985322dbf708 Mon Sep 17 00:00:00 2001
From: LinAGKar
Date: Thu, 17 Jun 2021 08:20:23 +0000
Subject: [PATCH 07/52] Translated using Weblate (Swedish)
Currently translated at 100.0% (2993 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sv/
---
src/i18n/strings/sv.json | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index f026e0fe02..6033b561bd 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -3311,5 +3311,23 @@
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Testa andra ord eller kolla efter felskrivningar. Vissa resultat kanske inte visas för att de är privata och du behöver en inbjudan för att gå med i dem.",
"No results for \"%(query)s\"": "Inga resultat för \"%(query)s\"",
"The user you called is busy.": "Användaren du ringde är upptagen.",
- "User Busy": "Användare upptagen"
+ "User Busy": "Användare upptagen",
+ "We're working on this as part of the beta, but just want to let you know.": "Vi jobbar på detta som en del av betan, men vi ville låta dig veta.",
+ "Teammates might not be able to view or join any private rooms you make.": "Teammedlemmar kanske inte kan se eller gå med i privata rum du skapar.",
+ "Or send invite link": "Eller skicka inbjudningslänk",
+ "If you can't see who you’re looking for, send them your invite link below.": "Om du inte kan se den du letar efter, skicka dem din inbjudningslänk nedan.",
+ "Some suggestions may be hidden for privacy.": "Vissa förslag kan vara dolda av sekretesskäl.",
+ "Search for rooms or people": "Sök efter rum eller personer",
+ "Message preview": "Meddelandeförhandsvisning",
+ "Forward message": "Vidarebefordra meddelande",
+ "Open link": "Öppna länk",
+ "Sent": "Skickat",
+ "You don't have permission to do this": "Du har inte behörighet att göra detta",
+ "Error - Mixed content": "Fel - blandat innehåll",
+ "Error loading Widget": "Fel vid laddning av widget",
+ "Pinned messages": "Fästa meddelanden",
+ "If you have permissions, open the menu on any message and select Pin to stick them here.": "Om du har behörighet, öppna menyn på ett meddelande och välj Fäst för att fösta dem här.",
+ "Nothing pinned, yet": "Inget fäst än",
+ "End-to-end encryption isn't enabled": "Totalsträckskryptering är inte aktiverat",
+ "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Dina privata meddelanden är normalt krypterade, men det här rummet är inte det. Oftast så beror detta på att en enhet eller metod som används ej stöds, som e-postinbjudningar. Aktivera kryptering i inställningarna."
}
From a9d33cbe1f258b8b37dcbfaa8c401c51bd79b95f Mon Sep 17 00:00:00 2001
From: Percy
Date: Wed, 16 Jun 2021 16:00:16 +0000
Subject: [PATCH 08/52] Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (2993 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/
---
src/i18n/strings/zh_Hans.json | 40 +++++++++++++++++++++++++----------
1 file changed, 29 insertions(+), 11 deletions(-)
diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json
index 3267060d64..7aa0d75539 100644
--- a/src/i18n/strings/zh_Hans.json
+++ b/src/i18n/strings/zh_Hans.json
@@ -510,8 +510,8 @@
"Restricted": "受限用户",
"To use it, just wait for autocomplete results to load and tab through them.": "若要使用自动补全,只要等待自动补全结果加载完成,按 Tab 键切换即可。",
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s 将他们的昵称修改成了 %(displayName)s 。",
- "Stickerpack": "贴图集",
- "You don't currently have any stickerpacks enabled": "你目前没有启用任何贴图集",
+ "Stickerpack": "贴纸包",
+ "You don't currently have any stickerpacks enabled": "你目前未启用任何贴纸包",
"Key request sent.": "已发送密钥共享请求。",
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "如果你是房间中最后一位有权限的用户,在你降低自己的权限等级后将无法撤回此修改,因为你将无法重新获得权限。",
"You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "你将无法撤回此修改,因为你正在将此用户的滥权等级提升至与你相同。",
@@ -1071,10 +1071,10 @@
"Ignored users": "已忽略的用户",
"Bulk options": "批量选择",
"Key backup": "密钥备份",
- "Security & Privacy": "安全与隐私",
+ "Security & Privacy": "隐私安全",
"Missing media permissions, click the button below to request.": "缺少媒体权限,点击下面的按钮以请求权限。",
"Request media permissions": "请求媒体权限",
- "Voice & Video": "语音与视频",
+ "Voice & Video": "语音视频",
"Room information": "聊天室信息",
"Internal room ID:": "内部聊天室 ID:",
"Room version": "聊天室版本",
@@ -1566,7 +1566,7 @@
"If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "如果你不想使用 以发现你认识的现存联系人并被其发现,请在下方输入另一个身份服务器。",
"Identity Server": "身份服务器",
"You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "你现在没有使用身份服务器。若想发现你认识的现存联系人并被其发现,请在下方添加一个身份服务器。",
- "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "从你的身份服务器断开连接意味着你将不可被别的用户发现,同时你也将不能用邮箱或电话邀请别人。",
+ "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "断开身份服务器连接意味着你将无法被其他用户发现,同时你也将无法使用电子邮件或电话邀请别人。",
"Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "使用身份服务器是可选的。如果你选择不使用身份服务器,你将不能被别的用户发现,也不能用邮箱或电话邀请别人。",
"Do not use an identity server": "不使用身份服务器",
"Enter a new identity server": "输入一个新的身份服务器",
@@ -1686,9 +1686,9 @@
"Cannot connect to integration manager": "不能连接到集成管理器",
"The integration manager is offline or it cannot reach your homeserver.": "此集成管理器为离线状态或者其不能访问你的主服务器。",
"check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "检查你的浏览器是否安装有可能屏蔽身份服务器的插件(例如 Privacy Badger)",
- "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "使用集成管理器 (%(serverName)s) 以管理机器人、挂件和贴图集。",
- "Use an Integration Manager to manage bots, widgets, and sticker packs.": "使用集成管理器以管理机器人、挂件和贴图集。",
- "Manage integrations": "管理集成",
+ "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "使用集成管理器 (%(serverName)s) 以管理机器人、挂件和贴纸包。",
+ "Use an Integration Manager to manage bots, widgets, and sticker packs.": "使用集成管理器以管理机器人、挂件和贴纸包。",
+ "Manage integrations": "集成管理",
"Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "集成管理器接收配置数据,并可以以你的名义修改挂件、发送聊天室邀请及设置权限级别。",
"Use between %(min)s pt and %(max)s pt": "请使用介于 %(min)s pt 和 %(max)s pt 之间的大小",
"Deactivate account": "停用账号",
@@ -1995,7 +1995,7 @@
"Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "验证此设备以将其标记为已信任。在收发端对端加密消息时,信任设备可让你与其他用户更加放心。",
"Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "验证此设备会将其标记为已信任,与此同时,其他验证了你的用户也会信任此设备。",
"Integrations are disabled": "集成已禁用",
- "Enable 'Manage Integrations' in Settings to do this.": "在设置中启用「管理集成」以执行此操作。",
+ "Enable 'Manage Integrations' in Settings to do this.": "在设置中启用「管理管理」以执行此操作。",
"Integrations not allowed": "集成未被允许",
"Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "你的 %(brand)s 不允许你使用集成管理器来完成此操作。请联系管理员。",
"To continue, use Single Sign On to prove your identity.": "要继续,请使用单点登录证明你的身份。",
@@ -2077,7 +2077,7 @@
"Integration Manager": "集成管理器",
"Find others by phone or email": "通过电话或邮箱寻找别人",
"Be found by phone or email": "通过电话或邮箱被寻找",
- "Use bots, bridges, widgets and sticker packs": "使用机器人、桥接、挂件和贴图集",
+ "Use bots, bridges, widgets and sticker packs": "使用机器人、桥接、挂件和贴纸包",
"Terms of Service": "服务协议",
"To continue you need to accept the terms of this service.": "要继续,你需要接受此服务协议。",
"Service": "服务",
@@ -3280,5 +3280,23 @@
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "尝试不同的单词或检查拼写错误。某些结果可能不可见,因为它们属于私有的,你需要一个邀请才能加入。",
"No results for \"%(query)s\"": "「%(query)s」没有结果",
"The user you called is busy.": "你所拨打的用户正在忙碌中。",
- "User Busy": "用户正在忙"
+ "User Busy": "用户正在忙",
+ "We're working on this as part of the beta, but just want to let you know.": "我们正在研究让它成为测试版的一部分,但只想让你找到。",
+ "Teammates might not be able to view or join any private rooms you make.": "队友可能无法查看或加入你所创建的任何一个私有聊天室。",
+ "Or send invite link": "或发送邀请链接",
+ "If you can't see who you’re looking for, send them your invite link below.": "如果你找不到你正在寻找的人,请在下方向他们发送你的邀请链接。",
+ "Some suggestions may be hidden for privacy.": "出于隐私考虑,部分建议可能会被隐藏。",
+ "Search for rooms or people": "搜索聊天室或用户",
+ "Message preview": "消息预览",
+ "Forward message": "转发消息",
+ "Open link": "打开链接",
+ "Sent": "已发送",
+ "You don't have permission to do this": "你无权执行此操作",
+ "Error - Mixed content": "错误 - 混合内容",
+ "Error loading Widget": "加载挂件时发生错误",
+ "Pinned messages": "已置顶的消息",
+ "If you have permissions, open the menu on any message and select Pin to stick them here.": "如果你拥有权限,请打开任何消息的菜单并选择置顶将它们粘贴至此。",
+ "Nothing pinned, yet": "没有置顶",
+ "End-to-end encryption isn't enabled": "未启用端对端加密",
+ "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "你的私人信息通常是被加密的,但此聊天室并未加密。一般而言,这可能是因为使用了不受支持的设备或方法,如电子邮件邀请。在设置中启用加密。"
}
From c0bc019e660c973e7707f680125330ca9718c853 Mon Sep 17 00:00:00 2001
From: Jeff Huang
Date: Wed, 16 Jun 2021 02:14:34 +0000
Subject: [PATCH 09/52] Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (2993 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/
---
src/i18n/strings/zh_Hant.json | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index 053839f937..d9429fc1c3 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -3384,5 +3384,22 @@
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "嘗試不同的詞或是檢查拼字。某些結果可能不可見,因為其為私人的,您必須要有邀請才能加入。",
"No results for \"%(query)s\"": "「%(query)s」沒有結果",
"The user you called is busy.": "您想要通話的使用者目前忙碌中。",
- "User Busy": "使用者忙碌"
+ "User Busy": "使用者忙碌",
+ "We're working on this as part of the beta, but just want to let you know.": "我們正將此作為測試版的一部分來處理,但只是想讓您知道。",
+ "Teammates might not be able to view or join any private rooms you make.": "隊友可能無法檢視或加入您建立的任何私人聊天室。",
+ "Or send invite link": "或傳送邀請連結",
+ "If you can't see who you’re looking for, send them your invite link below.": "如果您看不到您要找的人,請在下方向他們傳送您的邀請連結。",
+ "Some suggestions may be hidden for privacy.": "出於隱私考量,可能會隱藏一些建議。",
+ "Search for rooms or people": "搜尋聊天室或夥伴",
+ "Forward message": "轉寄訊息",
+ "Open link": "開啟連結",
+ "Sent": "已傳送",
+ "You don't have permission to do this": "您無權執行此動作",
+ "Error - Mixed content": "錯誤 - 混合內容",
+ "Error loading Widget": "載入小工具時發生錯誤",
+ "Pinned messages": "已釘選的訊息",
+ "If you have permissions, open the menu on any message and select Pin to stick them here.": "如果您有權限,請開啟任何訊息的選單,並選取釘選以將它們貼到這裡。",
+ "Nothing pinned, yet": "尚未釘選任何東西",
+ "End-to-end encryption isn't enabled": "端到端加密未啟用",
+ "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "您的私人訊息通常是被加密的,但此聊天室不是。一般來說,這可能是因為使用了不支援的裝置或方法,例如電子郵件邀請。在設定中啟用加密。"
}
From 1013c1ec2953d77951d2c3797c97c903e89142b3 Mon Sep 17 00:00:00 2001
From: random
Date: Thu, 17 Jun 2021 08:27:19 +0000
Subject: [PATCH 10/52] Translated using Weblate (Italian)
Currently translated at 100.0% (2993 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/
---
src/i18n/strings/it.json | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json
index c83800e82a..207ff24d58 100644
--- a/src/i18n/strings/it.json
+++ b/src/i18n/strings/it.json
@@ -3381,5 +3381,22 @@
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Prova parole diverse o controlla errori di battitura. Alcuni risultati potrebbero non essere visibili dato che sono privati e ti servirebbe un invito per unirti.",
"No results for \"%(query)s\"": "Nessun risultato per \"%(query)s\"",
"The user you called is busy.": "L'utente che hai chiamato è occupato.",
- "User Busy": "Utente occupato"
+ "User Busy": "Utente occupato",
+ "We're working on this as part of the beta, but just want to let you know.": "Stiamo lavorando a questo come parte della beta, ma vogliamo almeno fartelo sapere.",
+ "Teammates might not be able to view or join any private rooms you make.": "I tuoi compagni potrebbero non riuscire a vedere o unirsi a qualsiasi stanza privata che crei.",
+ "Or send invite link": "O manda un collegamento di invito",
+ "If you can't see who you’re looking for, send them your invite link below.": "Se non vedi chi stai cercando, mandagli il collegamento di invito sottostante.",
+ "Some suggestions may be hidden for privacy.": "Alcuni suggerimenti potrebbero essere nascosti per privacy.",
+ "Search for rooms or people": "Cerca stanze o persone",
+ "Forward message": "Inoltra messaggio",
+ "Open link": "Apri collegamento",
+ "Sent": "Inviato",
+ "You don't have permission to do this": "Non hai il permesso per farlo",
+ "Error - Mixed content": "Errore - Contenuto misto",
+ "Error loading Widget": "Errore di caricamento del widget",
+ "Nothing pinned, yet": "Non c'è ancora nulla di ancorato",
+ "If you have permissions, open the menu on any message and select Pin to stick them here.": "Se ne hai il permesso, apri il menu di qualsiasi messaggio e seleziona Fissa per ancorarlo qui.",
+ "Pinned messages": "Messaggi ancorati",
+ "End-to-end encryption isn't enabled": "La crittografia end-to-end non è attiva",
+ "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "I tuoi messaggi privati normalmente sono cifrati, ma questa stanza non lo è. Di solito ciò è dovuto ad un dispositivo non supportato o dal metodo usato, come gli inviti per email. Attiva la crittografia nelle impostazioni."
}
From f1a8c838f63ca9085ad042b6d2026308112cd0cd Mon Sep 17 00:00:00 2001
From: waclaw66
Date: Wed, 16 Jun 2021 07:16:47 +0000
Subject: [PATCH 11/52] Translated using Weblate (Czech)
Currently translated at 100.0% (2993 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/
---
src/i18n/strings/cs.json | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json
index a84a10f21c..27235665aa 100644
--- a/src/i18n/strings/cs.json
+++ b/src/i18n/strings/cs.json
@@ -3298,5 +3298,23 @@
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Vyzkoušejte jiná slova nebo zkontrolujte překlepy. Některé výsledky nemusí být viditelné, protože jsou soukromé a potřebujete k nim pozvánku.",
"No results for \"%(query)s\"": "Žádné výsledky pro \"%(query)s\"",
"The user you called is busy.": "Volaný uživatel je zaneprázdněn.",
- "User Busy": "Uživatel zaneprázdněn"
+ "User Busy": "Uživatel zaneprázdněn",
+ "We're working on this as part of the beta, but just want to let you know.": "Pracujeme na tom v rámci beta verze, ale jen vás o tom chceme informovat.",
+ "Teammates might not be able to view or join any private rooms you make.": "Je možné, že spolupracovníci nebudou moci zobrazit soukromé místnosti, které jste vytvořili, nebo se k nim připojit.",
+ "Or send invite link": "Nebo pošlete pozvánku",
+ "If you can't see who you’re looking for, send them your invite link below.": "Pokud jste nenašli, koho hledáte, pošlete mu odkaz na pozvánku níže.",
+ "Some suggestions may be hidden for privacy.": "Některé návrhy mohou být z důvodu ochrany soukromí skryty.",
+ "Search for rooms or people": "Hledat místnosti nebo osoby",
+ "Message preview": "Náhled zprávy",
+ "Forward message": "Přeposlat zprávu",
+ "Open link": "Otevřít odkaz",
+ "Sent": "Odesláno",
+ "You don't have permission to do this": "K tomu nemáte povolení",
+ "Error - Mixed content": "Chyba - Smíšený obsah",
+ "Error loading Widget": "Chyba při načítání widgetu",
+ "Pinned messages": "Připnuté zprávy",
+ "If you have permissions, open the menu on any message and select Pin to stick them here.": "Pokud máte oprávnění, otevřete nabídku na libovolné zprávě a výběrem možnosti Připnout je sem vložte.",
+ "Nothing pinned, yet": "Zatím není nic připnuto",
+ "End-to-end encryption isn't enabled": "Není povoleno koncové šifrování",
+ "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Vaše soukromé zprávy jsou obvykle šifrované, ale tato místnost není. Obvykle je to způsobeno nepodporovaným zařízením nebo použitou metodou, například emailovými pozvánkami. Zapněte šifrování v nastavení."
}
From fcf31240e319f0e4b76b05d427473c3900b8c23e Mon Sep 17 00:00:00 2001
From: XoseM
Date: Wed, 16 Jun 2021 06:03:11 +0000
Subject: [PATCH 12/52] Translated using Weblate (Galician)
Currently translated at 100.0% (2993 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/
---
src/i18n/strings/gl.json | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index abb4776a55..b880c5b548 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -3381,5 +3381,22 @@
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Intentao con outras palabras e fíxate nos erros de escritura. Algúns resultados poderían non ser visibles porque son privados e precisas un convite.",
"No results for \"%(query)s\"": "Sen resultados para \"%(query)s\"",
"The user you called is busy.": "A persoa á que chamas está ocupada.",
- "User Busy": "Usuaria ocupada"
+ "User Busy": "Usuaria ocupada",
+ "We're working on this as part of the beta, but just want to let you know.": "Estamos traballando nesto como parte da beta, só queriamos que o soubeses.",
+ "Teammates might not be able to view or join any private rooms you make.": "As outras compañeiras de grupo poderían non ver ou unirse ás salas privadas que creas.",
+ "Or send invite link": "Ou envía ligazón de convite",
+ "If you can't see who you’re looking for, send them your invite link below.": "Se non podes a quen estás a buscar, envíalle ti esta ligazón de convite.",
+ "Some suggestions may be hidden for privacy.": "Algunhas suxestións poderían estar agochadas por privacidade.",
+ "Search for rooms or people": "Busca salas ou persoas",
+ "Forward message": "Reenviar mensaxe",
+ "Open link": "Abrir ligazón",
+ "Sent": "Enviado",
+ "You don't have permission to do this": "Non tes permiso para facer isto",
+ "Error - Mixed content": "Erro - Contido variado",
+ "Error loading Widget": "Erro ao cargar o Widget",
+ "Pinned messages": "Mensaxes fixadas",
+ "If you have permissions, open the menu on any message and select Pin to stick them here.": "Se tes permisos, abre o menú en calquera mensaxe e elixe Fixar para pegalos aquí.",
+ "Nothing pinned, yet": "Nada fixado, por agora",
+ "End-to-end encryption isn't enabled": "Non está activado o cifrado de extremo-a-extremo",
+ "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "As túas mensaxes privadas normalmente están cifradas, pero esta sala non. Habitualmente esto é debido a que se utiliza un dispositivo ou métodos no soportados, como convites por email. Activa o cifrado nos axustes."
}
From e7b05963802f9eb6b8b1192c9a9e95dfa8958ff0 Mon Sep 17 00:00:00 2001
From: Besnik Bleta
Date: Wed, 16 Jun 2021 10:14:11 +0000
Subject: [PATCH 13/52] Translated using Weblate (Albanian)
Currently translated at 99.7% (2985 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sq/
---
src/i18n/strings/sq.json | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json
index 4996decbaf..b2101151e1 100644
--- a/src/i18n/strings/sq.json
+++ b/src/i18n/strings/sq.json
@@ -3370,5 +3370,21 @@
"The user you called is busy.": "Përdoruesi që thirrët është i zënë.",
"User Busy": "Përdoruesi Është i Zënë",
"Kick, ban, or invite people to your active room, and make you leave": "Përzini, dëboni, ose ftoni persona te dhoma juaj aktive, dhe bëni largimin tuaj",
- "Kick, ban, or invite people to this room, and make you leave": "Përzini, dëboni, ose ftoni persona në këtë dhomë, dhe bëni largimin tuaj"
+ "Kick, ban, or invite people to this room, and make you leave": "Përzini, dëboni, ose ftoni persona në këtë dhomë, dhe bëni largimin tuaj",
+ "We're working on this as part of the beta, but just want to let you know.": "Po merremi me këtë, si pjesë e versionit beta, thjesht duam ta dini.",
+ "Teammates might not be able to view or join any private rooms you make.": "Anëtarët e ekipit mund të mos jenë në gjendje të shohin ose hyjnë në çfarëdo dhome private që krijoni.",
+ "Or send invite link": "Ose dërgoni një lidhje ftese",
+ "If you can't see who you’re looking for, send them your invite link below.": "Nëse s’e shihni atë që po kërkoni, dërgojini nga më poshtë një lidhje ftese.",
+ "Some suggestions may be hidden for privacy.": "Disa sugjerime mund të jenë fshehur, për arsye privatësie.",
+ "Search for rooms or people": "Kërkoni për dhoma ose persona",
+ "Forward message": "Përcille mesazhin",
+ "Open link": "Hape lidhjen",
+ "You don't have permission to do this": "S’keni leje ta bëni këtë",
+ "Error - Mixed content": "Gabim - Lëndë e përzierë",
+ "Error loading Widget": "Gabim në ngarkim Widget-i",
+ "Pinned messages": "Mesazhe të fiksuar",
+ "If you have permissions, open the menu on any message and select Pin to stick them here.": "Nëse keni leje, hapni menunë për çfarëdo mesazhi dhe përzgjidhni Fiksoje, për ta ngjitur këtu.",
+ "Nothing pinned, yet": "Ende pa fiksuar gjë",
+ "End-to-end encryption isn't enabled": "Fshehtëzimi skaj-më-skaj s’është i aktivizuar",
+ "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Mesazhet tuaja private normalisht fshehtëzohen, por kjo dhomë nuk fshehtëzohet. Zakonisht kjo vjen si pasojë e përdorimit të një pajisjeje apo metode të pambuluar, bie fjala, ftesa me email. Aktivizoni fshehtëzimin që nga rregullimet."
}
From d33cfd6869e61e79b2f1d7d8f9da30dfbec9a943 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?=
Date: Fri, 18 Jun 2021 21:35:18 +0000
Subject: [PATCH 14/52] Translated using Weblate (Estonian)
Currently translated at 99.8% (2988 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/
---
src/i18n/strings/et.json | 31 ++++++++++++++++++++++++-------
1 file changed, 24 insertions(+), 7 deletions(-)
diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index a8e267fb01..9454efd956 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -126,7 +126,7 @@
"Explore Public Rooms": "Sirvi avalikke jututubasid",
"Explore": "Uuri",
"Filter rooms…": "Filtreeri jututubasid…",
- "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Liites kokku kasutajaid ja jututubasid loo oma kogukond! Loo kogukonna koduleht, et märkida oma koht Matrix'i universumis.",
+ "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Liites kokku kasutajaid ja jututubasid loo oma kogukond! Kogukonna kodulehega tähistad oma koha Matrix'i universumis.",
"Explore rooms": "Uuri jututubasid",
"If you've joined lots of rooms, this might take a while": "Kui oled liitunud paljude jututubadega, siis see võib natuke aega võtta",
"If disabled, messages from encrypted rooms won't appear in search results.": "Kui see seadistus pole kasutusel, siis krüptitud jututubade sõnumeid otsing ei vaata.",
@@ -2218,7 +2218,7 @@
"%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.": "%(brand)s ei võimalda veebibrauseris töötades krüptitud sõnumeid turvaliselt puhverdada. Selleks, et krüptitud sõnumeid saaks otsida, kasuta %(brand)s Desktop rakendust Matrix'i kliendina.",
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Krüptitud sõnumid kasutavad läbivat krüptimist. Ainult sinul ja saaja(te)l on võtmed selliste sõnumite lugemiseks.",
"Unable to load key backup status": "Võtmete varunduse oleku laadimine ei õnnestunud",
- "Restore from Backup": "Taasta varundusest",
+ "Restore from Backup": "Taasta varukoopiast",
"This session is backing up your keys. ": "See sessioon varundab sinu krüptovõtmeid. ",
"This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "See sessioon ei varunda sinu krüptovõtmeid, aga sul on olemas varundus, millest saad taastada ning millele saad võtmeid lisada.",
"Success": "Õnnestus",
@@ -3298,7 +3298,7 @@
"You have no ignored users.": "Sa ei ole veel kedagi eiranud.",
"Play": "Esita",
"Pause": "Peata",
- "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Kas sa tahaksid katsetada? Sa tutvud meie rakenduse uuendustega teistest varem ja võib-olla isegi saad mõjutada arenduse lõpptulemust. Lisateavet liad siit.",
+ "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Kas sa tahaksid katsetada? Sa tutvud meie rakenduse uuendustega teistest varem ja võib-olla isegi saad mõjutada arenduse lõpptulemust. Lisateavet leiad siit.",
"This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "See on katseline funktsionaalsus. Seetõttu uued kutse saanud kasutajad peavad tegelikuks liitumiseks avama kutse siin .",
"To join %(spaceName)s, turn on the Spaces beta": "%(spaceName)s kogukonnakeskusega liitumiseks lülita sisse vastav katseline funktsionaalsus",
"To view %(spaceName)s, turn on the Spaces beta": "%(spaceName)s kogukonnakeskuse vaatamiseks lülita sisse vastav katseline funktsionaalsus",
@@ -3327,9 +3327,9 @@
"Please enter a name for the space": "Palun sisesta kogukonnakeskuse nimi",
"Connecting": "Kõne on ühendamisel",
"Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Kasuta võrdõigusvõrku 1:1 kõnede jaoks (kui sa P2P-võrgu sisse lülitad, siis teine osapool ilmselt näeb sinu IP-aadressi)",
- "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Rakenduse beetaversioon on saadaval veebirakendusena, töölauarakendusena ja Androidi jaoks. Kõik funtsionaalsused ei pruugi sinu koduserveri poolt olla toetatud.",
+ "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Rakenduse beetaversioon on saadaval veebirakendusena, töölauarakendusena ja Androidi jaoks. Kõik funktsionaalsused ei pruugi sinu koduserveri poolt olla toetatud.",
"You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Sa võid beetaversiooni kasutamise lõpetada niipea, kui tahad. Selleks klõpsi beeta-silti, mida näed siin samas ülal.",
- "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "Käivitame %(brand)s uuesti nii, et kogukonnakeskused on kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid on siis välja lülitatud.",
+ "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "Käivitame %(brand)s'i uuesti nii, et kogukonnakeskused on kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid on siis välja lülitatud.",
"%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Käivitame %(brand)s uuesti nii, et kogukonnakeskused ei ole kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid saavad jälle olema kasutusel.",
"Beta available for web, desktop and Android. Thank you for trying the beta.": "Rakenduse beetaversioon on saadaval veebirakendusena, töölauarakendusena ja Androidi jaoks. Tänud, et oled huviline katsetama meie rakendust.",
"Spaces are a new way to group rooms and people.": "Kogukonnakeskused on uus viis jututubade ja inimeste ühendamiseks.",
@@ -3344,7 +3344,7 @@
"Add reaction": "Lisa reaktsioon",
"Send and receive voice messages": "Saada ja võta vastu häälsõnumeid",
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Sinu tagasiside aitab teha kogukonnakeskuseid paremaks. Mida detailsemalt sa oma arvamust kirjeldad, seda parem.",
- "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Kui sa lahkud, siis käivitame %(brand)s uuesti nii, et kogukonnakeskused ei ole kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid saavad jälle olema kasutusel.",
+ "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Kui sa lahkud, siis käivitame %(brand)s'i uuesti nii, et kogukonnakeskused ei ole kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid saavad jälle olema kasutusel.",
"Message search initialisation failed": "Sõnumite otsingu alustamine ei õnnestunud",
"sends space invaders": "korraldab ühe pisikese tulnukate vallutusretke",
"Sends the given message with a space themed effect": "Saadab antud sõnumi kosmoseteemalise efektiga",
@@ -3354,5 +3354,22 @@
"No results for \"%(query)s\"": "Päringule „%(query)s“ pole vastuseid",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Proovi muid otsingusõnu või kontrolli, et neis polnud trükivigu. Kuna mõned otsingutulemused on privaatsed ja sa vajad kutset nende nägemiseks, siis kõiki tulemusi siin ei pruugi näha olla.",
"Currently joining %(count)s rooms|other": "Parasjagu liitun %(count)s jututoaga",
- "Currently joining %(count)s rooms|one": "Parasjagu liitun %(count)s jututoaga"
+ "Currently joining %(count)s rooms|one": "Parasjagu liitun %(count)s jututoaga",
+ "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Sinu isiklikud sõnumid on tavaliselt läbivalt krüptitud, aga see jututuba ei ole. Tavaliselt on põhjuseks, et kasutusel on mõni seade või meetod nagu e-posti põhised kutsed, mis krüptimist veel ei toeta. Läbiva krüptimise saad sisse lülitada seadistustes.",
+ "We're working on this as part of the beta, but just want to let you know.": "Me küll alles arendame seda beetafunktsionaalsust, aga soovime, et sa sellest juba teaksid.",
+ "Teammates might not be able to view or join any private rooms you make.": "Kui muudad jututoa privaatseks, siis sinu kaaslased ei pruugi seda näha ega temaga liituda.",
+ "If you can't see who you’re looking for, send them your invite link below.": "Kui sa ei leia otsitavaid, siis saada neile kutse.",
+ "Or send invite link": "Või saada kutse link",
+ "If you have permissions, open the menu on any message and select Pin to stick them here.": "Kui sul on vastavad õigused olemas, siis ava sõnumi juuresolev menüü ning püsisõnumi tekitamiseks vali Klammerda.",
+ "Pinned messages": "Klammerdatud sõnumid",
+ "Nothing pinned, yet": "Klammerdatud sõnumeid veel pole",
+ "End-to-end encryption isn't enabled": "Läbiv krüptimine pole kasutusel",
+ "Some suggestions may be hidden for privacy.": "Mõned soovitused võivad privaatsusseadistuste tõttu olla peidetud.",
+ "Search for rooms or people": "Otsi jututubasid või inimesi",
+ "Forward message": "Edasta sõnum",
+ "Open link": "Ava link",
+ "Sent": "Saadetud",
+ "You don't have permission to do this": "Sul puuduvad selleks toiminguks õigused",
+ "Error - Mixed content": "Viga - erinev sisu",
+ "Error loading Widget": "Viga vidina laadimisel"
}
From cecf0ce299b7208fc2fe03e54c7c7adbeb06a98d Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 22 Jun 2021 20:41:26 +0100
Subject: [PATCH 15/52] Convert MessagePanel, TimelinePanel, ScrollPanel, and
more to Typescript
---
src/@types/global.d.ts | 32 +-
.../structures/AutoHideScrollbar.tsx | 6 +-
.../{MessagePanel.js => MessagePanel.tsx} | 723 +++++++++---------
.../structures/NotificationPanel.tsx | 3 +-
src/components/structures/RoomDirectory.tsx | 17 +-
src/components/structures/RoomView.tsx | 7 +-
.../{ScrollPanel.js => ScrollPanel.tsx} | 431 ++++++-----
.../{TimelinePanel.js => TimelinePanel.tsx} | 637 ++++++++-------
.../views/dialogs/ForwardDialog.tsx | 33 +-
.../{ErrorBoundary.js => ErrorBoundary.tsx} | 35 +-
.../views/elements/EventListSummary.tsx | 14 +-
.../views/elements/EventTilePreview.tsx | 11 +-
.../views/elements/MemberEventListSummary.tsx | 14 +-
.../{DateSeparator.js => DateSeparator.tsx} | 26 +-
...ErrorBoundary.js => TileErrorBoundary.tsx} | 28 +-
src/components/views/rooms/EventTile.tsx | 18 +-
16 files changed, 1087 insertions(+), 948 deletions(-)
rename src/components/structures/{MessagePanel.js => MessagePanel.tsx} (64%)
rename src/components/structures/{ScrollPanel.js => ScrollPanel.tsx} (73%)
rename src/components/structures/{TimelinePanel.js => TimelinePanel.tsx} (75%)
rename src/components/views/elements/{ErrorBoundary.js => ErrorBoundary.tsx} (80%)
rename src/components/views/messages/{DateSeparator.js => DateSeparator.tsx} (82%)
rename src/components/views/messages/{TileErrorBoundary.js => TileErrorBoundary.tsx} (77%)
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index 7eff341095..f75c17aaf4 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -16,6 +16,7 @@ limitations under the License.
import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
import * as ModernizrStatic from "modernizr";
+
import ContentMessages from "../ContentMessages";
import { IMatrixClientPeg } from "../MatrixClientPeg";
import ToastStore from "../stores/ToastStore";
@@ -23,25 +24,25 @@ import DeviceListener from "../DeviceListener";
import { RoomListStoreClass } from "../stores/room-list/RoomListStore";
import { PlatformPeg } from "../PlatformPeg";
import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore";
-import {IntegrationManagers} from "../integrations/IntegrationManagers";
-import {ModalManager} from "../Modal";
+import { IntegrationManagers } from "../integrations/IntegrationManagers";
+import { ModalManager } from "../Modal";
import SettingsStore from "../settings/SettingsStore";
-import {ActiveRoomObserver} from "../ActiveRoomObserver";
-import {Notifier} from "../Notifier";
-import type {Renderer} from "react-dom";
+import { ActiveRoomObserver } from "../ActiveRoomObserver";
+import { Notifier } from "../Notifier";
+import type { Renderer } from "react-dom";
import RightPanelStore from "../stores/RightPanelStore";
import WidgetStore from "../stores/WidgetStore";
import CallHandler from "../CallHandler";
-import {Analytics} from "../Analytics";
+import { Analytics } from "../Analytics";
import CountlyAnalytics from "../CountlyAnalytics";
import UserActivity from "../UserActivity";
-import {ModalWidgetStore} from "../stores/ModalWidgetStore";
+import { ModalWidgetStore } from "../stores/ModalWidgetStore";
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import VoipUserMapper from "../VoipUserMapper";
-import {SpaceStoreClass} from "../stores/SpaceStore";
+import { SpaceStoreClass } from "../stores/SpaceStore";
import TypingStore from "../stores/TypingStore";
import { EventIndexPeg } from "../indexing/EventIndexPeg";
-import {VoiceRecordingStore} from "../stores/VoiceRecordingStore";
+import { VoiceRecordingStore } from "../stores/VoiceRecordingStore";
import PerformanceMonitor from "../performance";
import UIStore from "../stores/UIStore";
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
@@ -127,11 +128,24 @@ declare global {
setSinkId(outputId: string);
}
+ // Add Chrome-specific `instant` ScrollBehaviour
+ type _ScrollBehavior = ScrollBehavior | "instant";
+
+ interface _ScrollOptions {
+ behavior?: _ScrollBehavior;
+ }
+
+ interface _ScrollIntoViewOptions extends _ScrollOptions {
+ block?: ScrollLogicalPosition;
+ inline?: ScrollLogicalPosition;
+ }
+
interface Element {
// Safari & IE11 only have this prefixed: we used prefixed versions
// previously so let's continue to support them for now
webkitRequestFullScreen(options?: FullscreenOptions): Promise;
msRequestFullscreen(options?: FullscreenOptions): Promise;
+ scrollIntoView(arg?: boolean | _ScrollIntoViewOptions): void;
}
interface Error {
diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx
index 66f998b616..3b7fee3a08 100644
--- a/src/components/structures/AutoHideScrollbar.tsx
+++ b/src/components/structures/AutoHideScrollbar.tsx
@@ -15,12 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from "react";
+import React, { WheelEvent } from "react";
interface IProps {
className?: string;
- onScroll?: () => void;
- onWheel?: () => void;
+ onScroll?: (event: Event) => void;
+ onWheel?: (event: WheelEvent) => void;
style?: React.CSSProperties
tabIndex?: number,
wrappedRef?: (ref: HTMLDivElement) => void;
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.tsx
similarity index 64%
rename from src/components/structures/MessagePanel.js
rename to src/components/structures/MessagePanel.tsx
index eb9611a6fc..492d9d9a53 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.tsx
@@ -1,7 +1,5 @@
/*
-Copyright 2016 OpenMarket Ltd
-Copyright 2018 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,32 +14,46 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {createRef} from 'react';
+import React, { createRef, KeyboardEvent, ReactNode, SyntheticEvent, TransitionEvent } from 'react';
import ReactDOM from 'react-dom';
-import PropTypes from 'prop-types';
-import shouldHideEvent from '../../shouldHideEvent';
-import {wantsDateSeparator} from '../../DateUtils';
-import * as sdk from '../../index';
+import { Room } from 'matrix-js-sdk/src/models/room';
+import { EventType } from 'matrix-js-sdk/src/@types/event';
+import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
+import { Relations } from "matrix-js-sdk/src/models/relations";
+import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
-import {MatrixClientPeg} from '../../MatrixClientPeg';
+import shouldHideEvent from '../../shouldHideEvent';
+import { wantsDateSeparator } from '../../DateUtils';
+import { MatrixClientPeg } from '../../MatrixClientPeg';
import SettingsStore from '../../settings/SettingsStore';
import RoomContext from "../../contexts/RoomContext";
-import {Layout, LayoutPropType} from "../../settings/Layout";
-import {_t} from "../../languageHandler";
-import {haveTileForEvent} from "../views/rooms/EventTile";
-import {hasText} from "../../TextForEvent";
+import { Layout } from "../../settings/Layout";
+import { _t } from "../../languageHandler";
+import EventTile, { haveTileForEvent, IReadReceiptProps, TileShape } from "../views/rooms/EventTile";
+import { hasText } from "../../TextForEvent";
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
import defaultDispatcher from '../../dispatcher/dispatcher';
+import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
+import ScrollPanel, { IScrollState } from "./ScrollPanel";
+import EventListSummary from '../views/elements/EventListSummary';
+import MemberEventListSummary from '../views/elements/MemberEventListSummary';
+import DateSeparator from '../views/messages/DateSeparator';
+import ErrorBoundary from '../views/elements/ErrorBoundary';
+import ResizeNotifier from "../../utils/ResizeNotifier";
+import Spinner from "../views/elements/Spinner";
+import TileErrorBoundary from '../views/messages/TileErrorBoundary';
+import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
+import EditorStateTransfer from "../../utils/EditorStateTransfer";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
-const continuedTypes = ['m.sticker', 'm.room.message'];
+const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
// check if there is a previous event and it has the same sender as this event
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
-function shouldFormContinuation(prevEvent, mxEvent) {
+function shouldFormContinuation(prevEvent: MatrixEvent, mxEvent: MatrixEvent): boolean {
// sanity check inputs
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
// check if within the max continuation period
@@ -52,8 +64,8 @@ function shouldFormContinuation(prevEvent, mxEvent) {
// Some events should appear as continuations from previous events of different types.
if (mxEvent.getType() !== prevEvent.getType() &&
- (!continuedTypes.includes(mxEvent.getType()) ||
- !continuedTypes.includes(prevEvent.getType()))) return false;
+ (!continuedTypes.includes(mxEvent.getType() as EventType) ||
+ !continuedTypes.includes(prevEvent.getType() as EventType))) return false;
// Check if the sender is the same and hasn't changed their displayname/avatar between these events
if (mxEvent.sender.userId !== prevEvent.sender.userId ||
@@ -66,96 +78,161 @@ function shouldFormContinuation(prevEvent, mxEvent) {
return true;
}
-const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() === 'm.room.third_party_invite';
+const isMembershipChange = (e: MatrixEvent): boolean => {
+ return e.getType() === EventType.RoomMember || e.getType() === EventType.RoomThirdPartyInvite;
+}
+
+interface IProps {
+ // the list of MatrixEvents to display
+ events: MatrixEvent[];
+
+ // true to give the component a 'display: none' style.
+ hidden?: boolean;
+
+ // true to show a spinner at the top of the timeline to indicate
+ // back-pagination in progress
+ backPaginating?: boolean;
+
+ // true to show a spinner at the end of the timeline to indicate
+ // forward-pagination in progress
+ forwardPaginating?: boolean;
+
+ // ID of an event to highlight. If undefined, no event will be highlighted.
+ highlightedEventId?: string;
+
+ // The room these events are all in together, if any.
+ // (The notification panel won't have a room here, for example.)
+ room?: Room;
+
+ // Should we show URL Previews
+ showUrlPreview?: boolean;
+
+ // event after which we should show a read marker
+ readMarkerEventId?: string;
+
+ // whether the read marker should be visible
+ readMarkerVisible?: boolean;
+
+ // the userid of our user. This is used to suppress the read marker
+ // for pending messages.
+ ourUserId?: string;
+
+ // true to suppress the date at the start of the timeline
+ suppressFirstDateSeparator?: boolean;
+
+ // whether to show read receipts
+ showReadReceipts?: boolean;
+
+ // true if updates to the event list should cause the scroll panel to
+ // scroll down when we are at the bottom of the window. See ScrollPanel
+ // for more details.
+ stickyBottom?: boolean;
+
+ // className for the panel
+ className: string;
+
+ // shape parameter to be passed to EventTiles
+ tileShape?: TileShape;
+
+ // show twelve hour timestamps
+ isTwelveHour?: boolean;
+
+ // show timestamps always
+ alwaysShowTimestamps?: boolean;
+
+ // whether to show reactions for an event
+ showReactions?: boolean;
+
+ // which layout to use
+ layout?: Layout;
+
+ // whether or not to show flair at all
+ enableFlair?: boolean;
+
+ resizeNotifier: ResizeNotifier;
+ permalinkCreator?: RoomPermalinkCreator;
+ editState?: EditorStateTransfer;
+
+ // callback which is called when the panel is scrolled.
+ onScroll?(event: Event): void;
+
+ // callback which is called when the user interacts with the room timeline
+ onUserScroll(event: SyntheticEvent): void;
+
+ // callback which is called when more content is needed.
+ onFillRequest?(backwards: boolean): Promise;
+
+ // helper function to access relations for an event
+ onUnfillRequest?(backwards: boolean, scrollToken: string): void;
+
+ getRelationsForEvent?(eventId: string, relationType: string, eventType: string): Relations;
+}
+
+interface IState {
+ ghostReadMarkers: string[];
+ showTypingNotifications: boolean;
+}
+
+interface IReadReceiptForUser {
+ lastShownEventId: string;
+ receipt: IReadReceiptProps;
+}
/* (almost) stateless UI component which builds the event tiles in the room timeline.
*/
@replaceableComponent("structures.MessagePanel")
-export default class MessagePanel extends React.Component {
- static propTypes = {
- // true to give the component a 'display: none' style.
- hidden: PropTypes.bool,
-
- // true to show a spinner at the top of the timeline to indicate
- // back-pagination in progress
- backPaginating: PropTypes.bool,
-
- // true to show a spinner at the end of the timeline to indicate
- // forward-pagination in progress
- forwardPaginating: PropTypes.bool,
-
- // the list of MatrixEvents to display
- events: PropTypes.array.isRequired,
-
- // ID of an event to highlight. If undefined, no event will be highlighted.
- highlightedEventId: PropTypes.string,
-
- // The room these events are all in together, if any.
- // (The notification panel won't have a room here, for example.)
- room: PropTypes.object,
-
- // Should we show URL Previews
- showUrlPreview: PropTypes.bool,
-
- // event after which we should show a read marker
- readMarkerEventId: PropTypes.string,
-
- // whether the read marker should be visible
- readMarkerVisible: PropTypes.bool,
-
- // the userid of our user. This is used to suppress the read marker
- // for pending messages.
- ourUserId: PropTypes.string,
-
- // true to suppress the date at the start of the timeline
- suppressFirstDateSeparator: PropTypes.bool,
-
- // whether to show read receipts
- showReadReceipts: PropTypes.bool,
-
- // true if updates to the event list should cause the scroll panel to
- // scroll down when we are at the bottom of the window. See ScrollPanel
- // for more details.
- stickyBottom: PropTypes.bool,
-
- // callback which is called when the panel is scrolled.
- onScroll: PropTypes.func,
-
- // callback which is called when the user interacts with the room timeline
- onUserScroll: PropTypes.func,
-
- // callback which is called when more content is needed.
- onFillRequest: PropTypes.func,
-
- // className for the panel
- className: PropTypes.string.isRequired,
-
- // shape parameter to be passed to EventTiles
- tileShape: PropTypes.string,
-
- // show twelve hour timestamps
- isTwelveHour: PropTypes.bool,
-
- // show timestamps always
- alwaysShowTimestamps: PropTypes.bool,
-
- // helper function to access relations for an event
- getRelationsForEvent: PropTypes.func,
-
- // whether to show reactions for an event
- showReactions: PropTypes.bool,
-
- // which layout to use
- layout: LayoutPropType,
-
- // whether or not to show flair at all
- enableFlair: PropTypes.bool,
- };
-
+export default class MessagePanel extends React.Component {
static contextType = RoomContext;
- constructor(props) {
- super(props);
+ // opaque readreceipt info for each userId; used by ReadReceiptMarker
+ // to manage its animations
+ private readonly readReceiptMap: Record = {};
+
+ // Track read receipts by event ID. For each _shown_ event ID, we store
+ // the list of read receipts to display:
+ // [
+ // {
+ // userId: string,
+ // member: RoomMember,
+ // ts: number,
+ // },
+ // ]
+ // This is recomputed on each render. It's only stored on the component
+ // for ease of passing the data around since it's computed in one pass
+ // over all events.
+ private readReceiptsByEvent: Record = {};
+
+ // Track read receipts by user ID. For each user ID we've ever shown a
+ // a read receipt for, we store an object:
+ // {
+ // lastShownEventId: string,
+ // receipt: {
+ // userId: string,
+ // member: RoomMember,
+ // ts: number,
+ // },
+ // }
+ // so that we can always keep receipts displayed by reverting back to
+ // the last shown event for that user ID when needed. This may feel like
+ // it duplicates the receipt storage in the room, but at this layer, we
+ // are tracking _shown_ event IDs, which the JS SDK knows nothing about.
+ // This is recomputed on each render, using the data from the previous
+ // render as our fallback for any user IDs we can't match a receipt to a
+ // displayed event in the current render cycle.
+ private readReceiptsByUserId: Record = {};
+
+ private readonly showHiddenEventsInTimeline: boolean;
+ private isMounted = false;
+
+ private readMarkerNode = createRef();
+ private whoIsTyping = createRef();
+ private scrollPanel = createRef();
+
+ private readonly showTypingNotificationsWatcherRef: string;
+ private eventNodes: Record;
+
+ constructor(props, context) {
+ super(props, context);
this.state = {
// previous positions the read marker has been in, so we can
@@ -164,65 +241,21 @@ export default class MessagePanel extends React.Component {
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
};
- // opaque readreceipt info for each userId; used by ReadReceiptMarker
- // to manage its animations
- this._readReceiptMap = {};
-
- // Track read receipts by event ID. For each _shown_ event ID, we store
- // the list of read receipts to display:
- // [
- // {
- // userId: string,
- // member: RoomMember,
- // ts: number,
- // },
- // ]
- // This is recomputed on each render. It's only stored on the component
- // for ease of passing the data around since it's computed in one pass
- // over all events.
- this._readReceiptsByEvent = {};
-
- // Track read receipts by user ID. For each user ID we've ever shown a
- // a read receipt for, we store an object:
- // {
- // lastShownEventId: string,
- // receipt: {
- // userId: string,
- // member: RoomMember,
- // ts: number,
- // },
- // }
- // so that we can always keep receipts displayed by reverting back to
- // the last shown event for that user ID when needed. This may feel like
- // it duplicates the receipt storage in the room, but at this layer, we
- // are tracking _shown_ event IDs, which the JS SDK knows nothing about.
- // This is recomputed on each render, using the data from the previous
- // render as our fallback for any user IDs we can't match a receipt to a
- // displayed event in the current render cycle.
- this._readReceiptsByUserId = {};
-
// Cache hidden events setting on mount since Settings is expensive to
// query, and we check this in a hot code path.
- this._showHiddenEventsInTimeline =
- SettingsStore.getValue("showHiddenEventsInTimeline");
+ this.showHiddenEventsInTimeline = SettingsStore.getValue("showHiddenEventsInTimeline");
- this._isMounted = false;
-
- this._readMarkerNode = createRef();
- this._whoIsTyping = createRef();
- this._scrollPanel = createRef();
-
- this._showTypingNotificationsWatcherRef =
+ this.showTypingNotificationsWatcherRef =
SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange);
}
componentDidMount() {
- this._isMounted = true;
+ this.isMounted = true;
}
componentWillUnmount() {
- this._isMounted = false;
- SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
+ this.isMounted = false;
+ SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef);
}
componentDidUpdate(prevProps, prevState) {
@@ -235,14 +268,14 @@ export default class MessagePanel extends React.Component {
}
}
- onShowTypingNotificationsChange = () => {
+ private onShowTypingNotificationsChange = (): void => {
this.setState({
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
});
};
/* get the DOM node representing the given event */
- getNodeForEventId(eventId) {
+ public getNodeForEventId(eventId: string): HTMLElement {
if (!this.eventNodes) {
return undefined;
}
@@ -252,8 +285,8 @@ export default class MessagePanel extends React.Component {
/* return true if the content is fully scrolled down right now; else false.
*/
- isAtBottom() {
- return this._scrollPanel.current && this._scrollPanel.current.isAtBottom();
+ public isAtBottom(): boolean {
+ return this.scrollPanel.current?.isAtBottom();
}
/* get the current scroll state. See ScrollPanel.getScrollState for
@@ -261,8 +294,8 @@ export default class MessagePanel extends React.Component {
*
* returns null if we are not mounted.
*/
- getScrollState() {
- return this._scrollPanel.current ? this._scrollPanel.current.getScrollState() : null;
+ public getScrollState(): IScrollState {
+ return this.scrollPanel.current?.getScrollState() ?? null;
}
// returns one of:
@@ -271,15 +304,15 @@ export default class MessagePanel extends React.Component {
// -1: read marker is above the window
// 0: read marker is within the window
// +1: read marker is below the window
- getReadMarkerPosition() {
- const readMarker = this._readMarkerNode.current;
- const messageWrapper = this._scrollPanel.current;
+ public getReadMarkerPosition(): number {
+ const readMarker = this.readMarkerNode.current;
+ const messageWrapper = this.scrollPanel.current;
if (!readMarker || !messageWrapper) {
return null;
}
- const wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
+ const wrapperRect = (ReactDOM.findDOMNode(messageWrapper) as HTMLElement).getBoundingClientRect();
const readMarkerRect = readMarker.getBoundingClientRect();
// the read-marker pretends to have zero height when it is actually
@@ -295,17 +328,17 @@ export default class MessagePanel extends React.Component {
/* jump to the top of the content.
*/
- scrollToTop() {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.scrollToTop();
+ public scrollToTop(): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.scrollToTop();
}
}
/* jump to the bottom of the content.
*/
- scrollToBottom() {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.scrollToBottom();
+ public scrollToBottom(): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.scrollToBottom();
}
}
@@ -314,9 +347,9 @@ export default class MessagePanel extends React.Component {
*
* @param {number} mult: -1 to page up, +1 to page down
*/
- scrollRelative(mult) {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.scrollRelative(mult);
+ public scrollRelative(mult: number): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.scrollRelative(mult);
}
}
@@ -325,9 +358,9 @@ export default class MessagePanel extends React.Component {
*
* @param {KeyboardEvent} ev: the keyboard event to handle
*/
- handleScrollKey(ev) {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.handleScrollKey(ev);
+ public handleScrollKey(ev: KeyboardEvent): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.handleScrollKey(ev);
}
}
@@ -341,38 +374,41 @@ export default class MessagePanel extends React.Component {
* node (specifically, the bottom of it) will be positioned. If omitted, it
* defaults to 0.
*/
- scrollToEvent(eventId, pixelOffset, offsetBase) {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase);
+ public scrollToEvent(eventId: string, pixelOffset: number, offsetBase: number): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase);
}
}
- scrollToEventIfNeeded(eventId) {
+ public scrollToEventIfNeeded(eventId: string): void {
const node = this.eventNodes[eventId];
if (node) {
- node.scrollIntoView({block: "nearest", behavior: "instant"});
+ node.scrollIntoView({
+ block: "nearest",
+ behavior: "instant",
+ });
}
}
/* check the scroll state and send out pagination requests if necessary.
*/
- checkFillState() {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.checkFillState();
+ public checkFillState(): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.checkFillState();
}
}
- _isUnmounting = () => {
- return !this._isMounted;
+ private isUnmounting = (): boolean => {
+ return !this.isMounted;
};
// TODO: Implement granular (per-room) hide options
- _shouldShowEvent(mxEv) {
+ public shouldShowEvent(mxEv: MatrixEvent): boolean {
if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) {
return false; // ignored = no show (only happens if the ignore happens after an event was received)
}
- if (this._showHiddenEventsInTimeline) {
+ if (this.showHiddenEventsInTimeline) {
return true;
}
@@ -386,7 +422,7 @@ export default class MessagePanel extends React.Component {
return !shouldHideEvent(mxEv, this.context);
}
- _readMarkerForEvent(eventId, isLastEvent) {
+ public readMarkerForEvent(eventId: string, isLastEvent: boolean): ReactNode {
const visible = !isLastEvent && this.props.readMarkerVisible;
if (this.props.readMarkerEventId === eventId) {
@@ -405,7 +441,7 @@ export default class MessagePanel extends React.Component {
return (
@@ -424,8 +460,8 @@ export default class MessagePanel extends React.Component {
// transition (ie. the read markers do but the event tiles do not)
// and TransitionGroup requires that all its children are Transitions.
const hr = ;
@@ -445,7 +481,7 @@ export default class MessagePanel extends React.Component {
return null;
}
- _collectGhostReadMarker = (node) => {
+ private collectGhostReadMarker = (node: HTMLElement): void => {
if (node) {
// now the element has appeared, change the style which will trigger the CSS transition
requestAnimationFrame(() => {
@@ -455,15 +491,15 @@ export default class MessagePanel extends React.Component {
}
};
- _onGhostTransitionEnd = (ev) => {
+ private onGhostTransitionEnd = (ev: TransitionEvent): void => {
// we can now clean up the ghost element
- const finishedEventId = ev.target.dataset.eventid;
+ const finishedEventId = (ev.target as HTMLElement).dataset.eventid;
this.setState({
ghostReadMarkers: this.state.ghostReadMarkers.filter(eid => eid !== finishedEventId),
});
};
- _getNextEventInfo(arr, i) {
+ private getNextEventInfo(arr: MatrixEvent[], i: number): { nextEvent: MatrixEvent, nextTile: MatrixEvent } {
const nextEvent = i < arr.length - 1
? arr[i + 1]
: null;
@@ -472,16 +508,16 @@ export default class MessagePanel extends React.Component {
// when rendering the tile. The shouldShowEvent function is pretty quick at what
// it does, so this should have no significant cost even when a room is used for
// not-chat purposes.
- const nextTile = arr.slice(i + 1).find(e => this._shouldShowEvent(e));
+ const nextTile = arr.slice(i + 1).find(e => this.shouldShowEvent(e));
- return {nextEvent, nextTile};
+ return { nextEvent, nextTile };
}
- get _roomHasPendingEdit() {
+ private get roomHasPendingEdit(): string {
return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
}
- _getEventTiles() {
+ private getEventTiles(): ReactNode[] {
this.eventNodes = {};
let i;
@@ -497,7 +533,7 @@ export default class MessagePanel extends React.Component {
let lastShownNonLocalEchoIndex = -1;
for (i = this.props.events.length-1; i >= 0; i--) {
const mxEv = this.props.events[i];
- if (!this._shouldShowEvent(mxEv)) {
+ if (!this.shouldShowEvent(mxEv)) {
continue;
}
@@ -521,18 +557,18 @@ export default class MessagePanel extends React.Component {
// Note: the EventTile might still render a "sent/sending receipt" independent of
// this information. When not providing read receipt information, the tile is likely
// to assume that sent receipts are to be shown more often.
- this._readReceiptsByEvent = {};
+ this.readReceiptsByEvent = {};
if (this.props.showReadReceipts) {
- this._readReceiptsByEvent = this._getReadReceiptsByShownEvent();
+ this.readReceiptsByEvent = this.getReadReceiptsByShownEvent();
}
- let grouper = null;
+ let grouper: BaseGrouper = null;
for (i = 0; i < this.props.events.length; i++) {
const mxEv = this.props.events[i];
const eventId = mxEv.getId();
const last = (mxEv === lastShownEvent);
- const {nextEvent, nextTile} = this._getNextEventInfo(this.props.events, i);
+ const { nextEvent, nextTile } = this.getNextEventInfo(this.props.events, i);
if (grouper) {
if (grouper.shouldGroup(mxEv)) {
@@ -553,26 +589,25 @@ export default class MessagePanel extends React.Component {
}
}
if (!grouper) {
- const wantTile = this._shouldShowEvent(mxEv);
+ const wantTile = this.shouldShowEvent(mxEv);
const isGrouped = false;
if (wantTile) {
- // make sure we unpack the array returned by _getTilesForEvent,
+ // make sure we unpack the array returned by getTilesForEvent,
// otherwise react will auto-generate keys and we will end up
// replacing all of the DOM elements every time we paginate.
- ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, isGrouped,
- nextEvent, nextTile));
+ ret.push(...this.getTilesForEvent(prevEvent, mxEv, last, isGrouped, nextEvent, nextTile));
prevEvent = mxEv;
}
- const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
+ const readMarker = this.readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
if (readMarker) ret.push(readMarker);
}
}
- if (!this.props.editState && this._roomHasPendingEdit) {
+ if (!this.props.editState && this.roomHasPendingEdit) {
defaultDispatcher.dispatch({
action: "edit_event",
- event: this.props.room.findEventById(this._roomHasPendingEdit),
+ event: this.props.room.findEventById(this.roomHasPendingEdit),
});
}
@@ -583,10 +618,14 @@ export default class MessagePanel extends React.Component {
return ret;
}
- _getTilesForEvent(prevEvent, mxEv, last, isGrouped=false, nextEvent, nextEventWithTile) {
- const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
- const EventTile = sdk.getComponent('rooms.EventTile');
- const DateSeparator = sdk.getComponent('messages.DateSeparator');
+ public getTilesForEvent(
+ prevEvent: MatrixEvent,
+ mxEv: MatrixEvent,
+ last = false,
+ isGrouped = false,
+ nextEvent?: MatrixEvent,
+ nextEventWithTile?: MatrixEvent,
+ ): ReactNode[] {
const ret = [];
const isEditing = this.props.editState &&
@@ -601,7 +640,7 @@ export default class MessagePanel extends React.Component {
}
// do we need a date separator since the last event?
- const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate);
+ const wantsDateSeparator = this.wantsDateSeparator(prevEvent, eventDate);
if (wantsDateSeparator && !isGrouped) {
const dateSeparator =
;
ret.push(dateSeparator);
@@ -609,7 +648,7 @@ export default class MessagePanel extends React.Component {
let willWantDateSeparator = false;
if (nextEvent) {
- willWantDateSeparator = this._wantsDateSeparator(mxEv, nextEvent.getDate() || new Date());
+ willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEvent.getDate() || new Date());
}
// is this a continuation of the previous message?
@@ -618,12 +657,12 @@ export default class MessagePanel extends React.Component {
const eventId = mxEv.getId();
const highlight = (eventId === this.props.highlightedEventId);
- const readReceipts = this._readReceiptsByEvent[eventId];
+ const readReceipts = this.readReceiptsByEvent[eventId];
let isLastSuccessful = false;
const isSentState = s => !s || s === 'sent';
const isSent = isSentState(mxEv.getAssociatedStatus());
- const hasNextEvent = nextEvent && this._shouldShowEvent(nextEvent);
+ const hasNextEvent = nextEvent && this.shouldShowEvent(nextEvent);
if (!hasNextEvent && isSent) {
isLastSuccessful = true;
} else if (hasNextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) {
@@ -649,18 +688,18 @@ export default class MessagePanel extends React.Component {
{
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
return; // ignore non-read receipts and receipts from self.
@@ -721,13 +760,13 @@ export default class MessagePanel extends React.Component {
// Get an object that maps from event ID to a list of read receipts that
// should be shown next to that event. If a hidden event has read receipts,
// they are folded into the receipts of the last shown event.
- _getReadReceiptsByShownEvent() {
+ private getReadReceiptsByShownEvent(): Record {
const receiptsByEvent = {};
const receiptsByUserId = {};
let lastShownEventId;
for (const event of this.props.events) {
- if (this._shouldShowEvent(event)) {
+ if (this.shouldShowEvent(event)) {
lastShownEventId = event.getId();
}
if (!lastShownEventId) {
@@ -735,7 +774,7 @@ export default class MessagePanel extends React.Component {
}
const existingReceipts = receiptsByEvent[lastShownEventId] || [];
- const newReceipts = this._getReadReceiptsForEvent(event);
+ const newReceipts = this.getReadReceiptsForEvent(event);
receiptsByEvent[lastShownEventId] = existingReceipts.concat(newReceipts);
// Record these receipts along with their last shown event ID for
@@ -754,16 +793,16 @@ export default class MessagePanel extends React.Component {
// someone which had one in the last. By looking through our previous
// mapping of receipts by user ID, we can cover recover any receipts
// that would have been lost by using the same event ID from last time.
- for (const userId in this._readReceiptsByUserId) {
+ for (const userId in this.readReceiptsByUserId) {
if (receiptsByUserId[userId]) {
continue;
}
- const { lastShownEventId, receipt } = this._readReceiptsByUserId[userId];
+ const { lastShownEventId, receipt } = this.readReceiptsByUserId[userId];
const existingReceipts = receiptsByEvent[lastShownEventId] || [];
receiptsByEvent[lastShownEventId] = existingReceipts.concat(receipt);
receiptsByUserId[userId] = { lastShownEventId, receipt };
}
- this._readReceiptsByUserId = receiptsByUserId;
+ this.readReceiptsByUserId = receiptsByUserId;
// After grouping receipts by shown events, do another pass to sort each
// receipt list.
@@ -776,21 +815,21 @@ export default class MessagePanel extends React.Component {
return receiptsByEvent;
}
- _collectEventNode = (eventId, node) => {
+ private collectEventNode = (eventId: string, node: EventTile): void => {
this.eventNodes[eventId] = node?.ref?.current;
}
// once dynamic content in the events load, make the scrollPanel check the
// scroll offsets.
- _onHeightChanged = () => {
- const scrollPanel = this._scrollPanel.current;
+ public onHeightChanged = (): void => {
+ const scrollPanel = this.scrollPanel.current;
if (scrollPanel) {
scrollPanel.checkScroll();
}
};
- _onTypingShown = () => {
- const scrollPanel = this._scrollPanel.current;
+ private onTypingShown = (): void => {
+ const scrollPanel = this.scrollPanel.current;
// this will make the timeline grow, so checkScroll
scrollPanel.checkScroll();
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
@@ -798,8 +837,8 @@ export default class MessagePanel extends React.Component {
}
};
- _onTypingHidden = () => {
- const scrollPanel = this._scrollPanel.current;
+ private onTypingHidden = (): void => {
+ const scrollPanel = this.scrollPanel.current;
if (scrollPanel) {
// as hiding the typing notifications doesn't
// update the scrollPanel, we tell it to apply
@@ -811,12 +850,12 @@ export default class MessagePanel extends React.Component {
}
};
- updateTimelineMinHeight() {
- const scrollPanel = this._scrollPanel.current;
+ public updateTimelineMinHeight(): void {
+ const scrollPanel = this.scrollPanel.current;
if (scrollPanel) {
const isAtBottom = scrollPanel.isAtBottom();
- const whoIsTyping = this._whoIsTyping.current;
+ const whoIsTyping = this.whoIsTyping.current;
const isTypingVisible = whoIsTyping && whoIsTyping.isVisible();
// when messages get added to the timeline,
// but somebody else is still typing,
@@ -828,18 +867,14 @@ export default class MessagePanel extends React.Component {
}
}
- onTimelineReset() {
- const scrollPanel = this._scrollPanel.current;
+ public onTimelineReset(): void {
+ const scrollPanel = this.scrollPanel.current;
if (scrollPanel) {
scrollPanel.clearPreventShrinking();
}
}
render() {
- const ErrorBoundary = sdk.getComponent('elements.ErrorBoundary');
- const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
- const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile");
- const Spinner = sdk.getComponent("elements.Spinner");
let topSpinner;
let bottomSpinner;
if (this.props.backPaginating) {
@@ -855,9 +890,9 @@ export default class MessagePanel extends React.Component {
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
whoIsTyping = (
+ onShown={this.onTypingShown}
+ onHidden={this.onTypingHidden}
+ ref={this.whoIsTyping} />
);
}
@@ -873,11 +908,10 @@ export default class MessagePanel extends React.Component {
return (
{ topSpinner }
- { this._getEventTiles() }
+ { this.getEventTiles() }
{ whoIsTyping }
{ bottomSpinner }
@@ -895,6 +929,31 @@ export default class MessagePanel extends React.Component {
}
}
+abstract class BaseGrouper {
+ static canStartGroup = (panel: MessagePanel, ev: MatrixEvent): boolean => true;
+
+ public events: MatrixEvent[] = [];
+ // events that we include in the group but then eject out and place above the group.
+ public ejectedEvents: MatrixEvent[] = [];
+ public readMarker: ReactNode;
+
+ constructor(
+ public readonly panel: MessagePanel,
+ public readonly event: MatrixEvent,
+ public readonly prevEvent: MatrixEvent,
+ public readonly lastShownEvent: MatrixEvent,
+ public readonly nextEvent?: MatrixEvent,
+ public readonly nextEventTile?: MatrixEvent,
+ ) {
+ this.readMarker = panel.readMarkerForEvent(event.getId(), event === lastShownEvent);
+ }
+
+ public abstract shouldGroup(ev: MatrixEvent): boolean;
+ public abstract add(ev: MatrixEvent): void;
+ public abstract getTiles(): ReactNode[];
+ public abstract getNewPrevEvent(): MatrixEvent;
+}
+
/* Grouper classes determine when events can be grouped together in a summary.
* Groupers should have the following methods:
* - canStartGroup (static): determines if a new group should be started with the
@@ -910,36 +969,21 @@ export default class MessagePanel extends React.Component {
// Wrap initial room creation events into an EventListSummary
// Grouping only events sent by the same user that sent the `m.room.create` and only until
// the first non-state event or membership event which is not regarding the sender of the `m.room.create` event
-class CreationGrouper {
- static canStartGroup = function(panel, ev) {
- return ev.getType() === "m.room.create";
+class CreationGrouper extends BaseGrouper {
+ static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
+ return ev.getType() === EventType.RoomCreate;
};
- constructor(panel, createEvent, prevEvent, lastShownEvent) {
- this.panel = panel;
- this.createEvent = createEvent;
- this.prevEvent = prevEvent;
- this.lastShownEvent = lastShownEvent;
- this.events = [];
- // events that we include in the group but then eject out and place
- // above the group.
- this.ejectedEvents = [];
- this.readMarker = panel._readMarkerForEvent(
- createEvent.getId(),
- createEvent === lastShownEvent,
- );
- }
-
- shouldGroup(ev) {
+ public shouldGroup(ev: MatrixEvent): boolean {
const panel = this.panel;
- const createEvent = this.createEvent;
- if (!panel._shouldShowEvent(ev)) {
+ const createEvent = this.event;
+ if (!panel.shouldShowEvent(ev)) {
return true;
}
- if (panel._wantsDateSeparator(this.createEvent, ev.getDate())) {
+ if (panel.wantsDateSeparator(this.event, ev.getDate())) {
return false;
}
- if (ev.getType() === "m.room.member"
+ if (ev.getType() === EventType.RoomMember
&& (ev.getStateKey() !== createEvent.getSender() || ev.getContent()["membership"] !== "join")) {
return false;
}
@@ -949,37 +993,35 @@ class CreationGrouper {
return false;
}
- add(ev) {
+ public add(ev: MatrixEvent): void {
const panel = this.panel;
- this.readMarker = this.readMarker || panel._readMarkerForEvent(
+ this.readMarker = this.readMarker || panel.readMarkerForEvent(
ev.getId(),
ev === this.lastShownEvent,
);
- if (!panel._shouldShowEvent(ev)) {
+ if (!panel.shouldShowEvent(ev)) {
return;
}
- if (ev.getType() === "m.room.encryption") {
+ if (ev.getType() === EventType.RoomEncryption) {
this.ejectedEvents.push(ev);
} else {
this.events.push(ev);
}
}
- getTiles() {
+ public getTiles(): ReactNode[] {
// If we don't have any events to group, don't even try to group them. The logic
// below assumes that we have a group of events to deal with, but we might not if
// the events we were supposed to group were redacted.
if (!this.events || !this.events.length) return [];
- const DateSeparator = sdk.getComponent('messages.DateSeparator');
- const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
const panel = this.panel;
const ret = [];
const isGrouped = true;
- const createEvent = this.createEvent;
+ const createEvent = this.event;
const lastShownEvent = this.lastShownEvent;
- if (panel._wantsDateSeparator(this.prevEvent, createEvent.getDate())) {
+ if (panel.wantsDateSeparator(this.prevEvent, createEvent.getDate())) {
const ts = createEvent.getTs();
ret.push(
,
@@ -987,13 +1029,13 @@ class CreationGrouper {
}
// If this m.room.create event should be shown (room upgrade) then show it before the summary
- if (panel._shouldShowEvent(createEvent)) {
+ if (panel.shouldShowEvent(createEvent)) {
// pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered
- ret.push(...panel._getTilesForEvent(createEvent, createEvent));
+ ret.push(...panel.getTilesForEvent(createEvent, createEvent));
}
for (const ejected of this.ejectedEvents) {
- ret.push(...panel._getTilesForEvent(
+ ret.push(...panel.getTilesForEvent(
createEvent, ejected, createEvent === lastShownEvent, isGrouped,
));
}
@@ -1003,7 +1045,7 @@ class CreationGrouper {
// of EventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
- return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
+ return panel.getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
}).reduce((a, b) => a.concat(b), []);
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
const ev = this.events[this.events.length - 1];
@@ -1023,7 +1065,7 @@ class CreationGrouper {
@@ -1038,62 +1080,59 @@ class CreationGrouper {
return ret;
}
- getNewPrevEvent() {
- return this.createEvent;
+ public getNewPrevEvent(): MatrixEvent {
+ return this.event;
}
}
-class RedactionGrouper {
- static canStartGroup = function(panel, ev) {
- return panel._shouldShowEvent(ev) && ev.isRedacted();
+class RedactionGrouper extends BaseGrouper {
+ static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
+ return panel.shouldShowEvent(ev) && ev.isRedacted();
}
- constructor(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile) {
- this.panel = panel;
- this.readMarker = panel._readMarkerForEvent(
- ev.getId(),
- ev === lastShownEvent,
- );
+ constructor(
+ panel: MessagePanel,
+ ev: MatrixEvent,
+ prevEvent: MatrixEvent,
+ lastShownEvent: MatrixEvent,
+ nextEvent: MatrixEvent,
+ nextEventTile: MatrixEvent,
+ ) {
+ super(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile);
this.events = [ev];
- this.prevEvent = prevEvent;
- this.lastShownEvent = lastShownEvent;
- this.nextEvent = nextEvent;
- this.nextEventTile = nextEventTile;
}
- shouldGroup(ev) {
+ public shouldGroup(ev: MatrixEvent): boolean {
// absorb hidden events so that they do not break up streams of messages & redaction events being grouped
- if (!this.panel._shouldShowEvent(ev)) {
+ if (!this.panel.shouldShowEvent(ev)) {
return true;
}
- if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) {
+ if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
return false;
}
return ev.isRedacted();
}
- add(ev) {
- this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
+ public add(ev: MatrixEvent): void {
+ this.readMarker = this.readMarker || this.panel.readMarkerForEvent(
ev.getId(),
ev === this.lastShownEvent,
);
- if (!this.panel._shouldShowEvent(ev)) {
+ if (!this.panel.shouldShowEvent(ev)) {
return;
}
this.events.push(ev);
}
- getTiles() {
+ public getTiles(): ReactNode[] {
if (!this.events || !this.events.length) return [];
- const DateSeparator = sdk.getComponent('messages.DateSeparator');
- const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
const isGrouped = true;
const panel = this.panel;
const ret = [];
const lastShownEvent = this.lastShownEvent;
- if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
+ if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
const ts = this.events[0].getTs();
ret.push(
,
@@ -1104,11 +1143,11 @@ class RedactionGrouper {
this.prevEvent ? this.events[0].getId() : "initial"
);
- const senders = new Set();
+ const senders = new Set();
let eventTiles = this.events.map((e, i) => {
senders.add(e.sender);
const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1];
- return panel._getTilesForEvent(
+ return panel.getTilesForEvent(
prevEvent, e, e === lastShownEvent, isGrouped, this.nextEvent, this.nextEventTile);
}).reduce((a, b) => a.concat(b), []);
@@ -1121,7 +1160,7 @@ class RedactionGrouper {
key={key}
threshold={2}
events={this.events}
- onToggle={panel._onHeightChanged} // Update scroll state
+ onToggle={panel.onHeightChanged} // Update scroll state
summaryMembers={Array.from(senders)}
summaryText={_t("%(count)s messages deleted.", { count: eventTiles.length })}
>
@@ -1136,61 +1175,58 @@ class RedactionGrouper {
return ret;
}
- getNewPrevEvent() {
+ public getNewPrevEvent(): MatrixEvent {
return this.events[this.events.length - 1];
}
}
// Wrap consecutive member events in a ListSummary, ignore if redacted
-class MemberGrouper {
- static canStartGroup = function(panel, ev) {
- return panel._shouldShowEvent(ev) && isMembershipChange(ev);
+class MemberGrouper extends BaseGrouper {
+ static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
+ return panel.shouldShowEvent(ev) && isMembershipChange(ev);
}
- constructor(panel, ev, prevEvent, lastShownEvent) {
- this.panel = panel;
- this.readMarker = panel._readMarkerForEvent(
- ev.getId(),
- ev === lastShownEvent,
- );
- this.events = [ev];
- this.prevEvent = prevEvent;
- this.lastShownEvent = lastShownEvent;
+ constructor(
+ public readonly panel: MessagePanel,
+ public readonly event: MatrixEvent,
+ public readonly prevEvent: MatrixEvent,
+ public readonly lastShownEvent: MatrixEvent,
+ ) {
+ super(panel, event, prevEvent, lastShownEvent);
+ this.events = [event];
}
- shouldGroup(ev) {
- if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) {
+ public shouldGroup(ev: MatrixEvent): boolean {
+ if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
return false;
}
return isMembershipChange(ev);
}
- add(ev) {
- if (ev.getType() === 'm.room.member') {
+ public add(ev: MatrixEvent): void {
+ if (ev.getType() === EventType.RoomMember) {
// We can ignore any events that don't actually have a message to display
if (!hasText(ev)) return;
}
- this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
+ this.readMarker = this.readMarker || this.panel.readMarkerForEvent(
ev.getId(),
ev === this.lastShownEvent,
);
this.events.push(ev);
}
- getTiles() {
+ public getTiles(): ReactNode[] {
// If we don't have any events to group, don't even try to group them. The logic
// below assumes that we have a group of events to deal with, but we might not if
// the events we were supposed to group were redacted.
if (!this.events || !this.events.length) return [];
- const DateSeparator = sdk.getComponent('messages.DateSeparator');
- const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
const isGrouped = true;
const panel = this.panel;
const lastShownEvent = this.lastShownEvent;
const ret = [];
- if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
+ if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
const ts = this.events[0].getTs();
ret.push(
,
@@ -1218,7 +1254,7 @@ class MemberGrouper {
// of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
- return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
+ return panel.getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
}).reduce((a, b) => a.concat(b), []);
if (eventTiles.length === 0) {
@@ -1226,9 +1262,10 @@ class MemberGrouper {
}
ret.push(
-
{ eventTiles }
@@ -1242,7 +1279,7 @@ class MemberGrouper {
return ret;
}
- getNewPrevEvent() {
+ public getNewPrevEvent(): MatrixEvent {
return this.events[0];
}
}
diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx
index 6c22835447..8c8fab7ece 100644
--- a/src/components/structures/NotificationPanel.tsx
+++ b/src/components/structures/NotificationPanel.tsx
@@ -22,6 +22,7 @@ import BaseCard from "../views/right_panel/BaseCard";
import { replaceableComponent } from "../../utils/replaceableComponent";
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
+import { TileShape } from "../views/rooms/EventTile";
interface IProps {
onClose(): void;
@@ -48,7 +49,7 @@ export default class NotificationPanel extends React.PureComponent {
manageReadMarkers={false}
timelineSet={timelineSet}
showUrlPreview={false}
- tileShape="notif"
+ tileShape={TileShape.Notif}
empty={emptyState}
alwaysShowTimestamps={true}
/>
diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx
index 1e0605f263..7770b32f04 100644
--- a/src/components/structures/RoomDirectory.tsx
+++ b/src/components/structures/RoomDirectory.tsx
@@ -207,9 +207,9 @@ export default class RoomDirectory extends React.Component {
this.getMoreRooms();
};
- private getMoreRooms() {
- if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms
- if (!MatrixClientPeg.get()) return Promise.resolve();
+ private getMoreRooms(): Promise {
+ if (this.state.selectedCommunityId) return Promise.resolve(false); // no more rooms
+ if (!MatrixClientPeg.get()) return Promise.resolve(false);
this.setState({
loading: true,
@@ -239,12 +239,12 @@ export default class RoomDirectory extends React.Component {
// if the filter or server has changed since this request was sent,
// throw away the result (don't even clear the busy flag
// since we must still have a request in flight)
- return;
+ return false;
}
if (this.unmounted) {
// if we've been unmounted, we don't care either.
- return;
+ return false;
}
if (this.state.filterString) {
@@ -264,14 +264,13 @@ export default class RoomDirectory extends React.Component {
filterString != this.state.filterString ||
roomServer != this.state.roomServer ||
nextBatch != this.nextBatch) {
- // as above: we don't care about errors for old
- // requests either
- return;
+ // as above: we don't care about errors for old requests either
+ return false;
}
if (this.unmounted) {
// if we've been unmounted, we don't care either.
- return;
+ return false;
}
console.error("Failed to get publicRooms: %s", JSON.stringify(err));
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 885851e8e6..338da29875 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -1125,7 +1125,7 @@ export default class RoomView extends React.Component {
}
}
- private onSearchResultsFillRequest = (backwards: boolean) => {
+ private onSearchResultsFillRequest = (backwards: boolean): Promise => {
if (!backwards) {
return Promise.resolve(false);
}
@@ -1291,7 +1291,7 @@ export default class RoomView extends React.Component {
this.handleSearchResult(searchPromise);
};
- private handleSearchResult(searchPromise: Promise) {
+ private handleSearchResult(searchPromise: Promise): Promise {
// keep a record of the current search id, so that if the search terms
// change before we get a response, we can ignore the results.
const localSearchId = this.searchId;
@@ -1304,7 +1304,7 @@ export default class RoomView extends React.Component {
debuglog("search complete");
if (this.unmounted || !this.state.searching || this.searchId != localSearchId) {
console.error("Discarding stale search results");
- return;
+ return false;
}
// postgres on synapse returns us precise details of the strings
@@ -1336,6 +1336,7 @@ export default class RoomView extends React.Component {
description: ((error && error.message) ? error.message :
_t("Server may be unavailable, overloaded, or search timed out :(")),
});
+ return false;
}).finally(() => {
this.setState({
searchInProgress: false,
diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.tsx
similarity index 73%
rename from src/components/structures/ScrollPanel.js
rename to src/components/structures/ScrollPanel.tsx
index f6e1530537..b8e0cdbc34 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,17 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {createRef} from "react";
-import PropTypes from 'prop-types';
+import React, { createRef, CSSProperties, ReactNode, SyntheticEvent, KeyboardEvent } from "react";
+
import Timer from '../../utils/Timer';
import AutoHideScrollbar from "./AutoHideScrollbar";
-import {replaceableComponent} from "../../utils/replaceableComponent";
-import {getKeyBindingsManager, RoomAction} from "../../KeyBindingsManager";
+import { replaceableComponent } from "../../utils/replaceableComponent";
+import { getKeyBindingsManager, RoomAction } from "../../KeyBindingsManager";
+import ResizeNotifier from "../../utils/ResizeNotifier";
const DEBUG_SCROLL = false;
// The amount of extra scroll distance to allow prior to unfilling.
-// See _getExcessHeight.
+// See getExcessHeight.
const UNPAGINATION_PADDING = 6000;
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
// many scroll events causing many unfilling requests.
@@ -43,6 +44,75 @@ if (DEBUG_SCROLL) {
debuglog = function() {};
}
+interface IProps {
+ /* stickyBottom: if set to true, then once the user hits the bottom of
+ * the list, any new children added to the list will cause the list to
+ * scroll down to show the new element, rather than preserving the
+ * existing view.
+ */
+ stickyBottom?: boolean;
+
+ /* startAtBottom: if set to true, the view is assumed to start
+ * scrolled to the bottom.
+ * XXX: It's likely this is unnecessary and can be derived from
+ * stickyBottom, but I'm adding an extra parameter to ensure
+ * behaviour stays the same for other uses of ScrollPanel.
+ * If so, let's remove this parameter down the line.
+ */
+ startAtBottom?: boolean;
+
+ /* className: classnames to add to the top-level div
+ */
+ className?: string;
+
+ /* style: styles to add to the top-level div
+ */
+ style?: CSSProperties;
+
+ /* resizeNotifier: ResizeNotifier to know when middle column has changed size
+ */
+ resizeNotifier?: ResizeNotifier;
+
+ /* fixedChildren: allows for children to be passed which are rendered outside
+ * of the wrapper
+ */
+ fixedChildren?: ReactNode;
+
+ /* onFillRequest(backwards): a callback which is called on scroll when
+ * the user nears the start (backwards = true) or end (backwards =
+ * false) of the list.
+ *
+ * This should return a promise; no more calls will be made until the
+ * promise completes.
+ *
+ * The promise should resolve to true if there is more data to be
+ * retrieved in this direction (in which case onFillRequest may be
+ * called again immediately), or false if there is no more data in this
+ * directon (at this time) - which will stop the pagination cycle until
+ * the user scrolls again.
+ */
+ onFillRequest?(backwards: boolean): Promise;
+
+ /* onUnfillRequest(backwards): a callback which is called on scroll when
+ * there are children elements that are far out of view and could be removed
+ * without causing pagination to occur.
+ *
+ * This function should accept a boolean, which is true to indicate the back/top
+ * of the panel and false otherwise, and a scroll token, which refers to the
+ * first element to remove if removing from the front/bottom, and last element
+ * to remove if removing from the back/top.
+ */
+ onUnfillRequest?(backwards: boolean, scrollToken: string): void;
+
+ /* onScroll: a callback which is called whenever any scroll happens.
+ */
+ onScroll?(event: Event): void;
+
+ /* onUserScroll: callback which is called when the user interacts with the room timeline
+ */
+ onUserScroll?(event: SyntheticEvent): void;
+}
+
/* This component implements an intelligent scrolling list.
*
* It wraps a list of
children; when items are added to the start or end
@@ -84,97 +154,54 @@ if (DEBUG_SCROLL) {
* offset as normal.
*/
+export interface IScrollState {
+ stuckAtBottom: boolean;
+ trackedNode?: HTMLElement;
+ trackedScrollToken?: string;
+ bottomOffset?: number;
+ pixelOffset?: number;
+}
+
+interface IPreventShrinkingState {
+ offsetFromBottom: number;
+ offsetNode: HTMLElement;
+}
+
@replaceableComponent("structures.ScrollPanel")
-export default class ScrollPanel extends React.Component {
- static propTypes = {
- /* stickyBottom: if set to true, then once the user hits the bottom of
- * the list, any new children added to the list will cause the list to
- * scroll down to show the new element, rather than preserving the
- * existing view.
- */
- stickyBottom: PropTypes.bool,
-
- /* startAtBottom: if set to true, the view is assumed to start
- * scrolled to the bottom.
- * XXX: It's likely this is unnecessary and can be derived from
- * stickyBottom, but I'm adding an extra parameter to ensure
- * behaviour stays the same for other uses of ScrollPanel.
- * If so, let's remove this parameter down the line.
- */
- startAtBottom: PropTypes.bool,
-
- /* onFillRequest(backwards): a callback which is called on scroll when
- * the user nears the start (backwards = true) or end (backwards =
- * false) of the list.
- *
- * This should return a promise; no more calls will be made until the
- * promise completes.
- *
- * The promise should resolve to true if there is more data to be
- * retrieved in this direction (in which case onFillRequest may be
- * called again immediately), or false if there is no more data in this
- * directon (at this time) - which will stop the pagination cycle until
- * the user scrolls again.
- */
- onFillRequest: PropTypes.func,
-
- /* onUnfillRequest(backwards): a callback which is called on scroll when
- * there are children elements that are far out of view and could be removed
- * without causing pagination to occur.
- *
- * This function should accept a boolean, which is true to indicate the back/top
- * of the panel and false otherwise, and a scroll token, which refers to the
- * first element to remove if removing from the front/bottom, and last element
- * to remove if removing from the back/top.
- */
- onUnfillRequest: PropTypes.func,
-
- /* onScroll: a callback which is called whenever any scroll happens.
- */
- onScroll: PropTypes.func,
-
- /* onUserScroll: callback which is called when the user interacts with the room timeline
- */
- onUserScroll: PropTypes.func,
-
- /* className: classnames to add to the top-level div
- */
- className: PropTypes.string,
-
- /* style: styles to add to the top-level div
- */
- style: PropTypes.object,
-
- /* resizeNotifier: ResizeNotifier to know when middle column has changed size
- */
- resizeNotifier: PropTypes.object,
-
- /* fixedChildren: allows for children to be passed which are rendered outside
- * of the wrapper
- */
- fixedChildren: PropTypes.node,
- };
-
+export default class ScrollPanel extends React.Component {
static defaultProps = {
stickyBottom: true,
startAtBottom: true,
- onFillRequest: function(backwards) { return Promise.resolve(false); },
- onUnfillRequest: function(backwards, scrollToken) {},
+ onFillRequest: function(backwards: boolean) { return Promise.resolve(false); },
+ onUnfillRequest: function(backwards: boolean, scrollToken: string) {},
onScroll: function() {},
};
- constructor(props) {
- super(props);
+ private readonly pendingFillRequests: Record<"b" | "f", boolean> = {
+ b: null,
+ f: null,
+ };
+ private readonly itemlist = createRef();
+ private unmounted = false;
+ private scrollTimeout: Timer;
+ private isFilling: boolean;
+ private fillRequestWhileRunning: boolean;
+ private scrollState: IScrollState;
+ private preventShrinkingState: IPreventShrinkingState;
+ private unfillDebouncer: NodeJS.Timeout;
+ private bottomGrowth: number;
+ private pages: number;
+ private heightUpdateInProgress: boolean;
+ private divScroll: HTMLDivElement;
- this._pendingFillRequests = {b: null, f: null};
+ constructor(props, context) {
+ super(props, context);
if (this.props.resizeNotifier) {
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
}
this.resetScrollState();
-
- this._itemlist = createRef();
}
componentDidMount() {
@@ -203,18 +230,18 @@ export default class ScrollPanel extends React.Component {
}
}
- onScroll = ev => {
+ private onScroll = ev => {
// skip scroll events caused by resizing
if (this.props.resizeNotifier && this.props.resizeNotifier.isResizing) return;
- debuglog("onScroll", this._getScrollNode().scrollTop);
- this._scrollTimeout.restart();
- this._saveScrollState();
+ debuglog("onScroll", this.getScrollNode().scrollTop);
+ this.scrollTimeout.restart();
+ this.saveScrollState();
this.updatePreventShrinking();
this.props.onScroll(ev);
this.checkFillState();
};
- onResize = () => {
+ private onResize = () => {
debuglog("onResize");
this.checkScroll();
// update preventShrinkingState if present
@@ -225,11 +252,11 @@ export default class ScrollPanel extends React.Component {
// after an update to the contents of the panel, check that the scroll is
// where it ought to be, and set off pagination requests if necessary.
- checkScroll = () => {
+ public checkScroll = () => {
if (this.unmounted) {
return;
}
- this._restoreSavedScrollState();
+ this.restoreSavedScrollState();
this.checkFillState();
};
@@ -238,8 +265,8 @@ export default class ScrollPanel extends React.Component {
// note that this is independent of the 'stuckAtBottom' state - it is simply
// about whether the content is scrolled down right now, irrespective of
// whether it will stay that way when the children update.
- isAtBottom = () => {
- const sn = this._getScrollNode();
+ public isAtBottom = () => {
+ const sn = this.getScrollNode();
// fractional values (both too big and too small)
// for scrollTop happen on certain browsers/platforms
// when scrolled all the way down. E.g. Chrome 72 on debian.
@@ -278,10 +305,10 @@ export default class ScrollPanel extends React.Component {
// |#########| - |
// |#########| |
// `---------' -
- _getExcessHeight(backwards) {
- const sn = this._getScrollNode();
- const contentHeight = this._getMessagesHeight();
- const listHeight = this._getListHeight();
+ private getExcessHeight(backwards: boolean): number {
+ const sn = this.getScrollNode();
+ const contentHeight = this.getMessagesHeight();
+ const listHeight = this.getListHeight();
const clippedHeight = contentHeight - listHeight;
const unclippedScrollTop = sn.scrollTop + clippedHeight;
@@ -293,13 +320,13 @@ export default class ScrollPanel extends React.Component {
}
// check the scroll state and send out backfill requests if necessary.
- checkFillState = async (depth=0) => {
+ public checkFillState = async (depth = 0): Promise => {
if (this.unmounted) {
return;
}
const isFirstCall = depth === 0;
- const sn = this._getScrollNode();
+ const sn = this.getScrollNode();
// if there is less than a screenful of messages above or below the
// viewport, try to get some more messages.
@@ -330,17 +357,17 @@ export default class ScrollPanel extends React.Component {
// do make a note when a new request comes in while already running one,
// so we can trigger a new chain of calls once done.
if (isFirstCall) {
- if (this._isFilling) {
- debuglog("_isFilling: not entering while request is ongoing, marking for a subsequent request");
- this._fillRequestWhileRunning = true;
+ if (this.isFilling) {
+ debuglog("isFilling: not entering while request is ongoing, marking for a subsequent request");
+ this.fillRequestWhileRunning = true;
return;
}
- debuglog("_isFilling: setting");
- this._isFilling = true;
+ debuglog("isFilling: setting");
+ this.isFilling = true;
}
- const itemlist = this._itemlist.current;
- const firstTile = itemlist && itemlist.firstElementChild;
+ const itemlist = this.itemlist.current;
+ const firstTile = itemlist && itemlist.firstElementChild as HTMLElement;
const contentTop = firstTile && firstTile.offsetTop;
const fillPromises = [];
@@ -348,13 +375,13 @@ export default class ScrollPanel extends React.Component {
// try backward filling
if (!firstTile || (sn.scrollTop - contentTop) < sn.clientHeight) {
// need to back-fill
- fillPromises.push(this._maybeFill(depth, true));
+ fillPromises.push(this.maybeFill(depth, true));
}
// if scrollTop gets to 2 screens from the end (so 1 screen below viewport),
// try forward filling
if ((sn.scrollHeight - sn.scrollTop) < sn.clientHeight * 2) {
// need to forward-fill
- fillPromises.push(this._maybeFill(depth, false));
+ fillPromises.push(this.maybeFill(depth, false));
}
if (fillPromises.length) {
@@ -365,26 +392,26 @@ export default class ScrollPanel extends React.Component {
}
}
if (isFirstCall) {
- debuglog("_isFilling: clearing");
- this._isFilling = false;
+ debuglog("isFilling: clearing");
+ this.isFilling = false;
}
- if (this._fillRequestWhileRunning) {
- this._fillRequestWhileRunning = false;
+ if (this.fillRequestWhileRunning) {
+ this.fillRequestWhileRunning = false;
this.checkFillState();
}
};
// check if unfilling is possible and send an unfill request if necessary
- _checkUnfillState(backwards) {
- let excessHeight = this._getExcessHeight(backwards);
+ private checkUnfillState(backwards: boolean): void {
+ let excessHeight = this.getExcessHeight(backwards);
if (excessHeight <= 0) {
return;
}
const origExcessHeight = excessHeight;
- const tiles = this._itemlist.current.children;
+ const tiles = this.itemlist.current.children;
// The scroll token of the first/last tile to be unpaginated
let markerScrollToken = null;
@@ -413,11 +440,11 @@ export default class ScrollPanel extends React.Component {
if (markerScrollToken) {
// Use a debouncer to prevent multiple unfill calls in quick succession
// This is to make the unfilling process less aggressive
- if (this._unfillDebouncer) {
- clearTimeout(this._unfillDebouncer);
+ if (this.unfillDebouncer) {
+ clearTimeout(this.unfillDebouncer);
}
- this._unfillDebouncer = setTimeout(() => {
- this._unfillDebouncer = null;
+ this.unfillDebouncer = setTimeout(() => {
+ this.unfillDebouncer = null;
debuglog("unfilling now", backwards, origExcessHeight);
this.props.onUnfillRequest(backwards, markerScrollToken);
}, UNFILL_REQUEST_DEBOUNCE_MS);
@@ -425,9 +452,9 @@ export default class ScrollPanel extends React.Component {
}
// check if there is already a pending fill request. If not, set one off.
- _maybeFill(depth, backwards) {
+ private maybeFill(depth: number, backwards: boolean): Promise {
const dir = backwards ? 'b' : 'f';
- if (this._pendingFillRequests[dir]) {
+ if (this.pendingFillRequests[dir]) {
debuglog("Already a "+dir+" fill in progress - not starting another");
return;
}
@@ -436,7 +463,7 @@ export default class ScrollPanel extends React.Component {
// onFillRequest can end up calling us recursively (via onScroll
// events) so make sure we set this before firing off the call.
- this._pendingFillRequests[dir] = true;
+ this.pendingFillRequests[dir] = true;
// wait 1ms before paginating, because otherwise
// this will block the scroll event handler for +700ms
@@ -445,13 +472,13 @@ export default class ScrollPanel extends React.Component {
return new Promise(resolve => setTimeout(resolve, 1)).then(() => {
return this.props.onFillRequest(backwards);
}).finally(() => {
- this._pendingFillRequests[dir] = false;
+ this.pendingFillRequests[dir] = false;
}).then((hasMoreResults) => {
if (this.unmounted) {
return;
}
// Unpaginate once filling is complete
- this._checkUnfillState(!backwards);
+ this.checkUnfillState(!backwards);
debuglog(""+dir+" fill complete; hasMoreResults:"+hasMoreResults);
if (hasMoreResults) {
@@ -477,7 +504,7 @@ export default class ScrollPanel extends React.Component {
* the number of pixels the bottom of the tracked child is above the
* bottom of the scroll panel.
*/
- getScrollState = () => this.scrollState;
+ public getScrollState = (): IScrollState => this.scrollState;
/* reset the saved scroll state.
*
@@ -491,35 +518,35 @@ export default class ScrollPanel extends React.Component {
* no use if no children exist yet, or if you are about to replace the
* child list.)
*/
- resetScrollState = () => {
+ public resetScrollState = (): void => {
this.scrollState = {
stuckAtBottom: this.props.startAtBottom,
};
- this._bottomGrowth = 0;
- this._pages = 0;
- this._scrollTimeout = new Timer(100);
- this._heightUpdateInProgress = false;
+ this.bottomGrowth = 0;
+ this.pages = 0;
+ this.scrollTimeout = new Timer(100);
+ this.heightUpdateInProgress = false;
};
/**
* jump to the top of the content.
*/
- scrollToTop = () => {
- this._getScrollNode().scrollTop = 0;
- this._saveScrollState();
+ public scrollToTop = (): void => {
+ this.getScrollNode().scrollTop = 0;
+ this.saveScrollState();
};
/**
* jump to the bottom of the content.
*/
- scrollToBottom = () => {
+ public scrollToBottom = (): void => {
// the easiest way to make sure that the scroll state is correctly
// saved is to do the scroll, then save the updated state. (Calculating
// it ourselves is hard, and we can't rely on an onScroll callback
// happening, since there may be no user-visible change here).
- const sn = this._getScrollNode();
+ const sn = this.getScrollNode();
sn.scrollTop = sn.scrollHeight;
- this._saveScrollState();
+ this.saveScrollState();
};
/**
@@ -527,18 +554,18 @@ export default class ScrollPanel extends React.Component {
*
* @param {number} mult: -1 to page up, +1 to page down
*/
- scrollRelative = mult => {
- const scrollNode = this._getScrollNode();
+ public scrollRelative = (mult: number): void => {
+ const scrollNode = this.getScrollNode();
const delta = mult * scrollNode.clientHeight * 0.9;
scrollNode.scrollBy(0, delta);
- this._saveScrollState();
+ this.saveScrollState();
};
/**
* Scroll up/down in response to a scroll key
* @param {object} ev the keyboard event
*/
- handleScrollKey = ev => {
+ public handleScrollKey = (ev: KeyboardEvent) => {
let isScrolling = false;
const roomAction = getKeyBindingsManager().getRoomAction(ev);
switch (roomAction) {
@@ -575,17 +602,17 @@ export default class ScrollPanel extends React.Component {
* node (specifically, the bottom of it) will be positioned. If omitted, it
* defaults to 0.
*/
- scrollToToken = (scrollToken, pixelOffset, offsetBase) => {
+ public scrollToToken = (scrollToken: string, pixelOffset: number, offsetBase: number): void => {
pixelOffset = pixelOffset || 0;
offsetBase = offsetBase || 0;
- // set the trackedScrollToken so we can get the node through _getTrackedNode
+ // set the trackedScrollToken so we can get the node through getTrackedNode
this.scrollState = {
stuckAtBottom: false,
trackedScrollToken: scrollToken,
};
- const trackedNode = this._getTrackedNode();
- const scrollNode = this._getScrollNode();
+ const trackedNode = this.getTrackedNode();
+ const scrollNode = this.getScrollNode();
if (trackedNode) {
// set the scrollTop to the position we want.
// note though, that this might not succeed if the combination of offsetBase and pixelOffset
@@ -595,34 +622,34 @@ export default class ScrollPanel extends React.Component {
// enough so it ends up in the top of the viewport.
debuglog("scrollToken: setting scrollTop", {offsetBase, pixelOffset, offsetTop: trackedNode.offsetTop});
scrollNode.scrollTop = (trackedNode.offsetTop - (scrollNode.clientHeight * offsetBase)) + pixelOffset;
- this._saveScrollState();
+ this.saveScrollState();
}
};
- _saveScrollState() {
+ private saveScrollState(): void {
if (this.props.stickyBottom && this.isAtBottom()) {
this.scrollState = { stuckAtBottom: true };
debuglog("saved stuckAtBottom state");
return;
}
- const scrollNode = this._getScrollNode();
+ const scrollNode = this.getScrollNode();
const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight);
- const itemlist = this._itemlist.current;
+ const itemlist = this.itemlist.current;
const messages = itemlist.children;
let node = null;
// TODO: do a binary search here, as items are sorted by offsetTop
// loop backwards, from bottom-most message (as that is the most common case)
- for (let i = messages.length-1; i >= 0; --i) {
- if (!messages[i].dataset.scrollTokens) {
+ for (let i = messages.length - 1; i >= 0; --i) {
+ if (!(messages[i] as HTMLElement).dataset.scrollTokens) {
continue;
}
node = messages[i];
// break at the first message (coming from the bottom)
// that has it's offsetTop above the bottom of the viewport.
- if (this._topFromBottom(node) > viewportBottom) {
+ if (this.topFromBottom(node) > viewportBottom) {
// Use this node as the scrollToken
break;
}
@@ -634,7 +661,7 @@ export default class ScrollPanel extends React.Component {
}
const scrollToken = node.dataset.scrollTokens.split(',')[0];
debuglog("saving anchored scroll state to message", node && node.innerText, scrollToken);
- const bottomOffset = this._topFromBottom(node);
+ const bottomOffset = this.topFromBottom(node);
this.scrollState = {
stuckAtBottom: false,
trackedNode: node,
@@ -644,35 +671,35 @@ export default class ScrollPanel extends React.Component {
};
}
- async _restoreSavedScrollState() {
+ private async restoreSavedScrollState(): Promise {
const scrollState = this.scrollState;
if (scrollState.stuckAtBottom) {
- const sn = this._getScrollNode();
+ const sn = this.getScrollNode();
if (sn.scrollTop !== sn.scrollHeight) {
sn.scrollTop = sn.scrollHeight;
}
} else if (scrollState.trackedScrollToken) {
- const itemlist = this._itemlist.current;
- const trackedNode = this._getTrackedNode();
+ const itemlist = this.itemlist.current;
+ const trackedNode = this.getTrackedNode();
if (trackedNode) {
- const newBottomOffset = this._topFromBottom(trackedNode);
+ const newBottomOffset = this.topFromBottom(trackedNode);
const bottomDiff = newBottomOffset - scrollState.bottomOffset;
- this._bottomGrowth += bottomDiff;
+ this.bottomGrowth += bottomDiff;
scrollState.bottomOffset = newBottomOffset;
- const newHeight = `${this._getListHeight()}px`;
+ const newHeight = `${this.getListHeight()}px`;
if (itemlist.style.height !== newHeight) {
itemlist.style.height = newHeight;
}
debuglog("balancing height because messages below viewport grew by", bottomDiff);
}
}
- if (!this._heightUpdateInProgress) {
- this._heightUpdateInProgress = true;
+ if (!this.heightUpdateInProgress) {
+ this.heightUpdateInProgress = true;
try {
- await this._updateHeight();
+ await this.updateHeight();
} finally {
- this._heightUpdateInProgress = false;
+ this.heightUpdateInProgress = false;
}
} else {
debuglog("not updating height because request already in progress");
@@ -680,11 +707,11 @@ export default class ScrollPanel extends React.Component {
}
// need a better name that also indicates this will change scrollTop? Rebalance height? Reveal content?
- async _updateHeight() {
+ private async updateHeight(): Promise {
// wait until user has stopped scrolling
- if (this._scrollTimeout.isRunning()) {
+ if (this.scrollTimeout.isRunning()) {
debuglog("updateHeight waiting for scrolling to end ... ");
- await this._scrollTimeout.finished();
+ await this.scrollTimeout.finished();
} else {
debuglog("updateHeight getting straight to business, no scrolling going on.");
}
@@ -694,14 +721,14 @@ export default class ScrollPanel extends React.Component {
return;
}
- const sn = this._getScrollNode();
- const itemlist = this._itemlist.current;
- const contentHeight = this._getMessagesHeight();
+ const sn = this.getScrollNode();
+ const itemlist = this.itemlist.current;
+ const contentHeight = this.getMessagesHeight();
const minHeight = sn.clientHeight;
const height = Math.max(minHeight, contentHeight);
- this._pages = Math.ceil(height / PAGE_SIZE);
- this._bottomGrowth = 0;
- const newHeight = `${this._getListHeight()}px`;
+ this.pages = Math.ceil(height / PAGE_SIZE);
+ this.bottomGrowth = 0;
+ const newHeight = `${this.getListHeight()}px`;
const scrollState = this.scrollState;
if (scrollState.stuckAtBottom) {
@@ -713,7 +740,7 @@ export default class ScrollPanel extends React.Component {
}
debuglog("updateHeight to", newHeight);
} else if (scrollState.trackedScrollToken) {
- const trackedNode = this._getTrackedNode();
+ const trackedNode = this.getTrackedNode();
// if the timeline has been reloaded
// this can be called before scrollToBottom or whatever has been called
// so don't do anything if the node has disappeared from
@@ -735,17 +762,17 @@ export default class ScrollPanel extends React.Component {
}
}
- _getTrackedNode() {
+ private getTrackedNode(): HTMLElement {
const scrollState = this.scrollState;
const trackedNode = scrollState.trackedNode;
if (!trackedNode || !trackedNode.parentElement) {
let node;
- const messages = this._itemlist.current.children;
+ const messages = this.itemlist.current.children;
const scrollToken = scrollState.trackedScrollToken;
for (let i = messages.length-1; i >= 0; --i) {
- const m = messages[i];
+ const m = messages[i] as HTMLElement;
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
// There might only be one scroll token
if (m.dataset.scrollTokens &&
@@ -768,45 +795,45 @@ export default class ScrollPanel extends React.Component {
return scrollState.trackedNode;
}
- _getListHeight() {
- return this._bottomGrowth + (this._pages * PAGE_SIZE);
+ private getListHeight(): number {
+ return this.bottomGrowth + (this.pages * PAGE_SIZE);
}
- _getMessagesHeight() {
- const itemlist = this._itemlist.current;
- const lastNode = itemlist.lastElementChild;
+ private getMessagesHeight(): number {
+ const itemlist = this.itemlist.current;
+ const lastNode = itemlist.lastElementChild as HTMLElement;
const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0;
- const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0;
+ const firstNodeTop = itemlist.firstElementChild ? (itemlist.firstElementChild as HTMLElement).offsetTop : 0;
// 18 is itemlist padding
return lastNodeBottom - firstNodeTop + (18 * 2);
}
- _topFromBottom(node) {
+ private topFromBottom(node: HTMLElement): number {
// current capped height - distance from top = distance from bottom of container to top of tracked element
- return this._itemlist.current.clientHeight - node.offsetTop;
+ return this.itemlist.current.clientHeight - node.offsetTop;
}
/* get the DOM node which has the scrollTop property we care about for our
* message panel.
*/
- _getScrollNode() {
+ private getScrollNode(): HTMLDivElement {
if (this.unmounted) {
// this shouldn't happen, but when it does, turn the NPE into
// something more meaningful.
- throw new Error("ScrollPanel._getScrollNode called when unmounted");
+ throw new Error("ScrollPanel.getScrollNode called when unmounted");
}
- if (!this._divScroll) {
+ if (!this.divScroll) {
// Likewise, we should have the ref by this point, but if not
// turn the NPE into something meaningful.
- throw new Error("ScrollPanel._getScrollNode called before AutoHideScrollbar ref collected");
+ throw new Error("ScrollPanel.getScrollNode called before AutoHideScrollbar ref collected");
}
- return this._divScroll;
+ return this.divScroll;
}
- _collectScroll = divScroll => {
- this._divScroll = divScroll;
+ private collectScroll = (divScroll: HTMLDivElement) => {
+ this.divScroll = divScroll;
};
/**
@@ -814,15 +841,15 @@ export default class ScrollPanel extends React.Component {
anything below it changes, by calling updatePreventShrinking, to keep
the same minimum bottom offset, effectively preventing the timeline to shrink.
*/
- preventShrinking = () => {
- const messageList = this._itemlist.current;
+ public preventShrinking = (): void => {
+ const messageList = this.itemlist.current;
const tiles = messageList && messageList.children;
if (!messageList) {
return;
}
let lastTileNode;
for (let i = tiles.length - 1; i >= 0; i--) {
- const node = tiles[i];
+ const node = tiles[i] as HTMLElement;
if (node.dataset.scrollTokens) {
lastTileNode = node;
break;
@@ -841,8 +868,8 @@ export default class ScrollPanel extends React.Component {
};
/** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */
- clearPreventShrinking = () => {
- const messageList = this._itemlist.current;
+ public clearPreventShrinking = (): void => {
+ const messageList = this.itemlist.current;
const balanceElement = messageList && messageList.parentElement;
if (balanceElement) balanceElement.style.paddingBottom = null;
this.preventShrinkingState = null;
@@ -857,11 +884,11 @@ export default class ScrollPanel extends React.Component {
from the bottom of the marked tile grows larger than
what it was when marking.
*/
- updatePreventShrinking = () => {
+ public updatePreventShrinking = (): void => {
if (this.preventShrinkingState) {
- const sn = this._getScrollNode();
+ const sn = this.getScrollNode();
const scrollState = this.scrollState;
- const messageList = this._itemlist.current;
+ const messageList = this.itemlist.current;
const {offsetNode, offsetFromBottom} = this.preventShrinkingState;
// element used to set paddingBottom to balance the typing notifs disappearing
const balanceElement = messageList.parentElement;
@@ -898,13 +925,15 @@ export default class ScrollPanel extends React.Component {
// list-style-type: none; is no longer a list
return (
+ className={`mx_ScrollPanel ${this.props.className}`}
+ style={this.props.style}
+ >
{ this.props.fixedChildren }
-
+
{ this.props.children }
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.tsx
similarity index 75%
rename from src/components/structures/TimelinePanel.js
rename to src/components/structures/TimelinePanel.tsx
index 03d0b5c6d7..c2e7a6f346 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.tsx
@@ -1,8 +1,5 @@
/*
-Copyright 2016 OpenMarket Ltd
-Copyright 2017 Vector Creations Ltd
-Copyright 2019 New Vector Ltd
-Copyright 2019-2020 The Matrix.org Foundation C.I.C.
+Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,13 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import SettingsStore from "../../settings/SettingsStore";
-import { LayoutPropType } from "../../settings/Layout";
-import React, { createRef } from 'react';
+import React, { createRef, ReactNode, SyntheticEvent } from 'react';
import ReactDOM from "react-dom";
-import PropTypes from 'prop-types';
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { TimelineSet } from "matrix-js-sdk/src/models/event-timeline-set";
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
import { TimelineWindow } from "matrix-js-sdk/src/timeline-window";
+
+import SettingsStore from "../../settings/SettingsStore";
+import { Layout } from "../../settings/Layout";
import { _t } from '../../languageHandler';
import { MatrixClientPeg } from "../../MatrixClientPeg";
import RoomContext from "../../contexts/RoomContext";
@@ -35,11 +35,19 @@ import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer';
import shouldHideEvent from '../../shouldHideEvent';
import EditorStateTransfer from '../../utils/EditorStateTransfer';
-import { haveTileForEvent } from "../views/rooms/EventTile";
+import { haveTileForEvent, TileShape } from "../views/rooms/EventTile";
import { UIFeature } from "../../settings/UIFeature";
import { replaceableComponent } from "../../utils/replaceableComponent";
import { arrayFastClone } from "../../utils/arrays";
import { Action } from "../../dispatcher/actions";
+import MessagePanel from "./MessagePanel";
+import { SyncState } from 'matrix-js-sdk/src/sync.api';
+import { IScrollState } from "./ScrollPanel";
+import { ActionPayload } from "../../dispatcher/payloads";
+import { EventType } from 'matrix-js-sdk/src/@types/event';
+import ResizeNotifier from "../../utils/ResizeNotifier";
+import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
+import Spinner from "../views/elements/Spinner";
const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20;
@@ -47,90 +55,159 @@ const READ_RECEIPT_INTERVAL_MS = 500;
const DEBUG = false;
-let debuglog = function() {};
+let debuglog = function(...s: any[]) {};
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
debuglog = console.log.bind(console);
}
+interface IProps {
+ // The js-sdk EventTimelineSet object for the timeline sequence we are
+ // representing. This may or may not have a room, depending on what it's
+ // a timeline representing. If it has a room, we maintain RRs etc for
+ // that room.
+ timelineSet: TimelineSet;
+ showReadReceipts?: boolean;
+ // Enable managing RRs and RMs. These require the timelineSet to have a room.
+ manageReadReceipts?: boolean;
+ sendReadReceiptOnLoad?: boolean;
+ manageReadMarkers?: boolean;
+
+ // true to give the component a 'display: none' style.
+ hidden?: boolean;
+
+ // ID of an event to highlight. If undefined, no event will be highlighted.
+ // typically this will be either 'eventId' or undefined.
+ highlightedEventId?: string;
+
+ // id of an event to jump to. If not given, will go to the end of the live timeline.
+ eventId?: string;
+
+ // where to position the event given by eventId, in pixels from the bottom of the viewport.
+ // If not given, will try to put the event half way down the viewport.
+ eventPixelOffset?: number;
+
+ // Should we show URL Previews
+ showUrlPreview?: boolean;
+
+ // maximum number of events to show in a timeline
+ timelineCap?: number;
+
+ // classname to use for the messagepanel
+ className?: string;
+
+ // shape property to be passed to EventTiles
+ tileShape?: TileShape;
+
+ // placeholder to use if the timeline is empty
+ empty?: ReactNode;
+
+ // whether to show reactions for an event
+ showReactions?: boolean;
+
+ // which layout to use
+ layout?: Layout;
+
+ // whether to always show timestamps for an event
+ alwaysShowTimestamps?: boolean;
+
+ resizeNotifier?: ResizeNotifier;
+ editState?: EditorStateTransfer;
+ permalinkCreator?: RoomPermalinkCreator;
+ membersLoaded?: boolean;
+
+ // callback which is called when the panel is scrolled.
+ onScroll?(event: Event): void;
+
+ // callback which is called when the user interacts with the room timeline
+ onUserScroll?(event: SyntheticEvent): void;
+
+ // callback which is called when the read-up-to mark is updated.
+ onReadMarkerUpdated?(): void;
+
+ // callback which is called when we wish to paginate the timeline window.
+ onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise,
+}
+
+interface IState {
+ events: MatrixEvent[];
+ liveEvents: MatrixEvent[];
+ // track whether our room timeline is loading
+ timelineLoading: boolean;
+
+ // the index of the first event that is to be shown
+ firstVisibleEventIndex: number;
+
+ // canBackPaginate == false may mean:
+ //
+ // * we haven't (successfully) loaded the timeline yet, or:
+ //
+ // * we have got to the point where the room was created, or:
+ //
+ // * the server indicated that there were no more visible events
+ // (normally implying we got to the start of the room), or:
+ //
+ // * we gave up asking the server for more events
+ canBackPaginate: boolean;
+
+ // canForwardPaginate == false may mean:
+ //
+ // * we haven't (successfully) loaded the timeline yet
+ //
+ // * we have got to the end of time and are now tracking the live
+ // timeline, or:
+ //
+ // * the server indicated that there were no more visible events
+ // (not sure if this ever happens when we're not at the live
+ // timeline), or:
+ //
+ // * we are looking at some historical point, but gave up asking
+ // the server for more events
+ canForwardPaginate: boolean;
+
+ // start with the read-marker visible, so that we see its animated
+ // disappearance when switching into the room.
+ readMarkerVisible: boolean;
+
+ readMarkerEventId: string;
+
+ backPaginating: boolean;
+ forwardPaginating: boolean;
+
+ // cache of matrixClient.getSyncState() (but from the 'sync' event)
+ clientSyncState: SyncState;
+
+ // should the event tiles have twelve hour times
+ isTwelveHour: boolean;
+
+ // always show timestamps on event tiles?
+ alwaysShowTimestamps: boolean;
+
+ // how long to show the RM for when it's visible in the window
+ readMarkerInViewThresholdMs: number;
+
+ // how long to show the RM for when it's scrolled off-screen
+ readMarkerOutOfViewThresholdMs: number;
+
+ editState?: EditorStateTransfer;
+}
+
+interface IEventIndexOpts {
+ ignoreOwn?: boolean;
+ allowPartial?: boolean;
+}
+
/*
* Component which shows the event timeline in a room view.
*
* Also responsible for handling and sending read receipts.
*/
@replaceableComponent("structures.TimelinePanel")
-class TimelinePanel extends React.Component {
- static propTypes = {
- // The js-sdk EventTimelineSet object for the timeline sequence we are
- // representing. This may or may not have a room, depending on what it's
- // a timeline representing. If it has a room, we maintain RRs etc for
- // that room.
- timelineSet: PropTypes.object.isRequired,
-
- showReadReceipts: PropTypes.bool,
- // Enable managing RRs and RMs. These require the timelineSet to have a room.
- manageReadReceipts: PropTypes.bool,
- sendReadReceiptOnLoad: PropTypes.bool,
- manageReadMarkers: PropTypes.bool,
-
- // true to give the component a 'display: none' style.
- hidden: PropTypes.bool,
-
- // ID of an event to highlight. If undefined, no event will be highlighted.
- // typically this will be either 'eventId' or undefined.
- highlightedEventId: PropTypes.string,
-
- // id of an event to jump to. If not given, will go to the end of the
- // live timeline.
- eventId: PropTypes.string,
-
- // where to position the event given by eventId, in pixels from the
- // bottom of the viewport. If not given, will try to put the event
- // half way down the viewport.
- eventPixelOffset: PropTypes.number,
-
- // Should we show URL Previews
- showUrlPreview: PropTypes.bool,
-
- // callback which is called when the panel is scrolled.
- onScroll: PropTypes.func,
-
- // callback which is called when the user interacts with the room timeline
- onUserScroll: PropTypes.func,
-
- // callback which is called when the read-up-to mark is updated.
- onReadMarkerUpdated: PropTypes.func,
-
- // callback which is called when we wish to paginate the timeline
- // window.
- onPaginationRequest: PropTypes.func,
-
- // maximum number of events to show in a timeline
- timelineCap: PropTypes.number,
-
- // classname to use for the messagepanel
- className: PropTypes.string,
-
- // shape property to be passed to EventTiles
- tileShape: PropTypes.string,
-
- // placeholder to use if the timeline is empty
- empty: PropTypes.node,
-
- // whether to show reactions for an event
- showReactions: PropTypes.bool,
-
- // which layout to use
- layout: LayoutPropType,
-
- // whether to always show timestamps for an event
- alwaysShowTimestamps: PropTypes.bool,
- }
-
+class TimelinePanel extends React.Component {
static contextType = RoomContext;
// a map from room id to read marker event timestamp
- static roomReadMarkerTsMap = {};
+ static roomReadMarkerTsMap: Record = {};
static defaultProps = {
// By default, disable the timelineCap in favour of unpaginating based on
@@ -140,16 +217,21 @@ class TimelinePanel extends React.Component {
sendReadReceiptOnLoad: true,
};
- constructor(props) {
- super(props);
+ private lastRRSentEventId: string = undefined;
+ private lastRMSentEventId: string = undefined;
+
+ private readonly messagePanel = createRef();
+ private readonly dispatcherRef: string;
+ private timelineWindow?: TimelineWindow;
+ private unmounted = false;
+ private readReceiptActivityTimer: Timer;
+ private readMarkerActivityTimer: Timer;
+
+ constructor(props, context) {
+ super(props, context);
debuglog("TimelinePanel: mounting");
- this.lastRRSentEventId = undefined;
- this.lastRMSentEventId = undefined;
-
- this._messagePanel = createRef();
-
// XXX: we could track RM per TimelineSet rather than per Room.
// but for now we just do it per room for simplicity.
let initialReadMarker = null;
@@ -158,82 +240,41 @@ class TimelinePanel extends React.Component {
if (readmarker) {
initialReadMarker = readmarker.getContent().event_id;
} else {
- initialReadMarker = this._getCurrentReadReceipt();
+ initialReadMarker = this.getCurrentReadReceipt();
}
}
this.state = {
events: [],
liveEvents: [],
- timelineLoading: true, // track whether our room timeline is loading
-
- // the index of the first event that is to be shown
+ timelineLoading: true,
firstVisibleEventIndex: 0,
-
- // canBackPaginate == false may mean:
- //
- // * we haven't (successfully) loaded the timeline yet, or:
- //
- // * we have got to the point where the room was created, or:
- //
- // * the server indicated that there were no more visible events
- // (normally implying we got to the start of the room), or:
- //
- // * we gave up asking the server for more events
canBackPaginate: false,
-
- // canForwardPaginate == false may mean:
- //
- // * we haven't (successfully) loaded the timeline yet
- //
- // * we have got to the end of time and are now tracking the live
- // timeline, or:
- //
- // * the server indicated that there were no more visible events
- // (not sure if this ever happens when we're not at the live
- // timeline), or:
- //
- // * we are looking at some historical point, but gave up asking
- // the server for more events
canForwardPaginate: false,
-
- // start with the read-marker visible, so that we see its animated
- // disappearance when switching into the room.
readMarkerVisible: true,
-
readMarkerEventId: initialReadMarker,
-
backPaginating: false,
forwardPaginating: false,
-
- // cache of matrixClient.getSyncState() (but from the 'sync' event)
clientSyncState: MatrixClientPeg.get().getSyncState(),
-
- // should the event tiles have twelve hour times
isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"),
-
- // always show timestamps on event tiles?
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
-
- // how long to show the RM for when it's visible in the window
readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
-
- // how long to show the RM for when it's scrolled off-screen
readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
};
this.dispatcherRef = dis.register(this.onAction);
- MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
- MatrixClientPeg.get().on("Room.timelineReset", this.onRoomTimelineReset);
- MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
+ const cli = MatrixClientPeg.get();
+ cli.on("Room.timeline", this.onRoomTimeline);
+ cli.on("Room.timelineReset", this.onRoomTimelineReset);
+ cli.on("Room.redaction", this.onRoomRedaction);
// same event handler as Room.redaction as for both we just do forceUpdate
- MatrixClientPeg.get().on("Room.redactionCancelled", this.onRoomRedaction);
- MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
- MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
- MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
- MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
- MatrixClientPeg.get().on("Event.replaced", this.onEventReplaced);
- MatrixClientPeg.get().on("sync", this.onSync);
+ cli.on("Room.redactionCancelled", this.onRoomRedaction);
+ cli.on("Room.receipt", this.onRoomReceipt);
+ cli.on("Room.localEchoUpdated", this.onLocalEchoUpdated);
+ cli.on("Room.accountData", this.onAccountData);
+ cli.on("Event.decrypted", this.onEventDecrypted);
+ cli.on("Event.replaced", this.onEventReplaced);
+ cli.on("sync", this.onSync);
}
// TODO: [REACT-WARNING] Move into constructor
@@ -246,7 +287,7 @@ class TimelinePanel extends React.Component {
this.updateReadMarkerOnUserActivity();
}
- this._initTimeline(this.props);
+ this.initTimeline(this.props);
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
@@ -272,7 +313,7 @@ class TimelinePanel extends React.Component {
if (differentEventId || differentHighlightedEventId) {
console.log("TimelinePanel switching to eventId " + newProps.eventId +
" (was " + this.props.eventId + ")");
- return this._initTimeline(newProps);
+ return this.initTimeline(newProps);
}
}
@@ -282,13 +323,13 @@ class TimelinePanel extends React.Component {
//
// (We could use isMounted, but facebook have deprecated that.)
this.unmounted = true;
- if (this._readReceiptActivityTimer) {
- this._readReceiptActivityTimer.abort();
- this._readReceiptActivityTimer = null;
+ if (this.readReceiptActivityTimer) {
+ this.readReceiptActivityTimer.abort();
+ this.readReceiptActivityTimer = null;
}
- if (this._readMarkerActivityTimer) {
- this._readMarkerActivityTimer.abort();
- this._readMarkerActivityTimer = null;
+ if (this.readMarkerActivityTimer) {
+ this.readMarkerActivityTimer.abort();
+ this.readMarkerActivityTimer = null;
}
dis.unregister(this.dispatcherRef);
@@ -308,7 +349,7 @@ class TimelinePanel extends React.Component {
}
}
- onMessageListUnfillRequest = (backwards, scrollToken) => {
+ private onMessageListUnfillRequest = (backwards: boolean, scrollToken: string): void => {
// If backwards, unpaginate from the back (i.e. the start of the timeline)
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
debuglog("TimelinePanel: unpaginating events in direction", dir);
@@ -327,21 +368,30 @@ class TimelinePanel extends React.Component {
if (count > 0) {
debuglog("TimelinePanel: Unpaginating", count, "in direction", dir);
- this._timelineWindow.unpaginate(count, backwards);
+ this.timelineWindow.unpaginate(count, backwards);
- // We can now paginate in the unpaginated direction
- const canPaginateKey = (backwards) ? 'canBackPaginate' : 'canForwardPaginate';
- const { events, liveEvents, firstVisibleEventIndex } = this._getEvents();
- this.setState({
- [canPaginateKey]: true,
+ const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
+ const newState: Partial = {
events,
liveEvents,
firstVisibleEventIndex,
- });
+ }
+
+ // We can now paginate in the unpaginated direction
+ if (backwards) {
+ newState.canBackPaginate = true;
+ } else {
+ newState.canForwardPaginate = true;
+ }
+ this.setState(newState);
}
};
- onPaginationRequest = (timelineWindow, direction, size) => {
+ private onPaginationRequest = (
+ timelineWindow: TimelineWindow,
+ direction: string,
+ size: number,
+ ): Promise => {
if (this.props.onPaginationRequest) {
return this.props.onPaginationRequest(timelineWindow, direction, size);
} else {
@@ -350,8 +400,8 @@ class TimelinePanel extends React.Component {
};
// set off a pagination request.
- onMessageListFillRequest = backwards => {
- if (!this._shouldPaginate()) return Promise.resolve(false);
+ private onMessageListFillRequest = (backwards: boolean): Promise => {
+ if (!this.shouldPaginate()) return Promise.resolve(false);
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
@@ -362,9 +412,9 @@ class TimelinePanel extends React.Component {
return Promise.resolve(false);
}
- if (!this._timelineWindow.canPaginate(dir)) {
+ if (!this.timelineWindow.canPaginate(dir)) {
debuglog("TimelinePanel: can't", dir, "paginate any further");
- this.setState({[canPaginateKey]: false});
+ this.setState({ [canPaginateKey]: false });
return Promise.resolve(false);
}
@@ -374,15 +424,15 @@ class TimelinePanel extends React.Component {
}
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
- this.setState({[paginatingKey]: true});
+ this.setState({ [paginatingKey]: true });
- return this.onPaginationRequest(this._timelineWindow, dir, PAGINATE_SIZE).then((r) => {
+ return this.onPaginationRequest(this.timelineWindow, dir, PAGINATE_SIZE).then((r) => {
if (this.unmounted) { return; }
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
- const { events, liveEvents, firstVisibleEventIndex } = this._getEvents();
- const newState = {
+ const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
+ const newState: Partial = {
[paginatingKey]: false,
[canPaginateKey]: r,
events,
@@ -395,7 +445,7 @@ class TimelinePanel extends React.Component {
const otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
const canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
if (!this.state[canPaginateOtherWayKey] &&
- this._timelineWindow.canPaginate(otherDirection)) {
+ this.timelineWindow.canPaginate(otherDirection)) {
debuglog('TimelinePanel: can now', otherDirection, 'paginate again');
newState[canPaginateOtherWayKey] = true;
}
@@ -406,9 +456,9 @@ class TimelinePanel extends React.Component {
// has in memory because we never gave the component a chance to scroll
// itself into the right place
return new Promise((resolve) => {
- this.setState(newState, () => {
+ this.setState(newState, () => {
// we can continue paginating in the given direction if:
- // - _timelineWindow.paginate says we can
+ // - timelineWindow.paginate says we can
// - we're paginating forwards, or we won't be trying to
// paginate backwards past the first visible event
resolve(r && (!backwards || firstVisibleEventIndex === 0));
@@ -417,7 +467,7 @@ class TimelinePanel extends React.Component {
});
};
- onMessageListScroll = e => {
+ private onMessageListScroll = e => {
if (this.props.onScroll) {
this.props.onScroll(e);
}
@@ -428,18 +478,18 @@ class TimelinePanel extends React.Component {
// it goes back off the top of the screen (presumably because the user
// clicks on the 'jump to bottom' button), we need to re-enable it.
if (rmPosition < 0) {
- this.setState({readMarkerVisible: true});
+ this.setState({ readMarkerVisible: true });
}
// if read marker position goes between 0 and -1/1,
// (and user is active), switch timeout
- const timeout = this._readMarkerTimeout(rmPosition);
+ const timeout = this.readMarkerTimeout(rmPosition);
// NO-OP when timeout already has set to the given value
- this._readMarkerActivityTimer.changeTimeout(timeout);
+ this.readMarkerActivityTimer.changeTimeout(timeout);
}
};
- onAction = payload => {
+ private onAction = (payload: ActionPayload): void => {
switch (payload.action) {
case "ignore_state_changed":
this.forceUpdate();
@@ -447,9 +497,9 @@ class TimelinePanel extends React.Component {
case "edit_event": {
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
- this.setState({editState}, () => {
- if (payload.event && this._messagePanel.current) {
- this._messagePanel.current.scrollToEventIfNeeded(
+ this.setState({ editState }, () => {
+ if (payload.event && this.messagePanel.current) {
+ this.messagePanel.current.scrollToEventIfNeeded(
payload.event.getId(),
);
}
@@ -479,7 +529,16 @@ class TimelinePanel extends React.Component {
}
};
- onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => {
+ private onRoomTimeline = (
+ ev: MatrixEvent,
+ room: Room,
+ toStartOfTimeline: boolean,
+ removed: boolean,
+ data: {
+ timeline: EventTimeline;
+ liveEvent?: boolean;
+ },
+ ): void => {
// ignore events for other timeline sets
if (data.timeline.getTimelineSet() !== this.props.timelineSet) return;
@@ -487,9 +546,9 @@ class TimelinePanel extends React.Component {
// updates from pagination will happen when the paginate completes.
if (toStartOfTimeline || !data || !data.liveEvent) return;
- if (!this._messagePanel.current) return;
+ if (!this.messagePanel.current) return;
- if (!this._messagePanel.current.getScrollState().stuckAtBottom) {
+ if (!this.messagePanel.current.getScrollState().stuckAtBottom) {
// we won't load this event now, because we don't want to push any
// events off the other end of the timeline. But we need to note
// that we can now paginate.
@@ -506,13 +565,13 @@ class TimelinePanel extends React.Component {
// timeline window.
//
// see https://github.com/vector-im/vector-web/issues/1035
- this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => {
+ this.timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => {
if (this.unmounted) { return; }
- const { events, liveEvents, firstVisibleEventIndex } = this._getEvents();
+ const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
const lastLiveEvent = liveEvents[liveEvents.length - 1];
- const updatedState = {
+ const updatedState: Partial = {
events,
liveEvents,
firstVisibleEventIndex,
@@ -537,15 +596,15 @@ class TimelinePanel extends React.Component {
// we know we're stuckAtBottom, so we can advance the RM
// immediately, to save a later render cycle
- this._setReadMarker(lastLiveEvent.getId(), lastLiveEvent.getTs(), true);
+ this.setReadMarker(lastLiveEvent.getId(), lastLiveEvent.getTs(), true);
updatedState.readMarkerVisible = false;
updatedState.readMarkerEventId = lastLiveEvent.getId();
callRMUpdated = true;
}
}
- this.setState(updatedState, () => {
- this._messagePanel.current.updateTimelineMinHeight();
+ this.setState(updatedState, () => {
+ this.messagePanel.current.updateTimelineMinHeight();
if (callRMUpdated) {
this.props.onReadMarkerUpdated();
}
@@ -553,17 +612,17 @@ class TimelinePanel extends React.Component {
});
};
- onRoomTimelineReset = (room, timelineSet) => {
+ private onRoomTimelineReset = (room: Room, timelineSet: TimelineSet): void => {
if (timelineSet !== this.props.timelineSet) return;
- if (this._messagePanel.current && this._messagePanel.current.isAtBottom()) {
- this._loadTimeline();
+ if (this.messagePanel.current && this.messagePanel.current.isAtBottom()) {
+ this.loadTimeline();
}
};
- canResetTimeline = () => this._messagePanel.current && this._messagePanel.current.isAtBottom();
+ public canResetTimeline = () => this.messagePanel?.current.isAtBottom();
- onRoomRedaction = (ev, room) => {
+ private onRoomRedaction = (ev: MatrixEvent, room: Room): void => {
if (this.unmounted) return;
// ignore events for other rooms
@@ -574,7 +633,7 @@ class TimelinePanel extends React.Component {
this.forceUpdate();
};
- onEventReplaced = (replacedEvent, room) => {
+ private onEventReplaced = (replacedEvent: MatrixEvent, room: Room): void => {
if (this.unmounted) return;
// ignore events for other rooms
@@ -585,7 +644,7 @@ class TimelinePanel extends React.Component {
this.forceUpdate();
};
- onRoomReceipt = (ev, room) => {
+ private onRoomReceipt = (ev: MatrixEvent, room: Room): void => {
if (this.unmounted) return;
// ignore events for other rooms
@@ -594,22 +653,22 @@ class TimelinePanel extends React.Component {
this.forceUpdate();
};
- onLocalEchoUpdated = (ev, room, oldEventId) => {
+ private onLocalEchoUpdated = (ev: MatrixEvent, room: Room, oldEventId: string): void => {
if (this.unmounted) return;
// ignore events for other rooms
if (room !== this.props.timelineSet.room) return;
- this._reloadEvents();
+ this.reloadEvents();
};
- onAccountData = (ev, room) => {
+ private onAccountData = (ev: MatrixEvent, room: Room): void => {
if (this.unmounted) return;
// ignore events for other rooms
if (room !== this.props.timelineSet.room) return;
- if (ev.getType() !== "m.fully_read") return;
+ if (ev.getType() !== EventType.FullyRead) return;
// XXX: roomReadMarkerTsMap not updated here so it is now inconsistent. Replace
// this mechanism of determining where the RM is relative to the view-port with
@@ -619,7 +678,7 @@ class TimelinePanel extends React.Component {
}, this.props.onReadMarkerUpdated);
};
- onEventDecrypted = ev => {
+ private onEventDecrypted = (ev: MatrixEvent): void => {
// Can be null for the notification timeline, etc.
if (!this.props.timelineSet.room) return;
@@ -634,46 +693,46 @@ class TimelinePanel extends React.Component {
}
};
- onSync = (state, prevState, data) => {
- this.setState({clientSyncState: state});
+ private onSync = (clientSyncState: SyncState, prevState: SyncState, data: object): void => {
+ this.setState({ clientSyncState });
};
- _readMarkerTimeout(readMarkerPosition) {
+ private readMarkerTimeout(readMarkerPosition: number): number {
return readMarkerPosition === 0 ?
this.state.readMarkerInViewThresholdMs :
this.state.readMarkerOutOfViewThresholdMs;
}
- async updateReadMarkerOnUserActivity() {
- const initialTimeout = this._readMarkerTimeout(this.getReadMarkerPosition());
- this._readMarkerActivityTimer = new Timer(initialTimeout);
+ private async updateReadMarkerOnUserActivity(): Promise {
+ const initialTimeout = this.readMarkerTimeout(this.getReadMarkerPosition());
+ this.readMarkerActivityTimer = new Timer(initialTimeout);
- while (this._readMarkerActivityTimer) { //unset on unmount
- UserActivity.sharedInstance().timeWhileActiveRecently(this._readMarkerActivityTimer);
+ while (this.readMarkerActivityTimer) { //unset on unmount
+ UserActivity.sharedInstance().timeWhileActiveRecently(this.readMarkerActivityTimer);
try {
- await this._readMarkerActivityTimer.finished();
+ await this.readMarkerActivityTimer.finished();
} catch (e) { continue; /* aborted */ }
// outside of try/catch to not swallow errors
this.updateReadMarker();
}
}
- async updateReadReceiptOnUserActivity() {
- this._readReceiptActivityTimer = new Timer(READ_RECEIPT_INTERVAL_MS);
- while (this._readReceiptActivityTimer) { //unset on unmount
- UserActivity.sharedInstance().timeWhileActiveNow(this._readReceiptActivityTimer);
+ private async updateReadReceiptOnUserActivity(): Promise {
+ this.readReceiptActivityTimer = new Timer(READ_RECEIPT_INTERVAL_MS);
+ while (this.readReceiptActivityTimer) { //unset on unmount
+ UserActivity.sharedInstance().timeWhileActiveNow(this.readReceiptActivityTimer);
try {
- await this._readReceiptActivityTimer.finished();
+ await this.readReceiptActivityTimer.finished();
} catch (e) { continue; /* aborted */ }
// outside of try/catch to not swallow errors
this.sendReadReceipt();
}
}
- sendReadReceipt = () => {
+ private sendReadReceipt = (): void => {
if (SettingsStore.getValue("lowBandwidth")) return;
- if (!this._messagePanel.current) return;
+ if (!this.messagePanel.current) return;
if (!this.props.manageReadReceipts) return;
// This happens on user_activity_end which is delayed, and it's
// very possible have logged out within that timeframe, so check
@@ -684,8 +743,8 @@ class TimelinePanel extends React.Component {
let shouldSendRR = true;
- const currentRREventId = this._getCurrentReadReceipt(true);
- const currentRREventIndex = this._indexForEventId(currentRREventId);
+ const currentRREventId = this.getCurrentReadReceipt(true);
+ const currentRREventIndex = this.indexForEventId(currentRREventId);
// We want to avoid sending out read receipts when we are looking at
// events in the past which are before the latest RR.
//
@@ -700,11 +759,11 @@ class TimelinePanel extends React.Component {
// the user eventually hits the live timeline.
//
if (currentRREventId && currentRREventIndex === null &&
- this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
+ this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
shouldSendRR = false;
}
- const lastReadEventIndex = this._getLastDisplayedEventIndex({
+ const lastReadEventIndex = this.getLastDisplayedEventIndex({
ignoreOwn: true,
});
if (lastReadEventIndex === null) {
@@ -778,7 +837,7 @@ class TimelinePanel extends React.Component {
// if the read marker is on the screen, we can now assume we've caught up to the end
// of the screen, so move the marker down to the bottom of the screen.
- updateReadMarker = () => {
+ private updateReadMarker = (): void => {
if (!this.props.manageReadMarkers) return;
if (this.getReadMarkerPosition() === 1) {
// the read marker is at an event below the viewport,
@@ -788,7 +847,7 @@ class TimelinePanel extends React.Component {
// move the RM to *after* the message at the bottom of the screen. This
// avoids a problem whereby we never advance the RM if there is a huge
// message which doesn't fit on the screen.
- const lastDisplayedIndex = this._getLastDisplayedEventIndex({
+ const lastDisplayedIndex = this.getLastDisplayedEventIndex({
allowPartial: true,
});
@@ -796,7 +855,7 @@ class TimelinePanel extends React.Component {
return;
}
const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
- this._setReadMarker(
+ this.setReadMarker(
lastDisplayedEvent.getId(),
lastDisplayedEvent.getTs(),
);
@@ -815,13 +874,13 @@ class TimelinePanel extends React.Component {
// advance the read marker past any events we sent ourselves.
- _advanceReadMarkerPastMyEvents() {
+ private advanceReadMarkerPastMyEvents(): void {
if (!this.props.manageReadMarkers) return;
- // we call `_timelineWindow.getEvents()` rather than using
+ // we call `timelineWindow.getEvents()` rather than using
// `this.state.liveEvents`, because React batches the update to the
// latter, so it may not have been updated yet.
- const events = this._timelineWindow.getEvents();
+ const events = this.timelineWindow.getEvents();
// first find where the current RM is
let i;
@@ -846,22 +905,22 @@ class TimelinePanel extends React.Component {
i--;
const ev = events[i];
- this._setReadMarker(ev.getId(), ev.getTs());
+ this.setReadMarker(ev.getId(), ev.getTs());
}
/* jump down to the bottom of this room, where new events are arriving
*/
- jumpToLiveTimeline = () => {
+ public jumpToLiveTimeline = (): void => {
// if we can't forward-paginate the existing timeline, then there
// is no point reloading it - just jump straight to the bottom.
//
// Otherwise, reload the timeline rather than trying to paginate
// through all of space-time.
- if (this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
- this._loadTimeline();
+ if (this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
+ this.loadTimeline();
} else {
- if (this._messagePanel.current) {
- this._messagePanel.current.scrollToBottom();
+ if (this.messagePanel.current) {
+ this.messagePanel.current.scrollToBottom();
}
}
};
@@ -869,22 +928,22 @@ class TimelinePanel extends React.Component {
/* scroll to show the read-up-to marker. We put it 1/3 of the way down
* the container.
*/
- jumpToReadMarker = () => {
+ public jumpToReadMarker = (): void => {
if (!this.props.manageReadMarkers) return;
- if (!this._messagePanel.current) return;
+ if (!this.messagePanel.current) return;
if (!this.state.readMarkerEventId) return;
// we may not have loaded the event corresponding to the read-marker
- // into the _timelineWindow. In that case, attempts to scroll to it
+ // into the timelineWindow. In that case, attempts to scroll to it
// will fail.
//
// a quick way to figure out if we've loaded the relevant event is
// simply to check if the messagepanel knows where the read-marker is.
- const ret = this._messagePanel.current.getReadMarkerPosition();
+ const ret = this.messagePanel.current.getReadMarkerPosition();
if (ret !== null) {
// The messagepanel knows where the RM is, so we must have loaded
// the relevant event.
- this._messagePanel.current.scrollToEvent(this.state.readMarkerEventId,
+ this.messagePanel.current.scrollToEvent(this.state.readMarkerEventId,
0, 1/3);
return;
}
@@ -892,15 +951,15 @@ class TimelinePanel extends React.Component {
// Looks like we haven't loaded the event corresponding to the read-marker.
// As with jumpToLiveTimeline, we want to reload the timeline around the
// read-marker.
- this._loadTimeline(this.state.readMarkerEventId, 0, 1/3);
+ this.loadTimeline(this.state.readMarkerEventId, 0, 1/3);
};
/* update the read-up-to marker to match the read receipt
*/
- forgetReadMarker = () => {
+ public forgetReadMarker = (): void => {
if (!this.props.manageReadMarkers) return;
- const rmId = this._getCurrentReadReceipt();
+ const rmId = this.getCurrentReadReceipt();
// see if we know the timestamp for the rr event
const tl = this.props.timelineSet.getTimelineForEvent(rmId);
@@ -912,17 +971,17 @@ class TimelinePanel extends React.Component {
}
}
- this._setReadMarker(rmId, rmTs);
+ this.setReadMarker(rmId, rmTs);
};
/* return true if the content is fully scrolled down and we are
* at the end of the live timeline.
*/
- isAtEndOfLiveTimeline = () => {
- return this._messagePanel.current
- && this._messagePanel.current.isAtBottom()
- && this._timelineWindow
- && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
+ public isAtEndOfLiveTimeline = (): boolean => {
+ return this.messagePanel.current
+ && this.messagePanel.current.isAtBottom()
+ && this.timelineWindow
+ && !this.timelineWindow.canPaginate(EventTimeline.FORWARDS);
}
@@ -931,9 +990,9 @@ class TimelinePanel extends React.Component {
*
* returns null if we are not mounted.
*/
- getScrollState = () => {
- if (!this._messagePanel.current) { return null; }
- return this._messagePanel.current.getScrollState();
+ public getScrollState = (): IScrollState => {
+ if (!this.messagePanel.current) { return null; }
+ return this.messagePanel.current.getScrollState();
};
// returns one of:
@@ -942,11 +1001,11 @@ class TimelinePanel extends React.Component {
// -1: read marker is above the window
// 0: read marker is visible
// +1: read marker is below the window
- getReadMarkerPosition = () => {
+ public getReadMarkerPosition = (): number => {
if (!this.props.manageReadMarkers) return null;
- if (!this._messagePanel.current) return null;
+ if (!this.messagePanel.current) return null;
- const ret = this._messagePanel.current.getReadMarkerPosition();
+ const ret = this.messagePanel.current.getReadMarkerPosition();
if (ret !== null) {
return ret;
}
@@ -965,7 +1024,7 @@ class TimelinePanel extends React.Component {
return null;
};
- canJumpToReadMarker = () => {
+ public canJumpToReadMarker = (): boolean => {
// 1. Do not show jump bar if neither the RM nor the RR are set.
// 3. We want to show the bar if the read-marker is off the top of the screen.
// 4. Also, if pos === null, the event might not be paginated - show the unread bar
@@ -980,19 +1039,19 @@ class TimelinePanel extends React.Component {
*
* We pass it down to the scroll panel.
*/
- handleScrollKey = ev => {
- if (!this._messagePanel.current) { return; }
+ public handleScrollKey = ev => {
+ if (!this.messagePanel.current) { return; }
// jump to the live timeline on ctrl-end, rather than the end of the
// timeline window.
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey && ev.key === Key.END) {
this.jumpToLiveTimeline();
} else {
- this._messagePanel.current.handleScrollKey(ev);
+ this.messagePanel.current.handleScrollKey(ev);
}
};
- _initTimeline(props) {
+ private initTimeline(props: IProps): void {
const initialEvent = props.eventId;
const pixelOffset = props.eventPixelOffset;
@@ -1003,7 +1062,7 @@ class TimelinePanel extends React.Component {
offsetBase = 0.5;
}
- return this._loadTimeline(initialEvent, pixelOffset, offsetBase);
+ return this.loadTimeline(initialEvent, pixelOffset, offsetBase);
}
/**
@@ -1019,34 +1078,32 @@ class TimelinePanel extends React.Component {
* @param {number?} offsetBase the reference point for the pixelOffset. 0
* means the top of the container, 1 means the bottom, and fractional
* values mean somewhere in the middle. If omitted, it defaults to 0.
- *
- * returns a promise which will resolve when the load completes.
*/
- _loadTimeline(eventId, pixelOffset, offsetBase) {
- this._timelineWindow = new TimelineWindow(
+ private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number): void {
+ this.timelineWindow = new TimelineWindow(
MatrixClientPeg.get(), this.props.timelineSet,
{windowLimit: this.props.timelineCap});
const onLoaded = () => {
// clear the timeline min-height when
// (re)loading the timeline
- if (this._messagePanel.current) {
- this._messagePanel.current.onTimelineReset();
+ if (this.messagePanel.current) {
+ this.messagePanel.current.onTimelineReset();
}
- this._reloadEvents();
+ this.reloadEvents();
// If we switched away from the room while there were pending
// outgoing events, the read-marker will be before those events.
// We need to skip over any which have subsequently been sent.
- this._advanceReadMarkerPastMyEvents();
+ this.advanceReadMarkerPastMyEvents();
this.setState({
- canBackPaginate: this._timelineWindow.canPaginate(EventTimeline.BACKWARDS),
- canForwardPaginate: this._timelineWindow.canPaginate(EventTimeline.FORWARDS),
+ canBackPaginate: this.timelineWindow.canPaginate(EventTimeline.BACKWARDS),
+ canForwardPaginate: this.timelineWindow.canPaginate(EventTimeline.FORWARDS),
timelineLoading: false,
}, () => {
// initialise the scroll state of the message panel
- if (!this._messagePanel.current) {
+ if (!this.messagePanel.current) {
// this shouldn't happen - we know we're mounted because
// we're in a setState callback, and we know
// timelineLoading is now false, so render() should have
@@ -1056,10 +1113,10 @@ class TimelinePanel extends React.Component {
return;
}
if (eventId) {
- this._messagePanel.current.scrollToEvent(eventId, pixelOffset,
+ this.messagePanel.current.scrollToEvent(eventId, pixelOffset,
offsetBase);
} else {
- this._messagePanel.current.scrollToBottom();
+ this.messagePanel.current.scrollToBottom();
}
if (this.props.sendReadReceiptOnLoad) {
@@ -1121,10 +1178,10 @@ class TimelinePanel extends React.Component {
if (timeline) {
// This is a hot-path optimization by skipping a promise tick
// by repeating a no-op sync branch in TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline
- this._timelineWindow.load(eventId, INITIAL_SIZE); // in this branch this method will happen in sync time
+ this.timelineWindow.load(eventId, INITIAL_SIZE); // in this branch this method will happen in sync time
onLoaded();
} else {
- const prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
+ const prom = this.timelineWindow.load(eventId, INITIAL_SIZE);
this.setState({
events: [],
liveEvents: [],
@@ -1139,17 +1196,17 @@ class TimelinePanel extends React.Component {
// handle the completion of a timeline load or localEchoUpdate, by
// reloading the events from the timelinewindow and pending event list into
// the state.
- _reloadEvents() {
+ private reloadEvents(): void {
// we might have switched rooms since the load started - just bin
// the results if so.
if (this.unmounted) return;
- this.setState(this._getEvents());
+ this.setState(this.getEvents());
}
// get the list of events from the timeline window and the pending event list
- _getEvents() {
- const events = this._timelineWindow.getEvents();
+ private getEvents(): Pick {
+ const events: MatrixEvent[] = this.timelineWindow.getEvents();
// `arrayFastClone` performs a shallow copy of the array
// we want the last event to be decrypted first but displayed last
@@ -1161,14 +1218,14 @@ class TimelinePanel extends React.Component {
client.decryptEventIfNeeded(event);
});
- const firstVisibleEventIndex = this._checkForPreJoinUISI(events);
+ const firstVisibleEventIndex = this.checkForPreJoinUISI(events);
// Hold onto the live events separately. The read receipt and read marker
// should use this list, so that they don't advance into pending events.
const liveEvents = [...events];
// if we're at the end of the live timeline, append the pending events
- if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
+ if (!this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(...this.props.timelineSet.getPendingEvents());
}
@@ -1189,7 +1246,7 @@ class TimelinePanel extends React.Component {
* undecryptable event that was sent while the user was not in the room. If no
* such events were found, then it returns 0.
*/
- _checkForPreJoinUISI(events) {
+ private checkForPreJoinUISI(events: MatrixEvent[]): number {
const room = this.props.timelineSet.room;
if (events.length === 0 || !room ||
@@ -1253,7 +1310,7 @@ class TimelinePanel extends React.Component {
return 0;
}
- _indexForEventId(evId) {
+ private indexForEventId(evId: string): number | null {
for (let i = 0; i < this.state.events.length; ++i) {
if (evId == this.state.events[i].getId()) {
return i;
@@ -1262,15 +1319,14 @@ class TimelinePanel extends React.Component {
return null;
}
- _getLastDisplayedEventIndex(opts) {
- opts = opts || {};
+ private getLastDisplayedEventIndex(opts: IEventIndexOpts = {}): number | null {
const ignoreOwn = opts.ignoreOwn || false;
const allowPartial = opts.allowPartial || false;
- const messagePanel = this._messagePanel.current;
+ const messagePanel = this.messagePanel.current;
if (!messagePanel) return null;
- const messagePanelNode = ReactDOM.findDOMNode(messagePanel);
+ const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as HTMLElement;
if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync
const wrapperRect = messagePanelNode.getBoundingClientRect();
const myUserId = MatrixClientPeg.get().credentials.userId;
@@ -1347,7 +1403,7 @@ class TimelinePanel extends React.Component {
* SDK.
* @return {String} the event ID
*/
- _getCurrentReadReceipt(ignoreSynthesized) {
+ private getCurrentReadReceipt(ignoreSynthesized = false): string {
const client = MatrixClientPeg.get();
// the client can be null on logout
if (client == null) {
@@ -1358,7 +1414,7 @@ class TimelinePanel extends React.Component {
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);
}
- _setReadMarker(eventId, eventTs, inhibitSetState) {
+ private setReadMarker(eventId: string, eventTs: number, inhibitSetState = false): void {
const roomId = this.props.timelineSet.room.roomId;
// don't update the state (and cause a re-render) if there is
@@ -1383,7 +1439,7 @@ class TimelinePanel extends React.Component {
}, this.props.onReadMarkerUpdated);
}
- _shouldPaginate() {
+ private shouldPaginate(): boolean {
// don't try to paginate while events in the timeline are
// still being decrypted. We don't render events while they're
// being decrypted, so they don't take up space in the timeline.
@@ -1394,12 +1450,9 @@ class TimelinePanel extends React.Component {
});
}
- getRelationsForEvent = (...args) => this.props.timelineSet.getRelationsForEvent(...args);
+ private getRelationsForEvent = (...args) => this.props.timelineSet.getRelationsForEvent(...args);
render() {
- const MessagePanel = sdk.getComponent("structures.MessagePanel");
- const Loader = sdk.getComponent("elements.Spinner");
-
// just show a spinner while the timeline loads.
//
// put it in a div of the right class (mx_RoomView_messagePanel) so
@@ -1414,7 +1467,7 @@ class TimelinePanel extends React.Component {
if (this.state.timelineLoading) {
return (
-
+
);
}
@@ -1435,7 +1488,7 @@ class TimelinePanel extends React.Component {
// forwards, otherwise if somebody hits the bottom of the loaded
// events when viewing historical messages, we get stuck in a loop
// of paginating our way through the entire history of the room.
- const stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
+ const stickyBottom = !this.timelineWindow.canPaginate(EventTimeline.FORWARDS);
// If the state is PREPARED or CATCHUP, we're still waiting for the js-sdk to sync with
// the HS and fetch the latest events, so we are effectively forward paginating.
@@ -1448,7 +1501,7 @@ class TimelinePanel extends React.Component {
: this.state.events;
return (
= ({ matrixClient: cli, event, permalinkCr
userId,
getAvatarUrl: (..._) => {
return avatarUrlForUser(
- { avatarUrl: profileInfo.avatar_url },
+ {avatarUrl: profileInfo.avatar_url},
AVATAR_SIZE, AVATAR_SIZE, "crop",
);
},
getMxcAvatarUrl: () => profileInfo.avatar_url,
- };
+ } as RoomMember;
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase();
diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.tsx
similarity index 80%
rename from src/components/views/elements/ErrorBoundary.js
rename to src/components/views/elements/ErrorBoundary.tsx
index 9037287f49..f967b8c594 100644
--- a/src/components/views/elements/ErrorBoundary.js
+++ b/src/components/views/elements/ErrorBoundary.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,21 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
-import * as sdk from '../../../index';
+import React, { ErrorInfo } from 'react';
+
import { _t } from '../../../languageHandler';
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
+import { MatrixClientPeg } from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg';
import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import BugReportDialog from '../dialogs/BugReportDialog';
+import AccessibleButton from './AccessibleButton';
+
+interface IState {
+ error: Error;
+}
/**
* This error boundary component can be used to wrap large content areas and
* catch exceptions during rendering in the component tree below them.
*/
@replaceableComponent("views.elements.ErrorBoundary")
-export default class ErrorBoundary extends React.PureComponent {
+export default class ErrorBoundary extends React.PureComponent<{}, IState> {
constructor(props) {
super(props);
@@ -37,13 +43,13 @@ export default class ErrorBoundary extends React.PureComponent {
};
}
- static getDerivedStateFromError(error) {
+ static getDerivedStateFromError(error: Error): Partial {
// Side effects are not permitted here, so we only update the state so
// that the next render shows an error message.
return { error };
}
- componentDidCatch(error, { componentStack }) {
+ componentDidCatch(error: Error, { componentStack }: ErrorInfo): void {
// Browser consoles are better at formatting output when native errors are passed
// in their own `console.error` invocation.
console.error(error);
@@ -53,7 +59,7 @@ export default class ErrorBoundary extends React.PureComponent {
);
}
- _onClearCacheAndReload = () => {
+ private onClearCacheAndReload = (): void => {
if (!PlatformPeg.get()) return;
MatrixClientPeg.get().stopClient();
@@ -62,11 +68,7 @@ export default class ErrorBoundary extends React.PureComponent {
});
};
- _onBugReport = () => {
- const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
- if (!BugReportDialog) {
- return;
- }
+ private onBugReport = (): void => {
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
label: 'react-soft-crash',
});
@@ -74,7 +76,6 @@ export default class ErrorBoundary extends React.PureComponent {
render() {
if (this.state.error) {
- const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
let bugReportSection;
@@ -95,7 +96,7 @@ export default class ErrorBoundary extends React.PureComponent {
"the rooms or groups you have visited and the usernames of " +
"other users. They do not contain messages.",
)}
{ bugReportSection }
-
+
{_t("Clear cache and reload")}
diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx
index 86d3e082ad..ab647db9ed 100644
--- a/src/components/views/elements/EventListSummary.tsx
+++ b/src/components/views/elements/EventListSummary.tsx
@@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {ReactChildren, useEffect} from 'react';
-import {MatrixEvent} from "matrix-js-sdk/src/models/event";
-import {RoomMember} from "matrix-js-sdk/src/models/room-member";
+import React, { ReactNode, useEffect } from 'react';
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler';
-import {useStateToggle} from "../../../hooks/useStateToggle";
+import { useStateToggle } from "../../../hooks/useStateToggle";
import AccessibleButton from "./AccessibleButton";
interface IProps {
@@ -31,11 +31,11 @@ interface IProps {
// Whether or not to begin with state.expanded=true
startExpanded?: boolean,
// The list of room members for which to show avatars next to the summary
- summaryMembers?: RoomMember[],
+ summaryMembers?: RoomMember[];
// The text to show as the summary of this event list
- summaryText?: string,
+ summaryText?: string;
// An array of EventTiles to render when expanded
- children: ReactChildren,
+ children: ReactNode[];
// Called when the event list expansion is toggled
onToggle?(): void;
}
diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
index cf3b7a6e61..8e73b3d9ca 100644
--- a/src/components/views/elements/EventTilePreview.tsx
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -17,13 +17,14 @@ limitations under the License.
import React from 'react';
import classnames from 'classnames';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
+import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import * as Avatar from '../../../Avatar';
import EventTile from '../rooms/EventTile';
import SettingsStore from "../../../settings/SettingsStore";
-import {Layout} from "../../../settings/Layout";
-import {UIFeature} from "../../../settings/UIFeature";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { Layout } from "../../../settings/Layout";
+import { UIFeature } from "../../../settings/UIFeature";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
/**
@@ -105,12 +106,12 @@ export default class EventTilePreview extends React.Component {
userId: this.props.userId,
getAvatarUrl: (..._) => {
return Avatar.avatarUrlForUser(
- { avatarUrl: this.props.avatarUrl },
+ {avatarUrl: this.props.avatarUrl},
AVATAR_SIZE, AVATAR_SIZE, "crop",
);
},
getMxcAvatarUrl: () => this.props.avatarUrl,
- };
+ } as RoomMember;
return event;
}
diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx
index f10884ce9d..8d411c5f6c 100644
--- a/src/components/views/elements/MemberEventListSummary.tsx
+++ b/src/components/views/elements/MemberEventListSummary.tsx
@@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { ReactChildren } from 'react';
+import React, { ComponentProps } from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
@@ -26,21 +26,11 @@ import { isValid3pidInvite } from "../../../RoomInvite";
import EventListSummary from "./EventListSummary";
import { replaceableComponent } from "../../../utils/replaceableComponent";
-interface IProps {
- // An array of member events to summarise
- events: MatrixEvent[];
+interface IProps extends Omit, "summaryText" | "summaryMembers"> {
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
summaryLength?: number;
// The maximum number of avatars to display in the summary
avatarsMaxLength?: number;
- // The minimum number of events needed to trigger summarisation
- threshold?: number,
- // Whether or not to begin with state.expanded=true
- startExpanded?: boolean,
- // An array of EventTiles to render when expanded
- children: ReactChildren;
- // Called when the MELS expansion is toggled
- onToggle?(): void,
}
interface IUserEvents {
diff --git a/src/components/views/messages/DateSeparator.js b/src/components/views/messages/DateSeparator.tsx
similarity index 82%
rename from src/components/views/messages/DateSeparator.js
rename to src/components/views/messages/DateSeparator.tsx
index 82ce8dc4ae..5d43e2182d 100644
--- a/src/components/views/messages/DateSeparator.js
+++ b/src/components/views/messages/DateSeparator.tsx
@@ -1,6 +1,6 @@
/*
-Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
+Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,12 +16,12 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import { _t } from '../../../languageHandler';
-import {formatFullDateNoTime} from '../../../DateUtils';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
-function getdaysArray() {
+import { _t } from '../../../languageHandler';
+import { formatFullDateNoTime } from '../../../DateUtils';
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+
+function getDaysArray(): string[] {
return [
_t('Sunday'),
_t('Monday'),
@@ -33,17 +33,17 @@ function getdaysArray() {
];
}
-@replaceableComponent("views.messages.DateSeparator")
-export default class DateSeparator extends React.Component {
- static propTypes = {
- ts: PropTypes.number.isRequired,
- };
+interface IProps {
+ ts: number;
+}
- getLabel() {
+@replaceableComponent("views.messages.DateSeparator")
+export default class DateSeparator extends React.Component {
+ private getLabel() {
const date = new Date(this.props.ts);
const today = new Date();
const yesterday = new Date();
- const days = getdaysArray();
+ const days = getDaysArray();
yesterday.setDate(today.getDate() - 1);
if (date.toDateString() === today.toDateString()) {
diff --git a/src/components/views/messages/TileErrorBoundary.js b/src/components/views/messages/TileErrorBoundary.tsx
similarity index 77%
rename from src/components/views/messages/TileErrorBoundary.js
rename to src/components/views/messages/TileErrorBoundary.tsx
index 0e9a7b6128..967127d275 100644
--- a/src/components/views/messages/TileErrorBoundary.js
+++ b/src/components/views/messages/TileErrorBoundary.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,14 +16,24 @@ limitations under the License.
import React from 'react';
import classNames from 'classnames';
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+
import { _t } from '../../../languageHandler';
-import * as sdk from '../../../index';
import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import BugReportDialog from '../dialogs/BugReportDialog';
+
+interface IProps {
+ mxEvent: MatrixEvent;
+}
+
+interface IState {
+ error: Error;
+}
@replaceableComponent("views.messages.TileErrorBoundary")
-export default class TileErrorBoundary extends React.Component {
+export default class TileErrorBoundary extends React.Component {
constructor(props) {
super(props);
@@ -32,17 +42,13 @@ export default class TileErrorBoundary extends React.Component {
};
}
- static getDerivedStateFromError(error) {
+ static getDerivedStateFromError(error: Error): Partial {
// Side effects are not permitted here, so we only update the state so
// that the next render shows an error message.
return { error };
}
- _onBugReport = () => {
- const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
- if (!BugReportDialog) {
- return;
- }
+ private onBugReport = (): void => {
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
label: 'react-soft-crash-tile',
});
@@ -60,7 +66,7 @@ export default class TileErrorBoundary extends React.Component {
let submitLogsButton;
if (SdkConfig.get().bug_report_endpoint_url) {
- submitLogsButton =
+ submitLogsButton =
{_t("Submit logs")}
;
}
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index 3d674efe04..6c306904f5 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
+import React, { createRef } from 'react';
import classNames from "classnames";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
@@ -176,12 +176,19 @@ const MAX_READ_AVATARS = 5;
// | '--------------------------------------' |
// '----------------------------------------------------------'
-interface IReadReceiptProps {
+export interface IReadReceiptProps {
userId: string;
roomMember: RoomMember;
ts: number;
}
+export enum TileShape {
+ Notif = "notif",
+ FileGrid = "file_grid",
+ Reply = "reply",
+ ReplyPreview = "reply_preview",
+}
+
interface IProps {
// the MatrixEvent to show
mxEvent: MatrixEvent;
@@ -248,7 +255,7 @@ interface IProps {
// It could also be done by subclassing EventTile, but that'd be quite
// boiilerplatey. So just make the necessary render decisions conditional
// for now.
- tileShape?: 'notif' | 'file_grid' | 'reply' | 'reply_preview';
+ tileShape?: TileShape;
// show twelve hour timestamps
isTwelveHour?: boolean;
@@ -306,10 +313,11 @@ interface IState {
export default class EventTile extends React.Component {
private suppressReadReceiptAnimation: boolean;
private isListeningForReceipts: boolean;
- private ref: React.RefObject;
private tile = React.createRef();
private replyThread = React.createRef();
+ public readonly ref = createRef();
+
static defaultProps = {
// no-op function because onHeightChanged is optional yet some sub-components assume its existence
onHeightChanged: function() {},
@@ -345,8 +353,6 @@ export default class EventTile extends React.Component {
// to determine if we've already subscribed and use a combination of other flags to find
// out if we should even be subscribed at all.
this.isListeningForReceipts = false;
-
- this.ref = React.createRef();
}
/**
From 3d6c6cea89e3b4a6b5483217468ad993e0439504 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 24 Jun 2021 16:43:12 +0100
Subject: [PATCH 16/52] Fix AutoHideScrollbar typing
---
src/components/structures/AutoHideScrollbar.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx
index c5c9f0a518..e8a9872b48 100644
--- a/src/components/structures/AutoHideScrollbar.tsx
+++ b/src/components/structures/AutoHideScrollbar.tsx
@@ -17,7 +17,7 @@ limitations under the License.
import React, { HTMLAttributes, WheelEvent } from "react";
-interface IProps extends HTMLAttributes {
+interface IProps extends Omit, "onScroll"> {
className?: string;
onScroll?: (event: Event) => void;
onWheel?: (event: WheelEvent) => void;
From 4492627401acd75bc0db3d73c88eb68baada9f42 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 24 Jun 2021 19:20:02 +0100
Subject: [PATCH 17/52] More js-sdk type consolidation
---
src/BasePlatform.ts | 2 +-
src/Lifecycle.ts | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts
index 5483ea6874..82eb67a2d6 100644
--- a/src/BasePlatform.ts
+++ b/src/BasePlatform.ts
@@ -348,7 +348,7 @@ export default abstract class BasePlatform {
/**
* Create and store a pickle key for encrypting libolm objects.
* @param {string} userId the user ID for the user that the pickle key is for.
- * @param {string} userId the device ID that the pickle key is for.
+ * @param {string} deviceId the device ID that the pickle key is for.
* @returns {string|null} the pickle key, or null if the platform does not
* support storing pickle keys.
*/
diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts
index b0a1292ba1..dd19e4ea16 100644
--- a/src/Lifecycle.ts
+++ b/src/Lifecycle.ts
@@ -20,7 +20,7 @@ limitations under the License.
import { createClient } from 'matrix-js-sdk/src/matrix';
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { MatrixClient } from "matrix-js-sdk/src/client";
-import {decryptAES, encryptAES} from "matrix-js-sdk/src/crypto/aes";
+import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes";
import {IMatrixClientCreds, MatrixClientPeg} from './MatrixClientPeg';
import SecurityCustomisations from "./customisations/Security";
@@ -303,7 +303,7 @@ export interface IStoredSession {
hsUrl: string;
isUrl: string;
hasAccessToken: boolean;
- accessToken: string | object;
+ accessToken: string | IEncryptedPayload;
userId: string;
deviceId: string;
isGuest: boolean;
@@ -346,11 +346,11 @@ export async function getStoredSessionVars(): Promise {
isGuest = localStorage.getItem("matrix-is-guest") === "true";
}
- return {hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest};
+ return { hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest };
}
// The pickle key is a string of unspecified length and format. For AES, we
-// need a 256-bit Uint8Array. So we HKDF the pickle key to generate the AES
+// need a 256-bit Uint8Array. So we HKDF the pickle key to generate the AES
// key. The AES key should be zeroed after it is used.
async function pickleKeyToAesKey(pickleKey: string): Promise {
const pickleKeyBuffer = new Uint8Array(pickleKey.length);
From 231b40473b991cbb58df82ea13f7466041a07d7d Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 24 Jun 2021 21:22:56 +0100
Subject: [PATCH 18/52] fix the upset CI
---
.../views/dialogs/security/CreateCrossSigningDialog.tsx | 4 +++-
src/stores/SetupEncryptionStore.ts | 7 ++++---
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
index 840390f6fb..e16dc448c5 100644
--- a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
+++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
@@ -16,6 +16,8 @@ limitations under the License.
*/
import React from 'react';
+import { CrossSigningKeys } from 'matrix-js-sdk/src/client';
+
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import { _t } from '../../../../languageHandler';
import Modal from '../../../../Modal';
@@ -71,7 +73,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
try {
- await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {});
+ await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {} as CrossSigningKeys);
// We should never get here: the server should always require
// UI auth to upload device signing keys. If we do, we upload
// no keys which would be a no-op.
diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts
index 88385d0399..b0a0e8f265 100644
--- a/src/stores/SetupEncryptionStore.ts
+++ b/src/stores/SetupEncryptionStore.ts
@@ -16,11 +16,12 @@ limitations under the License.
import EventEmitter from 'events';
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
-import { IKeyBackupVersion } from "matrix-js-sdk/src/crypto/keybackup";
+import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/matrix";
+import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
+
import { MatrixClientPeg } from '../MatrixClientPeg';
import { accessSecretStorage, AccessCancelledError } from '../SecurityManager';
-import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
export enum Phase {
Loading = 0,
@@ -35,7 +36,7 @@ export class SetupEncryptionStore extends EventEmitter {
private started: boolean;
public phase: Phase;
public verificationRequest: VerificationRequest;
- public backupInfo: IKeyBackupVersion;
+ public backupInfo: IKeyBackupInfo;
public keyId: string;
public keyInfo: ISecretStorageKeyInfo;
public hasDevicesToVerifyAgainst: boolean;
From 3f104205036446eb87f2fca93e0df5699ed99088 Mon Sep 17 00:00:00 2001
From: libexus
Date: Mon, 21 Jun 2021 18:35:35 +0000
Subject: [PATCH 19/52] Translated using Weblate (German)
Currently translated at 99.6% (2983 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/
---
src/i18n/strings/de_DE.json | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 90530b5b07..2cc68125d7 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -1211,7 +1211,7 @@
"Send messages": "Nachrichten senden",
"Invite users": "Benutzer einladen",
"Change settings": "Einstellungen ändern",
- "Kick users": "Benutzer rauswerfen",
+ "Kick users": "Benutzer kicken",
"Ban users": "Benutzer verbannen",
"Remove messages": "Nachrichten löschen",
"Notify everyone": "Jeden benachrichtigen",
@@ -1723,7 +1723,7 @@
"Uploaded sound": "Hochgeladener Ton",
"Upgrade this room to the recommended room version": "Aktualisiere diesen Raum auf die empfohlene Raumversion",
"this room": "Dieser Raum",
- "View older messages in %(roomName)s.": "Zeige alte Nachrichten in %(roomName)s.",
+ "View older messages in %(roomName)s.": "Alte Nachrichten in %(roomName)s anzeigen.",
"Send a bug report with logs": "Einen Fehlerbericht mit der Protokolldatei senden",
"Verify all your sessions to ensure your account & messages are safe": "Verifiziere alle deine Sitzungen, um dein Konto und deine Nachrichten zu schützen",
"Verify your other session using one of the options below.": "Verifiziere deine andere Sitzung mit einer der folgenden Optionen.",
@@ -3242,7 +3242,7 @@
"Confirm abort of host creation": "Bestätige das Beenden der Host-Erstellung",
"Are you sure you wish to abort creation of the host? The process cannot be continued.": "Soll die Host-Erstellung wirklich beendet werden? Dieser Prozess kann nicht wieder fortgesetzt werden.",
"Invite to just this room": "Nur in diesen Raum einladen",
- "Consult first": "Konsultiere zuerst",
+ "Consult first": "Zuerst Anfragen",
"Reset event store?": "Ereignisspeicher zurück setzen?",
"You most likely do not want to reset your event index store": "Es ist wahrscheinlich, dass du den Ereignis-Indexspeicher nicht zurück setzen möchtest",
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Falls du dies tust, werden keine deiner Nachrichten gelöscht. Allerdings wird die Such-Funktion eine Weile lang schlecht funktionieren, bis der Index wieder hergestellt ist",
@@ -3256,7 +3256,7 @@
"You are the only person here. If you leave, no one will be able to join in the future, including you.": "Du bist die einzige Person hier. Wenn du ihn jetzt verlässt, ist er für immer verloren (eine lange Zeit).",
"Edit settings relating to your space.": "Einstellungen vom Space bearbeiten.",
"Please choose a strong password": "Bitte gib ein sicheres Passwort ein",
- "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Wenn du alles zurücksetzt, gehen alle verifizierten Anmeldungen, Benutzer und verschlüsselte Nachrichten verloren.",
+ "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Wenn du alles zurücksetzt, gehen alle verifizierten Anmeldungen, Benutzer und vergangenen Nachrichten verloren.",
"Only do this if you have no other device to complete verification with.": "Verwende es nur, wenn du kein Gerät, mit dem du dich verifizieren, kannst bei dir hast.",
"Reset everything": "Alles zurücksetzen",
"Forgotten or lost all recovery methods? Reset all": "Hast du alle Wiederherstellungsmethoden vergessen? Setze sie hier zurück",
@@ -3282,12 +3282,12 @@
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "Falls du es wirklich willst: Es werden keine Nachrichten gelöscht. Außerdem wird die Suche, während der Index erstellt wird, etwas langsamer sein",
"%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s Mitglieder inklusive %(commaSeparatedMembers)s",
"Including %(commaSeparatedMembers)s": "Inklusive%(commaSeparatedMembers)s",
- "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Beratung mit %(transferTarget)s. Übertragung zu %(transferee)s",
+ "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "%(transferTarget)s wird angefragt. Übertragung zu %(transferee)s",
"Play": "Abspielen",
"Pause": "Pause",
"What do you want to organise?": "Was willst du organisieren?",
"Enter your Security Phrase a second time to confirm it.": "Gib dein Kennwort ein zweites Mal zur Bestätigung ein.",
- "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Wähle Räume oder Konversationen die Du hinzufügen möchtest. Dieser Bereich ist nur für Dich, niemand wird informiert. Du kannst später mehr hinzufügen.",
+ "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Wähle Räume oder Konversationen die du hinzufügen willst. Dieser Space ist nur für dich, niemand wird informiert. Du kannst später mehr hinzufügen.",
"Filter all spaces": "Alle Spaces durchsuchen",
"Delete recording": "Aufnahme löschen",
"Stop the recording": "Aufnahme stoppen",
@@ -3349,7 +3349,7 @@
"Currently joining %(count)s rooms|other": "Betrete %(count)s Räume",
"Go to my space": "Zu meinem Space",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Überprüfe auf Tippfehler oder verwende andere Suchbegriffe. Beachte, dass Ergebnisse aus privaten Räumen, in die du nicht eingeladen wurdest, nicht angezeigt werden.",
- "See when people join, leave, or are invited to this room": "Anzeigen, wenn Leute eingeladen werden oder den Raum betreten und verlassen",
+ "See when people join, leave, or are invited to this room": "Anzeigen, wenn Leute eingeladen werden, den Raum betreten oder verlassen",
"The user you called is busy.": "Der angerufene Benutzer ist momentan beschäftigt.",
"User Busy": "Benutzer beschäftigt",
"No results for \"%(query)s\"": "Keine Ergebnisse für \"%(query)s\"",
@@ -3368,5 +3368,7 @@
"Pinned messages": "Angeheftete Nachrichten",
"Nothing pinned, yet": "Es ist nichts angepinnt. Noch nicht.",
"End-to-end encryption isn't enabled": "Ende-zu-Ende-Verschlüsselung ist deaktiviert",
- "See when people join, leave, or are invited to your active room": "Anzeigen, wenn Leute den aktuellen Raum betreten, verlassen oder in ihn eingeladen werden"
+ "See when people join, leave, or are invited to your active room": "Anzeigen, wenn Leute den aktuellen Raum betreten, verlassen oder in ihn eingeladen werden",
+ "Teammates might not be able to view or join any private rooms you make.": "Mitglieder werden private Räume möglicherweise weder sehen noch betreten können.",
+ "Error - Mixed content": "Fehler - Uneinheitlicher Inhalt"
}
From 2ff45339f756b34dcab4aaa2c428c80b85f77d24 Mon Sep 17 00:00:00 2001
From: Thibault Martin
Date: Thu, 24 Jun 2021 07:15:18 +0000
Subject: [PATCH 20/52] Translated using Weblate (French)
Currently translated at 100.0% (2993 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/
---
src/i18n/strings/fr.json | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json
index e199e094e7..16373f0853 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -3358,5 +3358,22 @@
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Essayez d'autres mots ou vérifiez les fautes de frappe. Certains salons peuvent ne pas être visibles car ils sont privés et vous devez être invité pour les rejoindre.",
"No results for \"%(query)s\"": "Aucun résultat pour « %(query)s »",
"The user you called is busy.": "L’utilisateur que vous avez appelé est indisponible.",
- "User Busy": "Utilisateur indisponible"
+ "User Busy": "Utilisateur indisponible",
+ "We're working on this as part of the beta, but just want to let you know.": "Nous travaillons sur cette partie en bêta, mais nous voulions vous en faire part.",
+ "Teammates might not be able to view or join any private rooms you make.": "Votre équipe pourrait ne pas voir ou rejoindre les salons privés que vous créez.",
+ "Or send invite link": "Ou envoyer le lien d’invitation",
+ "If you can't see who you’re looking for, send them your invite link below.": "Si vous ne trouvez pas la personne que vous cherchez, envoyez leur le lien d’invitation ci-dessous.",
+ "Some suggestions may be hidden for privacy.": "Certaines suggestions pourraient être masquées pour votre confidentialité.",
+ "Search for rooms or people": "Rechercher des salons ou des gens",
+ "Forward message": "Transférer le message",
+ "Open link": "Ouvrir le lien",
+ "Sent": "Envoyé",
+ "You don't have permission to do this": "Vous n’avez pas les permissions nécessaires pour effectuer cette action",
+ "Error - Mixed content": "Erreur - Contenu mixte",
+ "Error loading Widget": "Erreur lors du chargement du widget",
+ "Pinned messages": "Messages épinglés",
+ "If you have permissions, open the menu on any message and select Pin to stick them here.": "Si vous avez les permissions, ouvrez le menu de n’importe quel message et sélectionnez Épingler pour les afficher ici.",
+ "Nothing pinned, yet": "Rien d’épinglé, pour l’instant",
+ "End-to-end encryption isn't enabled": "Le chiffrement de bout en bout n’est pas activé",
+ "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Vous messages privés sont normalement chiffrés, mais ce salon ne l’est pas. Ceci est souvent du à un appareil ou une méthode qui ne le prend pas en charge, comme les invitations par e-mail. Activer le chiffrement dans les paramètres."
}
From 628c91485bfc5f79e04bc341e8139d2e3998c569 Mon Sep 17 00:00:00 2001
From: rkfg
Date: Thu, 24 Jun 2021 13:11:53 +0000
Subject: [PATCH 21/52] Translated using Weblate (Russian)
Currently translated at 94.9% (2842 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ru/
---
src/i18n/strings/ru.json | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index da42347b49..91b9919d0a 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -272,7 +272,7 @@
"Server may be unavailable, overloaded, or you hit a bug.": "Возможно, сервер недоступен, перегружен или случилась ошибка.",
"Server unavailable, overloaded, or something else went wrong.": "Возможно, сервер недоступен, перегружен или что-то еще пошло не так.",
"Session ID": "ID сессии",
- "%(senderName)s set a profile picture.": "%(senderName)s установил себе аватар.",
+ "%(senderName)s set a profile picture.": "%(senderName)s установил(а) себе аватар.",
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s изменил(а) отображаемое имя на %(displayName)s.",
"Signed Out": "Выполнен выход",
"This room is not accessible by remote Matrix servers": "Это комната недоступна из других серверов Matrix",
@@ -853,7 +853,7 @@
"Sets the room name": "Устанавливает название комнаты",
"Forces the current outbound group session in an encrypted room to be discarded": "Принудительно отбрасывает текущую групповую сессию для отправки сообщений в зашифрованную комнату",
"%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s модернизировал эту комнату.",
- "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s установил %(address)s в качестве главного адреса комнаты.",
+ "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s установил(а) %(address)s в качестве главного адреса комнаты.",
"%(senderName)s removed the main address for this room.": "%(senderName)s удалил главный адрес комнаты.",
"%(displayName)s is typing …": "%(displayName)s печатает…",
"%(names)s and %(count)s others are typing …|other": "%(names)s и %(count)s других печатают…",
@@ -3211,5 +3211,13 @@
"Failed to remove some rooms. Try again later": "Не удалось удалить несколько комнат. Попробуйте позже",
"%(count)s rooms and 1 space|one": "%(count)s комната и одно пространство",
"%(count)s rooms and 1 space|other": "%(count)s комнат и одно пространство",
- "Sends the given message as a spoiler": "Отправить данное сообщение под спойлером"
+ "Sends the given message as a spoiler": "Отправить данное сообщение под спойлером",
+ "Play": "Воспроизведение",
+ "Pause": "Пауза",
+ "Connecting": "Подключение",
+ "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Разрешить прямые соединения для вызовов 1:1 (при включении данной опции другая сторона может узнать ваш IP адрес)",
+ "Send and receive voice messages": "Отправлять и получать голосовые сообщения",
+ "%(deviceId)s from %(ip)s": "%(deviceId)s с %(ip)s",
+ "The user you called is busy.": "Вызываемый пользователь занят.",
+ "User Busy": "Пользователь занят"
}
From 037ac3ce8d40758384e5fee28bd61398ec0be5b6 Mon Sep 17 00:00:00 2001
From: m4sk1n
Date: Tue, 22 Jun 2021 12:55:01 +0000
Subject: [PATCH 22/52] Translated using Weblate (Polish)
Currently translated at 73.2% (2193 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pl/
---
src/i18n/strings/pl.json | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json
index 51ba733cfa..641247e6ee 100644
--- a/src/i18n/strings/pl.json
+++ b/src/i18n/strings/pl.json
@@ -2352,5 +2352,21 @@
"Converts the room to a DM": "Zmienia pokój w wiadomość bezpośrednią",
"Sends the given message as a spoiler": "Wysyła podaną wiadomość jako spoiler",
"User Busy": "Użytkownik zajęty",
- "The user you called is busy.": "Użytkownik do którego zadzwoniłeś(-aś) jest zajęty."
+ "The user you called is busy.": "Użytkownik do którego zadzwoniłeś(-aś) jest zajęty.",
+ "End-to-end encryption isn't enabled": "Szyfrowanie end-to-end nie jest włączone",
+ "Nothing pinned, yet": "Nie przypięto tu jeszcze niczego",
+ "If you have permissions, open the menu on any message and select Pin to stick them here.": "Jeżeli masz uprawnienia, przejdź do menu dowolnej wiadomości i wybierz Przypnij, aby przyczepić ją tutaj.",
+ "Pinned messages": "Przypięte wiadomości",
+ "Error loading Widget": "Błąd ładowania widżetu",
+ "Error - Mixed content": "Błąd — zawartość mieszana",
+ "You don't have permission to do this": "Nie masz uprawnień aby to zrobić",
+ "Sent": "Wysłano",
+ "Open link": "Otwórz odnośnik",
+ "Forward message": "Przekaż wiadomość",
+ "Message preview": "Podgląd wiadomości",
+ "Search for rooms or people": "Szukaj pokojów i ludzi",
+ "Some suggestions may be hidden for privacy.": "Niektóre propozycje mogą być ukryte z uwagi na prywatność.",
+ "If you can't see who you’re looking for, send them your invite link below.": "Jeżeli nie możesz zobaczyć osób, których szukasz, wyślij im poniższy odnośnik z zaproszeniem.",
+ "Or send invite link": "Lub wyślij odnośnik z zaproszeniem",
+ "We're working on this as part of the beta, but just want to let you know.": "Pracujemy nad tym w ramach bety, ale chcemy, żebyś wiedział(a)."
}
From 213cd3903033d7d9acade30462cb2a51f82f97e7 Mon Sep 17 00:00:00 2001
From: Kaede
Date: Wed, 23 Jun 2021 16:50:14 +0000
Subject: [PATCH 23/52] Translated using Weblate (Japanese)
Currently translated at 77.5% (2322 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/
---
src/i18n/strings/ja.json | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json
index 495e240051..180d63f33e 100644
--- a/src/i18n/strings/ja.json
+++ b/src/i18n/strings/ja.json
@@ -2502,5 +2502,6 @@
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "部屋や会話を追加できます。これはあなた専用のスペースで、他の人からは見えません。後から部屋や会話を追加することもできます。",
"Support": "サポート",
"You can change these anytime.": "ここで入力した情報はいつでも編集できます。",
- "Add some details to help people recognise it.": "情報を入力してください。"
+ "Add some details to help people recognise it.": "情報を入力してください。",
+ "View dev tools": "開発者ツールを表示"
}
From 4ce5df2fb52caa43a489d67935eed1a7002d29dc Mon Sep 17 00:00:00 2001
From: Ridhubharan
Date: Tue, 22 Jun 2021 17:24:23 +0000
Subject: [PATCH 24/52] Translated using Weblate (Tamil)
Currently translated at 6.7% (202 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ta/
---
src/i18n/strings/ta.json | 86 ++++++++++++++++++++++++++++++++--------
1 file changed, 69 insertions(+), 17 deletions(-)
diff --git a/src/i18n/strings/ta.json b/src/i18n/strings/ta.json
index 4f87230ef3..9dc89b1b94 100644
--- a/src/i18n/strings/ta.json
+++ b/src/i18n/strings/ta.json
@@ -21,7 +21,7 @@
"Enable email notifications": "மின்னஞ்சல் அறிவிப்புகளை ஏதுவாக்கு",
"Enable notifications for this account": "இந்த கணக்கிற்கான அறிவிப்புகளை ஏதுவாக்கு",
"Enable them now": "இப்போது அவற்றை ஏதுவாக்கு",
- "Error": "கோளாறு",
+ "Error": "பிழை",
"Failed to add tag %(tagName)s to room": "%(tagName)s எனும் குறிச்சொல்லை அறையில் சேர்ப்பதில் தோல்வி",
"Failed to change settings": "அமைப்புகள் மாற்றத்தில் தோல்வி",
"Failed to forget room %(errCode)s": "அறையை மறப்பதில் தோல்வி %(errCode)s",
@@ -125,17 +125,17 @@
"Register": "பதிவு செய்",
"Rooms": "அறைகள்",
"Add rooms to this community": "அறைகளை இந்த சமூகத்தில் சேர்க்கவும்",
- "This email address is already in use": "இந்த மின் அஞ்சல் முகவரி ஏற்கனவே பயன்பாட்டில் உள்ளது",
- "This phone number is already in use": "இந்த தொலைபேசி எண் ஏற்கனவே பயன்பாட்டில் உள்ளது",
- "Failed to verify email address: make sure you clicked the link in the email": "மின்னஞ்சல் முகவரியைச் சரிபார்க்கத் தவறிவிட்டது: மின்னஞ்சலில் உள்ள இணைப்பைக் கிளிக் செய்துள்ளீர்கள் என்பதை உறுதிப்படுத்திக் கொள்ளுங்கள்",
+ "This email address is already in use": "இந்த மின்னஞ்சல் முகவரி முன்னதாகவே பயன்பாட்டில் உள்ளது",
+ "This phone number is already in use": "இந்த தொலைபேசி எண் முன்னதாகவே பயன்பாட்டில் உள்ளது",
+ "Failed to verify email address: make sure you clicked the link in the email": "மின்னஞ்சல் முகவரியை சரிபார்க்க முடியவில்லை: மின்னஞ்சலில் உள்ள இணைப்பை அழுத்தியுள்ளீர்களா என்பதை உறுதிப்படுத்தவும்",
"The platform you're on": "நீங்கள் இருக்கும் தளம்",
"The version of %(brand)s": "%(brand)s இன் பதிப்பு",
"Whether or not you're logged in (we don't record your username)": "நீங்கள் உள்நுழைந்திருந்தாலும் இல்லாவிட்டாலும் (உங்கள் பயனர்பெயரை நாங்கள் பதிவு செய்ய மாட்டோம்)",
"Your language of choice": "நீங்கள் விரும்பும் மொழி",
- "Which officially provided instance you are using, if any": "நீங்கள் பயன்படுத்தும் அதிகாரப்பூர்வமாக வழங்கப்பட்ட உதாரணம் ஏதேனும் இருந்தால்",
- "Whether or not you're using the Richtext mode of the Rich Text Editor": "பணக்கார உரை எடிட்டரின் ரிச்ச்டெக்ஸ்ட் பயன்முறையைப் பயன்படுத்துகிறீர்களா இல்லையா",
- "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "நீங்கள் 'breadcrumbs' அம்சத்தைப் பயன்படுத்துகிறீர்களோ இல்லையோ (அறை பட்டியலுக்கு மேலே உள்ள அவதாரங்கள்)",
- "Your homeserver's URL": "உங்கள் வீட்டு சேவையகத்தின் URL",
+ "Which officially provided instance you are using, if any": "நீங்கள் பயன்படுத்தும் அதிகாரப்பூர்வமாக வழங்கப்பட்ட நிகழ்வு ஏதேனும் இருந்தால்",
+ "Whether or not you're using the Richtext mode of the Rich Text Editor": "நீங்கள் ரிச்டெக்ஸ்ட் உரைத்திருத்தியின் ரிச்டெக்ஸ்ட் பயன்முறையைப் பயன்படுத்துகிறீர்களா இல்லையா",
+ "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "நீங்கள் 'breadcrumbs' அம்சத்தைப் பயன்படுத்துகிறீர்களோ இல்லையோ (அறை பட்டியலுக்கு மேலே உள்ள பயனர் படங்கள்)",
+ "Your homeserver's URL": "உங்கள் வீட்டுச்சேவையகத்தின் URL",
"e.g. %(exampleValue)s": "உதாரணமாக %(exampleValue)s",
"Every page you use in the app": "பயன்பாட்டில் நீங்கள் பயன்படுத்தும் ஒவ்வொரு பக்கமும்",
"Your %(brand)s is misconfigured": "உங்கள் %(brand)s தவறாக உள்ளமைக்கப்பட்டுள்ளது",
@@ -143,7 +143,7 @@
"e.g. ": "உதாரணமாக ",
"Your device resolution": "உங்கள் சாதனத் தீர்மானம்",
"Analytics": "பகுப்பாய்வு",
- "The information being sent to us to help make %(brand)s better includes:": "%(brand)s ஐ சிறப்பாகச் செய்ய எங்களுக்கு அனுப்பப்படும் தகவல்களில் பின்வருவன அடங்கும்:",
+ "The information being sent to us to help make %(brand)s better includes:": "%(brand)s ஐ சிறப்பாக்க எங்களுக்கு அனுப்பப்படும் தகவல்களில் பின்வருவன அடங்கும்:",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "இந்த பக்கம் ஒரு அறை, பயனர் அல்லது குழு ஐடி போன்ற அடையாளம் காணக்கூடிய தகவல்களை உள்ளடக்கியது, அந்த தரவு சேவையகத்திற்கு அனுப்பப்படுவதற்கு முன்பு அகற்றப்படும்.",
"Call Failed": "அழைப்பு தோல்வியுற்றது",
"Call Timeout": "அழைப்பு நேரம் முடிந்தது",
@@ -152,19 +152,19 @@
"Existing Call": "இருக்கும் அழைப்பு",
"You are already in a call.": "நீங்கள் ஏற்கனவே அழைப்பில் உள்ளீர்கள்.",
"VoIP is unsupported": "VoIP ஆதரிக்கப்படவில்லை",
- "You cannot place VoIP calls in this browser.": "இந்த உலாவியில் நீங்கள் VoIP அழைப்புகளை வைக்க முடியாது.",
- "You cannot place a call with yourself.": "உங்களுடன் அழைப்பை மேற்கொள்ள முடியாது.",
- "Call in Progress": "அழைப்பு முன்னேற்றத்தில் உள்ளது",
- "A call is currently being placed!": "தற்போது அழைப்பு வைக்கப்பட்டுள்ளது!",
+ "You cannot place VoIP calls in this browser.": "இந்த உலாவியில் நீங்கள் VoIP அழைப்புகளை மேற்கொள்ள முடியாது.",
+ "You cannot place a call with yourself.": "நீங்கள் உங்களுடனே அழைப்பை மேற்கொள்ள முடியாது.",
+ "Call in Progress": "அழைப்பு நடந்துகொண்டிருக்கிறது",
+ "A call is currently being placed!": "தற்போது ஒரு அழைப்பு செயல்படுத்தப்பட்டுள்ளது!",
"A call is already in progress!": "அழைப்பு ஏற்கனவே செயலில் உள்ளது!",
"Permission Required": "அனுமதி தேவை",
- "You do not have permission to start a conference call in this room": "இந்த அறையில் ஒரு மாநாட்டு அழைப்பைத் தொடங்க உங்களுக்கு அனுமதி இல்லை",
+ "You do not have permission to start a conference call in this room": "இந்த அறையில் ஒரு கூட்டு அழைப்பைத் தொடங்க உங்களுக்கு அனுமதி இல்லை",
"Replying With Files": "கோப்புகளுடன் பதிலளித்தல்",
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "இந்த நேரத்தில் ஒரு கோப்புடன் பதிலளிக்க முடியாது. பதிலளிக்காமல் இந்த கோப்பை பதிவேற்ற விரும்புகிறீர்களா?",
"The file '%(fileName)s' failed to upload.": "'%(fileName)s' கோப்பு பதிவேற்றத் தவறிவிட்டது.",
- "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "'%(fileName)s' கோப்பு பதிவேற்றங்களுக்கான இந்த ஹோம்சர்வரின் அளவு வரம்பை மீறுகிறது",
+ "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "'%(fileName)s' கோப்பு இந்த வீட்டுச்சேவையகத்தின் பதிவேற்றங்களுக்கான அளவு வரம்பை மீறுகிறது",
"Upload Failed": "பதிவேற்றம் தோல்வியுற்றது",
- "Server may be unavailable, overloaded, or you hit a bug.": "சேவையகம் கிடைக்காமல் போகலாம், அதிக சுமை அல்லது பிழையைத் தாக்கலாம்.",
+ "Server may be unavailable, overloaded, or you hit a bug.": "",
"The server does not support the room version specified.": "குறிப்பிடப்பட்ட அறை பதிப்பை சேவையகம் ஆதரிக்கவில்லை.",
"Failure to create room": "அறையை உருவாக்கத் தவறியது",
"Sun": "ஞாயிறு",
@@ -181,5 +181,57 @@
"May": "மே",
"Jun": "ஜூன்",
"Explore rooms": "அறைகளை ஆராயுங்கள்",
- "Create Account": "உங்கள் கணக்கை துவங்குங்கள்"
+ "Create Account": "உங்கள் கணக்கை துவங்குங்கள்",
+ "Who would you like to add to this community?": "இந்த சமூகத்தில் யாரைச் சேர்க்க விரும்புகிறீர்கள்?",
+ "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
+ "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
+ "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
+ "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
+ "AM": "காலை",
+ "PM": "மாலை",
+ "Dec": "டிசம்பர்",
+ "Nov": "நவம்பர்",
+ "Oct": "அக்டோபர்",
+ "Sep": "செப்டம்பர்",
+ "Aug": "ஆகஸ்ட்",
+ "Jul": "ஜூலை",
+ "This will end the conference for everyone. Continue?": "இது அனைவருக்கும் கூட்டு அழைப்பை நிறுத்திவிடும். தொடரவா?",
+ "End conference": "கூட்டு அழைப்பை நிறுத்து",
+ "There was an error looking up the phone number": "தொலைபேசி எண்ணைத் தேடுவதில் பிழை ஏற்பட்டது",
+ "Unable to look up phone number": "தொலைபேசி எண்ணைத் தேட முடியவில்லை",
+ "You're already in a call with this person.": "நீங்கள் முன்னதாகவே இந்த நபருடன் அழைப்பில் உள்ளீர்கள்.",
+ "Already in call": "முன்னதாகவே அழைப்பில் உள்ளது",
+ "You've reached the maximum number of simultaneous calls.": "ஒரே நேரத்தில் அழைக்கக்கூடிய அதிகபட்ச அழைப்புகளை நீங்கள் அடைந்துவிட்டீர்கள்.",
+ "Too Many Calls": "மிக அதிக அழைப்புகள்",
+ "No other application is using the webcam": "வேறு எந்த பயன்பாடும் புகைப்படக்கருவியைப் பயன்படுத்துவதில்லை",
+ "Permission is granted to use the webcam": "புகைப்படக்கருவியைப் பயன்படுத்த அனுமதி வழங்கப்பட்டுள்ளது",
+ "A microphone and webcam are plugged in and set up correctly": "ஒரு ஒலிவாங்கி மற்றும் புகைப்படக்கருவி செருகப்பட்டு சரியாக அமைக்கப்பட்டுள்ளது",
+ "Call failed because webcam or microphone could not be accessed. Check that:": "புகைப்படக்கருவி அல்லது ஒலிவாங்கியை அணுக முடியாததால் அழைப்பு தோல்வியடைந்தது. அதை சரிபார்க்கவும்:",
+ "Unable to access webcam / microphone": "புகைப்படக்கருவி / ஒலிவாங்கியை அணுக முடியவில்லை",
+ "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "ஒலிவாங்கியை அணுக முடியாததால் அழைப்பு தோல்வியடைந்தது. ஒலிவாங்கி செருகப்பட்டுள்ளதா, சரியாக அமைக்கவும் என சரிபார்க்கவும்.",
+ "Unable to access microphone": "ஒலிவாங்கியை அணுக முடியவில்லை",
+ "Try using turn.matrix.org": "turn.matrix.org ஐப் பயன்படுத்த முயற்சிக்கவும்",
+ "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "மாற்றாக, நீங்கள் பொது சேவையகத்தை turn.matrix.org பயன்படுத்தி முயற்சி செய்யலாம், ஆனால் இது அவ்வளவு நம்பகமானதாக இருக்காது, மேலும் அது உங்கள் ஐபி முகவரியை அந்த சேவையகத்துடன் பகிர்ந்து கொள்ளும். இதை அமைப்புகளிலும் நிர்வகிக்கலாம்.",
+ "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "அழைப்புகள் நம்பத்தகுந்த வகையில் இயங்குவதற்காக, TURN சேவையகத்தை உள்ளமைக்க உங்கள் வீட்டுசேவையகத்தின் (%(homeserverDomain)s) நிர்வாகியிடம் கேளுங்கள்.",
+ "Call failed due to misconfigured server": "தவறாக உள்ளமைக்கப்பட்ட சேவையகம் காரணமாக அழைப்பு தோல்வியடைந்தது",
+ "The call was answered on another device.": "அழைப்பு மற்றொரு சாதனத்தில் பதிலளிக்கப்பட்டது.",
+ "Answered Elsewhere": "வேறு எங்கோ பதிலளிக்கப்பட்டது",
+ "The call could not be established": "அழைப்பை நிறுவ முடியவில்லை",
+ "The other party declined the call.": "மற்றவர் அழைப்பை மறுத்துவிட்டார்.",
+ "Call Declined": "அழைப்பு மறுக்கப்பட்டது",
+ "Unable to load! Check your network connectivity and try again.": "ஏற்ற முடியவில்லை! உங்கள் பிணைய இணைப்பைச் சரிபார்த்து மீண்டும் முயற்சிக்கவும்.",
+ "Your user agent": "உங்கள் பயனர் முகவர்",
+ "Whether you're using %(brand)s as an installed Progressive Web App": "நிறுவப்பட்ட முற்போக்கான வலை பயன்பாடாக %(brand)s ஐப் பயன்படுத்துகிறீர்களா",
+ "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "தொடுதல் உள்ளீட்டு பொறிமுறை முதன்மையாக இருக்கும் சாதனத்தில் நீங்கள் %(brand)s ஐப் பயன்படுத்துகிறீர்களா",
+ "Add Phone Number": "தொலைபேசி எண்ணை சேர்க்கவும்",
+ "Click the button below to confirm adding this phone number.": "இந்த தொலைபேசி எண்ணைச் சேர்ப்பதை உறுதிப்படுத்த கீழே உள்ள பொத்தானை அழுத்தவும்.",
+ "Confirm adding phone number": "தொலைபேசி எண்ணைச் சேர்ப்பதை உறுதிப்படுத்தவும்",
+ "Confirm adding this phone number by using Single Sign On to prove your identity.": "உங்கள் அடையாளத்தை நிரூபிக்க ஒற்றை உள்நுழைவைப் பயன்படுத்தி இந்த தொலைபேசி எண்ணைச் சேர்ப்பதை உறுதிப்படுத்தவும்.",
+ "Add Email Address": "மின்னஞ்சல் முகவரியை சேர்க்கவும்",
+ "Confirm": "உறுதிப்படுத்தவும்",
+ "Click the button below to confirm adding this email address.": "இந்த மின்னஞ்சல் முகவரியை சேர்ப்பதை உறுதிப்படுத்த கீழே உள்ள பொத்தானை அழுத்தவும்.",
+ "Confirm adding email": "மின்னஞ்சலை சேர்ப்பதை உறுதிப்படுத்தவும்",
+ "Single Sign On": "ஒற்றை உள்நுழைவு",
+ "Confirm adding this email address by using Single Sign On to prove your identity.": "உங்கள் அடையாளத்தை நிரூபிக்க ஒற்றை உள்நுழைவைப் பயன்படுத்தி இந்த மின்னஞ்சல் முகவரியை சேர்ப்பதை உறுதிப்படுத்தவும்.",
+ "Use Single Sign On to continue": "தொடர ஒற்றை உள்நுழைவைப் பயன்படுத்தவும்"
}
From e539b91d581498049596d724ecd4767264b84a77 Mon Sep 17 00:00:00 2001
From: Andrei
Date: Mon, 21 Jun 2021 13:00:38 +0000
Subject: [PATCH 25/52] Translated using Weblate (Romanian)
Currently translated at 3.6% (110 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ro/
---
src/i18n/strings/ro.json | 40 +++++++++++++++++++++++++++++++++++++++-
1 file changed, 39 insertions(+), 1 deletion(-)
diff --git a/src/i18n/strings/ro.json b/src/i18n/strings/ro.json
index 062a89f2e3..6d0e5fe021 100644
--- a/src/i18n/strings/ro.json
+++ b/src/i18n/strings/ro.json
@@ -74,5 +74,43 @@
"Explore rooms": "Explorează camerele",
"Sign In": "Autentificare",
"Create Account": "Înregistare",
- "Dismiss": "Închide"
+ "Dismiss": "Închide",
+ "You've reached the maximum number of simultaneous calls.": "Ați atins numărul maxim de apeluri simultane.",
+ "Too Many Calls": "Prea multe apeluri",
+ "No other application is using the webcam": "Nicio altă aplicație nu folosește camera web",
+ "Permission is granted to use the webcam": "Permisiunea de a utiliza camera web este acordată",
+ "A microphone and webcam are plugged in and set up correctly": "Microfonul și camera web sunt conectate și configurate corect",
+ "Call failed because webcam or microphone could not be accessed. Check that:": "Apelul nu a reușit deoarece camera web sau microfonul nu au putut fi accesate. Verifică:",
+ "Unable to access webcam / microphone": "Imposibil de accesat camera web / microfonul",
+ "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Apelul nu a reușit deoarece microfonul nu a putut fi accesat. Verificați dacă un microfon este conectat și configurați corect.",
+ "Unable to access microphone": "Nu se poate accesa microfonul",
+ "OK": "OK",
+ "Try using turn.matrix.org": "Încercați să utilizați turn.matrix.org",
+ "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativ, puteți încerca să utilizați serverul public la turn.matrix.org, dar acest lucru nu va fi la fel de fiabil și va partaja adresa dvs. IP cu acel server. Puteți gestiona acest lucru și în Setări.",
+ "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Vă rugăm să cereți administratorului serverului dvs. (%(homeserverDomain)s) să configureze un server TURN pentru ca apelurile să funcționeze în mod fiabil.",
+ "Call failed due to misconfigured server": "Apelul nu a reușit din cauza serverului configurat greșit",
+ "The call was answered on another device.": "Apelul a primit răspuns pe un alt dispozitiv.",
+ "Answered Elsewhere": "A răspuns în altă parte",
+ "The call could not be established": "Apelul nu a putut fi stabilit",
+ "The user you called is busy.": "Utilizatorul pe care l-ați sunat este ocupat.",
+ "User Busy": "Utilizator ocupat",
+ "The other party declined the call.": "Cealaltă parte a refuzat apelul.",
+ "Call Declined": "Apel refuzat",
+ "Unable to load! Check your network connectivity and try again.": "Imposibil de incarcat! Verificați conectivitatea rețelei și încercați din nou.",
+ "Error": "Eroare",
+ "Your user agent": "Agentul dvs. de utilizator",
+ "Whether you're using %(brand)s as an installed Progressive Web App": "Dacă utilizați %(brand)s ca aplicație web progresivă instalată",
+ "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Dacă utilizați sau nu funcția „breadcrumbs” (avatare deasupra listei de camere)",
+ "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Dacă utilizați %(brand)s pe un dispozitiv în care atingerea este principalul mecanism de intrare",
+ "Add Phone Number": "Adaugă numărul de telefon",
+ "Click the button below to confirm adding this phone number.": "Faceți clic pe butonul de mai jos pentru a confirma adăugarea acestui număr de telefon.",
+ "Confirm adding phone number": "Confirmați adăugarea numărului de telefon",
+ "Confirm adding this phone number by using Single Sign On to prove your identity.": "Confirmați adăugarea acestui număr de telefon utilizând Single Sign On pentru a vă dovedi identitatea.",
+ "Add Email Address": "Adăugați o adresă de e-mail",
+ "Confirm": "Confirmă",
+ "Click the button below to confirm adding this email address.": "Faceți clic pe butonul de mai jos pentru a confirma adăugarea acestei adrese de e-mail.",
+ "Confirm adding email": "Confirmați adăugarea e-mailului",
+ "Single Sign On": "Single Sign On",
+ "Confirm adding this email address by using Single Sign On to prove your identity.": "Confirmați adăugarea acestei adrese de e-mail utilizând Single Sign On pentru a vă dovedi identitatea.",
+ "Use Single Sign On to continue": "Utilizați Single Sign On pentru a continua"
}
From 39cb5a7058af6ae04ae718dc1b13ca158880c7dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?=
Date: Tue, 22 Jun 2021 02:53:06 +0000
Subject: [PATCH 26/52] Translated using Weblate (Estonian)
Currently translated at 99.8% (2988 of 2993 strings)
Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/
---
src/i18n/strings/et.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 9454efd956..2b723758bc 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2229,7 +2229,7 @@
"Deactivate Account": "Deaktiveeri konto",
"Discovery": "Leia kasutajaid",
"Deactivate account": "Deaktiveeri kasutajakonto",
- "For help with using %(brand)s, click here.": "Kui otsid lisateavet %(brand)s kasutamise kohta, palun vaata siia.",
+ "For help with using %(brand)s, click here.": "Kui otsid lisateavet %(brand)s'i kasutamise kohta, palun vaata siia.",
"Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Lisa siia kasutajad ja serverid, mida sa soovid eirata. Kui soovid, et %(brand)s kasutaks üldist asendamist, siis kasuta tärni. Näiteks @bot:* eirab kõikide serverite kasutajat 'bot'.",
"Use default": "Kasuta vaikimisi väärtusi",
"Mentions & Keywords": "Mainimised ja võtmesõnad",
From 4e6260e617b6a62c6e61cb63046c90790833e760 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 25 Jun 2021 20:01:03 +0100
Subject: [PATCH 27/52] delint
---
src/components/views/elements/EventTilePreview.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
index 0696ee566e..332f3ac333 100644
--- a/src/components/views/elements/EventTilePreview.tsx
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -107,7 +107,7 @@ export default class EventTilePreview extends React.Component {
userId: this.props.userId,
getAvatarUrl: (..._) => {
return Avatar.avatarUrlForUser(
- {avatarUrl: this.props.avatarUrl},
+ { avatarUrl: this.props.avatarUrl },
AVATAR_SIZE, AVATAR_SIZE, "crop",
);
},
From 208c62048fa873c0c6e7d3ec2b26b93838c9493a Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 28 Jun 2021 15:48:13 +0100
Subject: [PATCH 28/52] Fix right panel store cleaning some state when
permalink within room is clicked
---
src/stores/RightPanelStore.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/stores/RightPanelStore.ts b/src/stores/RightPanelStore.ts
index 2bad0572b1..03f12832fd 100644
--- a/src/stores/RightPanelStore.ts
+++ b/src/stores/RightPanelStore.ts
@@ -22,6 +22,7 @@ import {RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS} from "./RightPanelStorePha
import {ActionPayload} from "../dispatcher/payloads";
import {Action} from '../dispatcher/actions';
import { SettingLevel } from "../settings/SettingLevel";
+import RoomViewStore from './RoomViewStore';
interface RightPanelStoreState {
// Whether or not to show the right panel at all. We split out rooms and groups
@@ -147,6 +148,8 @@ export default class RightPanelStore extends Store {
switch (payload.action) {
case 'view_room':
case 'view_group':
+ if (payload.room_id === RoomViewStore.getRoomId()) break; // skip this transition, probably a permalink
+
// Reset to the member list if we're viewing member info
if (MEMBER_INFO_PHASES.includes(this.state.lastRoomPhase)) {
this.setState({lastRoomPhase: RightPanelPhases.RoomMemberList, lastRoomPhaseParams: {}});
From 66072945730e5687f4c399401c62ac82c6429746 Mon Sep 17 00:00:00 2001
From: Germain Souquet
Date: Mon, 28 Jun 2021 16:30:48 +0100
Subject: [PATCH 29/52] Deprecate Tinter and TintableSVG
---
src/Tinter.js | 458 ------------------
src/components/structures/GroupView.js | 9 +-
src/components/structures/MatrixChat.tsx | 10 -
src/components/structures/MyGroups.js | 2 +-
src/components/structures/RoomView.tsx | 16 +-
src/components/views/elements/ActionButton.js | 4 +-
src/components/views/elements/AddressTile.js | 3 +-
src/components/views/elements/TintableSvg.js | 82 ----
.../views/rooms/SimpleRoomHeader.js | 4 +-
src/i18n/strings/en_EN.json | 1 -
src/settings/Settings.tsx | 8 -
.../handlers/RoomAccountSettingsHandler.ts | 16 -
src/theme.js | 2 -
13 files changed, 8 insertions(+), 607 deletions(-)
delete mode 100644 src/Tinter.js
delete mode 100644 src/components/views/elements/TintableSvg.js
diff --git a/src/Tinter.js b/src/Tinter.js
deleted file mode 100644
index ca5a460e16..0000000000
--- a/src/Tinter.js
+++ /dev/null
@@ -1,458 +0,0 @@
-/*
-Copyright 2015 OpenMarket Ltd
-Copyright 2017 New Vector Ltd
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-const DEBUG = 0;
-
-// utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue]
-function colorToRgb(color) {
- if (!color) {
- return [0, 0, 0];
- }
-
- if (color[0] === '#') {
- color = color.slice(1);
- if (color.length === 3) {
- color = color[0] + color[0] +
- color[1] + color[1] +
- color[2] + color[2];
- }
- const val = parseInt(color, 16);
- const r = (val >> 16) & 255;
- const g = (val >> 8) & 255;
- const b = val & 255;
- return [r, g, b];
- } else {
- const match = color.match(/rgb\((.*?),(.*?),(.*?)\)/);
- if (match) {
- return [
- parseInt(match[1]),
- parseInt(match[2]),
- parseInt(match[3]),
- ];
- }
- }
- return [0, 0, 0];
-}
-
-// utility to turn [red,green,blue] into #rrggbb
-function rgbToColor(rgb) {
- const val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
- return '#' + (0x1000000 + val).toString(16).slice(1);
-}
-
-class Tinter {
- constructor() {
- // The default colour keys to be replaced as referred to in CSS
- // (should be overridden by .mx_theme_accentColor and .mx_theme_secondaryAccentColor)
- this.keyRgb = [
- "rgb(118, 207, 166)", // Vector Green
- "rgb(234, 245, 240)", // Vector Light Green
- "rgb(211, 239, 225)", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
- ];
-
- // Some algebra workings for calculating the tint % of Vector Green & Light Green
- // x * 118 + (1 - x) * 255 = 234
- // x * 118 + 255 - 255 * x = 234
- // x * 118 - x * 255 = 234 - 255
- // (255 - 118) x = 255 - 234
- // x = (255 - 234) / (255 - 118) = 0.16
-
- // The colour keys to be replaced as referred to in SVGs
- this.keyHex = [
- "#76CFA6", // Vector Green
- "#EAF5F0", // Vector Light Green
- "#D3EFE1", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
- "#FFFFFF", // white highlights of the SVGs (for switching to dark theme)
- "#000000", // black lowlights of the SVGs (for switching to dark theme)
- ];
-
- // track the replacement colours actually being used
- // defaults to our keys.
- this.colors = [
- this.keyHex[0],
- this.keyHex[1],
- this.keyHex[2],
- this.keyHex[3],
- this.keyHex[4],
- ];
-
- // track the most current tint request inputs (which may differ from the
- // end result stored in this.colors
- this.currentTint = [
- undefined,
- undefined,
- undefined,
- undefined,
- undefined,
- ];
-
- this.cssFixups = [
- // { theme: {
- // style: a style object that should be fixed up taken from a stylesheet
- // attr: name of the attribute to be clobbered, e.g. 'color'
- // index: ordinal of primary, secondary or tertiary
- // },
- // }
- ];
-
- // CSS attributes to be fixed up
- this.cssAttrs = [
- "color",
- "backgroundColor",
- "borderColor",
- "borderTopColor",
- "borderBottomColor",
- "borderLeftColor",
- ];
-
- this.svgAttrs = [
- "fill",
- "stroke",
- ];
-
- // List of functions to call when the tint changes.
- this.tintables = [];
-
- // the currently loaded theme (if any)
- this.theme = undefined;
-
- // whether to force a tint (e.g. after changing theme)
- this.forceTint = false;
- }
-
- /**
- * Register a callback to fire when the tint changes.
- * This is used to rewrite the tintable SVGs with the new tint.
- *
- * It's not possible to unregister a tintable callback. So this can only be
- * used to register a static callback. If a set of tintables will change
- * over time then the best bet is to register a single callback for the
- * entire set.
- *
- * To ensure the tintable work happens at least once, it is also called as
- * part of registration.
- *
- * @param {Function} tintable Function to call when the tint changes.
- */
- registerTintable(tintable) {
- this.tintables.push(tintable);
- tintable();
- }
-
- getKeyRgb() {
- return this.keyRgb;
- }
-
- tint(primaryColor, secondaryColor, tertiaryColor) {
- return;
- // eslint-disable-next-line no-unreachable
- this.currentTint[0] = primaryColor;
- this.currentTint[1] = secondaryColor;
- this.currentTint[2] = tertiaryColor;
-
- this.calcCssFixups();
-
- if (DEBUG) {
- console.log("Tinter.tint(" + primaryColor + ", " +
- secondaryColor + ", " +
- tertiaryColor + ")");
- }
-
- if (!primaryColor) {
- primaryColor = this.keyRgb[0];
- secondaryColor = this.keyRgb[1];
- tertiaryColor = this.keyRgb[2];
- }
-
- if (!secondaryColor) {
- const x = 0.16; // average weighting factor calculated from vector green & light green
- const rgb = colorToRgb(primaryColor);
- rgb[0] = x * rgb[0] + (1 - x) * 255;
- rgb[1] = x * rgb[1] + (1 - x) * 255;
- rgb[2] = x * rgb[2] + (1 - x) * 255;
- secondaryColor = rgbToColor(rgb);
- }
-
- if (!tertiaryColor) {
- const x = 0.19;
- const rgb1 = colorToRgb(primaryColor);
- const rgb2 = colorToRgb(secondaryColor);
- rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
- rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
- rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
- tertiaryColor = rgbToColor(rgb1);
- }
-
- if (this.forceTint == false &&
- this.colors[0] === primaryColor &&
- this.colors[1] === secondaryColor &&
- this.colors[2] === tertiaryColor) {
- return;
- }
-
- this.forceTint = false;
-
- this.colors[0] = primaryColor;
- this.colors[1] = secondaryColor;
- this.colors[2] = tertiaryColor;
-
- if (DEBUG) {
- console.log("Tinter.tint final: (" + primaryColor + ", " +
- secondaryColor + ", " +
- tertiaryColor + ")");
- }
-
- // go through manually fixing up the stylesheets.
- this.applyCssFixups();
-
- // tell all the SVGs to go fix themselves up
- // we don't do this as a dispatch otherwise it will visually lag
- this.tintables.forEach(function(tintable) {
- tintable();
- });
- }
-
- tintSvgWhite(whiteColor) {
- this.currentTint[3] = whiteColor;
-
- if (!whiteColor) {
- whiteColor = this.colors[3];
- }
- if (this.colors[3] === whiteColor) {
- return;
- }
- this.colors[3] = whiteColor;
- this.tintables.forEach(function(tintable) {
- tintable();
- });
- }
-
- tintSvgBlack(blackColor) {
- this.currentTint[4] = blackColor;
-
- if (!blackColor) {
- blackColor = this.colors[4];
- }
- if (this.colors[4] === blackColor) {
- return;
- }
- this.colors[4] = blackColor;
- this.tintables.forEach(function(tintable) {
- tintable();
- });
- }
-
-
- setTheme(theme) {
- this.theme = theme;
-
- // update keyRgb from the current theme CSS itself, if it defines it
- if (document.getElementById('mx_theme_accentColor')) {
- this.keyRgb[0] = window.getComputedStyle(
- document.getElementById('mx_theme_accentColor')).color;
- }
- if (document.getElementById('mx_theme_secondaryAccentColor')) {
- this.keyRgb[1] = window.getComputedStyle(
- document.getElementById('mx_theme_secondaryAccentColor')).color;
- }
- if (document.getElementById('mx_theme_tertiaryAccentColor')) {
- this.keyRgb[2] = window.getComputedStyle(
- document.getElementById('mx_theme_tertiaryAccentColor')).color;
- }
-
- this.calcCssFixups();
- this.forceTint = true;
-
- this.tint(this.currentTint[0], this.currentTint[1], this.currentTint[2]);
-
- if (theme === 'dark') {
- // abuse the tinter to change all the SVG's #fff to #2d2d2d
- // XXX: obviously this shouldn't be hardcoded here.
- this.tintSvgWhite('#2d2d2d');
- this.tintSvgBlack('#dddddd');
- } else {
- this.tintSvgWhite('#ffffff');
- this.tintSvgBlack('#000000');
- }
- }
-
- calcCssFixups() {
- // cache our fixups
- if (this.cssFixups[this.theme]) return;
-
- if (DEBUG) {
- console.debug("calcCssFixups start for " + this.theme + " (checking " +
- document.styleSheets.length +
- " stylesheets)");
- }
-
- this.cssFixups[this.theme] = [];
-
- for (let i = 0; i < document.styleSheets.length; i++) {
- const ss = document.styleSheets[i];
- try {
- if (!ss) continue; // well done safari >:(
- // Chromium apparently sometimes returns null here; unsure why.
- // see $14534907369972FRXBx:matrix.org in HQ
- // ...ah, it's because there's a third party extension like
- // privacybadger inserting its own stylesheet in there with a
- // resource:// URI or something which results in a XSS error.
- // See also #vector:matrix.org/$145357669685386ebCfr:matrix.org
- // ...except some browsers apparently return stylesheets without
- // hrefs, which we have no choice but ignore right now
-
- // XXX seriously? we are hardcoding the name of vector's CSS file in
- // here?
- //
- // Why do we need to limit it to vector's CSS file anyway - if there
- // are other CSS files affecting the doc don't we want to apply the
- // same transformations to them?
- //
- // Iterating through the CSS looking for matches to hack on feels
- // pretty horrible anyway. And what if the application skin doesn't use
- // Vector Green as its primary color?
- // --richvdh
-
- // Yes, tinting assumes that you are using the Element skin for now.
- // The right solution will be to move the CSS over to react-sdk.
- // And yes, the default assets for the base skin might as well use
- // Vector Green as any other colour.
- // --matthew
-
- // stylesheets we don't have permission to access (eg. ones from extensions) have a null
- // href and will throw exceptions if we try to access their rules.
- if (!ss.href || !ss.href.match(new RegExp('/theme-' + this.theme + '.css$'))) continue;
- if (ss.disabled) continue;
- if (!ss.cssRules) continue;
-
- if (DEBUG) console.debug("calcCssFixups checking " + ss.cssRules.length + " rules for " + ss.href);
-
- for (let j = 0; j < ss.cssRules.length; j++) {
- const rule = ss.cssRules[j];
- if (!rule.style) continue;
- if (rule.selectorText && rule.selectorText.match(/#mx_theme/)) continue;
- for (let k = 0; k < this.cssAttrs.length; k++) {
- const attr = this.cssAttrs[k];
- for (let l = 0; l < this.keyRgb.length; l++) {
- if (rule.style[attr] === this.keyRgb[l]) {
- this.cssFixups[this.theme].push({
- style: rule.style,
- attr: attr,
- index: l,
- });
- }
- }
- }
- }
- } catch (e) {
- // Catch any random exceptions that happen here: all sorts of things can go
- // wrong with this (nulls, SecurityErrors) and mostly it's for other
- // stylesheets that we don't want to proces anyway. We should not propagate an
- // exception out since this will cause the app to fail to start.
- console.log("Failed to calculate CSS fixups for a stylesheet: " + ss.href, e);
- }
- }
- if (DEBUG) {
- console.log("calcCssFixups end (" +
- this.cssFixups[this.theme].length +
- " fixups)");
- }
- }
-
- applyCssFixups() {
- if (DEBUG) {
- console.log("applyCssFixups start (" +
- this.cssFixups[this.theme].length +
- " fixups)");
- }
- for (let i = 0; i < this.cssFixups[this.theme].length; i++) {
- const cssFixup = this.cssFixups[this.theme][i];
- try {
- cssFixup.style[cssFixup.attr] = this.colors[cssFixup.index];
- } catch (e) {
- // Firefox Quantum explodes if you manually edit the CSS in the
- // inspector and then try to do a tint, as apparently all the
- // fixups are then stale.
- console.error("Failed to apply cssFixup in Tinter! ", e.name);
- }
- }
- if (DEBUG) console.log("applyCssFixups end");
- }
-
- // XXX: we could just move this all into TintableSvg, but as it's so similar
- // to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
- // keeping it here for now.
- calcSvgFixups(svgs) {
- // go through manually fixing up SVG colours.
- // we could do this by stylesheets, but keeping the stylesheets
- // updated would be a PITA, so just brute-force search for the
- // key colour; cache the element and apply.
-
- if (DEBUG) console.log("calcSvgFixups start for " + svgs);
- const fixups = [];
- for (let i = 0; i < svgs.length; i++) {
- let svgDoc;
- try {
- svgDoc = svgs[i].contentDocument;
- } catch (e) {
- let msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
- if (e.message) {
- msg += e.message;
- }
- if (e.stack) {
- msg += ' | stack: ' + e.stack;
- }
- console.error(msg);
- }
- if (!svgDoc) continue;
- const tags = svgDoc.getElementsByTagName("*");
- for (let j = 0; j < tags.length; j++) {
- const tag = tags[j];
- for (let k = 0; k < this.svgAttrs.length; k++) {
- const attr = this.svgAttrs[k];
- for (let l = 0; l < this.keyHex.length; l++) {
- if (tag.getAttribute(attr) &&
- tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) {
- fixups.push({
- node: tag,
- attr: attr,
- index: l,
- });
- }
- }
- }
- }
- }
- if (DEBUG) console.log("calcSvgFixups end");
-
- return fixups;
- }
-
- applySvgFixups(fixups) {
- if (DEBUG) console.log("applySvgFixups start for " + fixups);
- for (let i = 0; i < fixups.length; i++) {
- const svgFixup = fixups[i];
- svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]);
- }
- if (DEBUG) console.log("applySvgFixups end");
- }
-}
-
-if (global.singletonTinter === undefined) {
- global.singletonTinter = new Tinter();
-}
-export default global.singletonTinter;
diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js
index 3a2c611cc9..2442ff5e25 100644
--- a/src/components/structures/GroupView.js
+++ b/src/components/structures/GroupView.js
@@ -126,12 +126,11 @@ class CategoryRoomList extends React.Component {
};
render() {
- const TintableSvg = sdk.getComponent("elements.TintableSvg");
const addButton = this.props.editing ?
(
-
+
{ _t('Add rooms to this community') }
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 7986da203d..db11ee17a0 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -34,7 +34,6 @@ import dis from "../../dispatcher/dispatcher";
import Notifier from '../../Notifier';
import Modal from "../../Modal";
-import Tinter from "../../Tinter";
import * as sdk from '../../index';
import { showRoomInviteDialog, showStartChatInviteDialog } from '../../RoomInvite';
import * as Rooms from '../../Rooms';
@@ -283,11 +282,6 @@ export default class MatrixChat extends React.PureComponent {
this.pageChanging = false;
- // check we have the right tint applied for this theme.
- // N.B. we don't call the whole of setTheme() here as we may be
- // racing with the theme CSS download finishing from index.js
- Tinter.tint();
-
// For PersistentElement
this.state.resizeNotifier.on("middlePanelResized", this.dispatchTimelineResize);
@@ -1573,10 +1567,6 @@ export default class MatrixChat extends React.PureComponent {
});
}
});
- // Fire the tinter right on startup to ensure the default theme is applied
- // A later sync can/will correct the tint to be the right value for the user
- const colorScheme = SettingsStore.getValue("roomColor");
- Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
}
/**
diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js
index d0a2fbff41..62a67ee4db 100644
--- a/src/components/structures/MyGroups.js
+++ b/src/components/structures/MyGroups.js
@@ -123,7 +123,7 @@ export default class MyGroups extends React.Component {
{/*
-
+
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 885851e8e6..37cce818df 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -37,7 +37,6 @@ import Modal from '../../Modal';
import * as sdk from '../../index';
import CallHandler, { PlaceCallType } from '../../CallHandler';
import dis from '../../dispatcher/dispatcher';
-import Tinter from '../../Tinter';
import rateLimitedFunc from '../../ratelimitedfunc';
import * as Rooms from '../../Rooms';
import eventSearch, { searchPagination } from '../../Searching';
@@ -677,10 +676,6 @@ export default class RoomView extends React.Component {
// cancel any pending calls to the rate_limited_funcs
this.updateRoomMembers.cancelPendingCall();
- // no need to do this as Dir & Settings are now overlays. It just burnt CPU.
- // console.log("Tinter.tint from RoomView.unmount");
- // Tinter.tint(); // reset colourscheme
-
for (const watcher of this.settingWatchers) {
SettingsStore.unwatchSetting(watcher);
}
@@ -1030,10 +1025,6 @@ export default class RoomView extends React.Component {
private updateTint() {
const room = this.state.room;
if (!room) return;
-
- console.log("Tinter.tint from updateTint");
- const colorScheme = SettingsStore.getValue("roomColor", room.roomId);
- Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
}
private onAccountData = (event: MatrixEvent) => {
@@ -1047,12 +1038,7 @@ export default class RoomView extends React.Component {
private onRoomAccountData = (event: MatrixEvent, room: Room) => {
if (room.roomId == this.state.roomId) {
const type = event.getType();
- if (type === "org.matrix.room.color_scheme") {
- const colorScheme = event.getContent();
- // XXX: we should validate the event
- console.log("Tinter.tint from onRoomAccountData");
- Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
- } else if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
+ if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
this.updatePreviewUrlVisibility(room);
}
diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js
index 5013bcec0d..ffc4b47817 100644
--- a/src/components/views/elements/ActionButton.js
+++ b/src/components/views/elements/ActionButton.js
@@ -62,8 +62,6 @@ export default class ActionButton extends React.Component {
};
render() {
- const TintableSvg = sdk.getComponent("elements.TintableSvg");
-
let tooltip;
if (this.state.showTooltip) {
const Tooltip = sdk.getComponent("elements.Tooltip");
@@ -71,7 +69,7 @@ export default class ActionButton extends React.Component {
}
const icon = this.props.iconPath ?
- () :
+ () :
undefined;
const classNames = ["mx_RoleButton"];
diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js
index f8fa294b71..ca85d73a11 100644
--- a/src/components/views/elements/AddressTile.js
+++ b/src/components/views/elements/AddressTile.js
@@ -53,7 +53,6 @@ export default class AddressTile extends React.Component {
}
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
- const TintableSvg = sdk.getComponent("elements.TintableSvg");
const nameClasses = classNames({
"mx_AddressTile_name": true,
@@ -124,7 +123,7 @@ export default class AddressTile extends React.Component {
if (this.props.canDismiss) {
dismiss = (
{ _t("If you can't find the room you're looking for, ask for an invite or create a new room.",
null,
- {a: sub => {
+ { a: sub => {
return {sub};
- }},
+ } },
) }
{
) : null}
}
-
diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx
index 0097d55cf5..3d77eaeac1 100644
--- a/src/components/structures/TabbedView.tsx
+++ b/src/components/structures/TabbedView.tsx
@@ -17,10 +17,10 @@ limitations under the License.
*/
import * as React from "react";
-import {_t} from '../../languageHandler';
+import { _t } from '../../languageHandler';
import * as sdk from "../../index";
import AutoHideScrollbar from './AutoHideScrollbar';
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
/**
* Represents a tab for the TabbedView.
@@ -75,7 +75,7 @@ export default class TabbedView extends React.Component {
private _setActiveTab(tab: Tab) {
const idx = this.props.tabs.indexOf(tab);
if (idx !== -1) {
- this.setState({activeTabIndex: idx});
+ this.setState({ activeTabIndex: idx });
} else {
console.error("Could not find tab " + tab.label + " in tabs");
}
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index ad20d38139..4cd71056a6 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -364,7 +364,7 @@ class TimelinePanel extends React.Component {
if (!this._timelineWindow.canPaginate(dir)) {
debuglog("TimelinePanel: can't", dir, "paginate any further");
- this.setState({[canPaginateKey]: false});
+ this.setState({ [canPaginateKey]: false });
return Promise.resolve(false);
}
@@ -374,7 +374,7 @@ class TimelinePanel extends React.Component {
}
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
- this.setState({[paginatingKey]: true});
+ this.setState({ [paginatingKey]: true });
return this.onPaginationRequest(this._timelineWindow, dir, PAGINATE_SIZE).then((r) => {
if (this.unmounted) { return; }
@@ -428,7 +428,7 @@ class TimelinePanel extends React.Component {
// it goes back off the top of the screen (presumably because the user
// clicks on the 'jump to bottom' button), we need to re-enable it.
if (rmPosition < 0) {
- this.setState({readMarkerVisible: true});
+ this.setState({ readMarkerVisible: true });
}
// if read marker position goes between 0 and -1/1,
@@ -461,7 +461,7 @@ class TimelinePanel extends React.Component {
// we won't load this event now, because we don't want to push any
// events off the other end of the timeline. But we need to note
// that we can now paginate.
- this.setState({canForwardPaginate: true});
+ this.setState({ canForwardPaginate: true });
return;
}
@@ -603,7 +603,7 @@ class TimelinePanel extends React.Component {
};
onSync = (state, prevState, data) => {
- this.setState({clientSyncState: state});
+ this.setState({ clientSyncState: state });
};
_readMarkerTimeout(readMarkerPosition) {
@@ -781,7 +781,6 @@ class TimelinePanel extends React.Component {
this.sendReadReceipt();
};
-
// advance the read marker past any events we sent ourselves.
_advanceReadMarkerPastMyEvents() {
if (!this.props.manageReadMarkers) return;
@@ -899,7 +898,6 @@ class TimelinePanel extends React.Component {
&& !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
}
-
/* get the current scroll state. See ScrollPanel.getScrollState for
* details.
*
@@ -999,7 +997,7 @@ class TimelinePanel extends React.Component {
_loadTimeline(eventId, pixelOffset, offsetBase) {
this._timelineWindow = new TimelineWindow(
MatrixClientPeg.get(), this.props.timelineSet,
- {windowLimit: this.props.timelineCap});
+ { windowLimit: this.props.timelineCap });
const onLoaded = () => {
// clear the timeline min-height when
diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx
index 273c8a079f..79a73735f4 100644
--- a/src/components/structures/ToastContainer.tsx
+++ b/src/components/structures/ToastContainer.tsx
@@ -15,9 +15,9 @@ limitations under the License.
*/
import * as React from "react";
-import ToastStore, {IToast} from "../../stores/ToastStore";
+import ToastStore, { IToast } from "../../stores/ToastStore";
import classNames from "classnames";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
interface IState {
toasts: IToast[];
@@ -58,7 +58,7 @@ export default class ToastContainer extends React.Component<{}, IState> {
let containerClasses;
if (totalCount !== 0) {
const topToast = this.state.toasts[0];
- const {title, icon, key, component, className, props} = topToast;
+ const { title, icon, key, component, className, props } = topToast;
const toastClasses = classNames("mx_Toast_toast", {
"mx_Toast_hasIcon": icon,
[`mx_Toast_icon_${icon}`]: icon,
diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx
index e19e312f58..b8dce48235 100644
--- a/src/components/structures/UploadBar.tsx
+++ b/src/components/structures/UploadBar.tsx
@@ -25,7 +25,7 @@ import { Action } from "../../dispatcher/actions";
import ProgressBar from "../views/elements/ProgressBar";
import AccessibleButton from "../views/elements/AccessibleButton";
import { IUpload } from "../../models/IUpload";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
interface IProps {
room: Room;
@@ -47,7 +47,7 @@ export default class UploadBar extends React.Component {
// Set initial state to any available upload in this room - we might be mounting
// earlier than the first progress event, so should show something relevant.
const uploadsHere = this.getUploadsInRoom();
- this.state = {currentUpload: uploadsHere[0], uploadsHere};
+ this.state = { currentUpload: uploadsHere[0], uploadsHere };
}
componentDidMount() {
@@ -74,7 +74,7 @@ export default class UploadBar extends React.Component {
case Action.UploadFailed: {
if (!this.mounted) return;
const uploadsHere = this.getUploadsInRoom();
- this.setState({currentUpload: uploadsHere[0], uploadsHere});
+ this.setState({ currentUpload: uploadsHere[0], uploadsHere });
break;
}
}
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index 3cf0dc5f84..d85817486b 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -32,8 +32,8 @@ import FeedbackDialog from "../views/dialogs/FeedbackDialog";
import Modal from "../../Modal";
import LogoutDialog from "../views/dialogs/LogoutDialog";
import SettingsStore from "../../settings/SettingsStore";
-import {getCustomTheme} from "../../theme";
-import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
+import { getCustomTheme } from "../../theme";
+import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
import SdkConfig from "../../SdkConfig";
import { getHomePageUrl } from "../../utils/pages";
import { OwnProfileStore } from "../../stores/OwnProfileStore";
@@ -56,7 +56,7 @@ import HostSignupAction from "./HostSignupAction";
import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
import RoomName from "../views/elements/RoomName";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
import InlineSpinner from "../views/elements/InlineSpinner";
import TooltipButton from "../views/elements/TooltipButton";
interface IProps {
@@ -123,7 +123,7 @@ export default class UserMenu extends React.Component {
private onRoom = (room: Room): void => {
this.removePendingJoinRoom(room.roomId);
- }
+ };
private onTagStoreUpdate = () => {
this.forceUpdate(); // we don't have anything useful in state to update
@@ -152,14 +152,14 @@ export default class UserMenu extends React.Component {
};
private onThemeChanged = () => {
- this.setState({isDarkTheme: this.isUserOnDarkTheme()});
+ this.setState({ isDarkTheme: this.isUserOnDarkTheme() });
};
private onAction = (ev: ActionPayload) => {
switch (ev.action) {
case Action.ToggleUserMenu:
if (this.state.contextMenuPosition) {
- this.setState({contextMenuPosition: null});
+ this.setState({ contextMenuPosition: null });
} else {
if (this.buttonRef.current) this.buttonRef.current.click();
}
@@ -185,7 +185,7 @@ export default class UserMenu extends React.Component {
if (this.state.pendingRoomJoin.delete(roomId)) {
this.setState({
pendingRoomJoin: new Set(this.state.pendingRoomJoin),
- })
+ });
}
}
@@ -193,7 +193,7 @@ export default class UserMenu extends React.Component {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target as HTMLButtonElement;
- this.setState({contextMenuPosition: target.getBoundingClientRect()});
+ this.setState({ contextMenuPosition: target.getBoundingClientRect() });
};
private onContextMenu = (ev: React.MouseEvent) => {
@@ -210,7 +210,7 @@ export default class UserMenu extends React.Component {
};
private onCloseMenu = () => {
- this.setState({contextMenuPosition: null});
+ this.setState({ contextMenuPosition: null });
};
private onSwitchThemeClick = (ev: React.MouseEvent) => {
@@ -228,9 +228,9 @@ export default class UserMenu extends React.Component {
ev.preventDefault();
ev.stopPropagation();
- const payload: OpenToTabPayload = {action: Action.ViewUserSettings, initialTabId: tabId};
+ const payload: OpenToTabPayload = { action: Action.ViewUserSettings, initialTabId: tabId };
defaultDispatcher.dispatch(payload);
- this.setState({contextMenuPosition: null}); // also close the menu
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onShowArchived = (ev: ButtonEvent) => {
@@ -247,7 +247,7 @@ export default class UserMenu extends React.Component {
ev.stopPropagation();
Modal.createTrackedDialog('Feedback Dialog', '', FeedbackDialog);
- this.setState({contextMenuPosition: null}); // also close the menu
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onSignOutClick = async (ev: ButtonEvent) => {
@@ -257,30 +257,30 @@ export default class UserMenu extends React.Component {
const cli = MatrixClientPeg.get();
if (!cli || !cli.isCryptoEnabled() || !(await cli.exportRoomKeys())?.length) {
// log out without user prompt if they have no local megolm sessions
- dis.dispatch({action: 'logout'});
+ dis.dispatch({ action: 'logout' });
} else {
Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog);
}
- this.setState({contextMenuPosition: null}); // also close the menu
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onSignInClick = () => {
dis.dispatch({ action: 'start_login' });
- this.setState({contextMenuPosition: null}); // also close the menu
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onRegisterClick = () => {
dis.dispatch({ action: 'start_registration' });
- this.setState({contextMenuPosition: null}); // also close the menu
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onHomeClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
- defaultDispatcher.dispatch({action: 'view_home_page'});
- this.setState({contextMenuPosition: null}); // also close the menu
+ defaultDispatcher.dispatch({ action: 'view_home_page' });
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onCommunitySettingsClick = (ev: ButtonEvent) => {
@@ -290,7 +290,7 @@ export default class UserMenu extends React.Component {
Modal.createTrackedDialog('Edit Community', '', EditCommunityPrototypeDialog, {
communityId: CommunityPrototypeStore.instance.getSelectedCommunityId(),
});
- this.setState({contextMenuPosition: null}); // also close the menu
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onCommunityMembersClick = (ev: ButtonEvent) => {
@@ -307,7 +307,7 @@ export default class UserMenu extends React.Component {
action: 'view_room',
room_id: chat.roomId,
}, true);
- dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList});
+ dis.dispatch({ action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList });
} else {
// "This should never happen" clauses go here for the prototype.
Modal.createTrackedDialog('Failed to find general chat', '', ErrorDialog, {
@@ -315,7 +315,7 @@ export default class UserMenu extends React.Component {
description: _t("Failed to find the general chat for this community"),
});
}
- this.setState({contextMenuPosition: null}); // also close the menu
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onCommunityInviteClick = (ev: ButtonEvent) => {
@@ -323,7 +323,7 @@ export default class UserMenu extends React.Component {
ev.stopPropagation();
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
- this.setState({contextMenuPosition: null}); // also close the menu
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onDndToggle = (ev) => {
@@ -357,7 +357,7 @@ export default class UserMenu extends React.Component {
),
})}
- )
+ );
} else if (hostSignupConfig) {
if (hostSignupConfig && hostSignupConfig.url) {
// If hostSignup.domains is set to a non-empty array, only show
@@ -509,7 +509,7 @@ export default class UserMenu extends React.Component {
/>
- )
+ );
} else if (MatrixClientPeg.get().isGuest()) {
primaryOptionList = (
diff --git a/src/components/structures/UserView.js b/src/components/structures/UserView.js
index 6b472783bb..eb839be7be 100644
--- a/src/components/structures/UserView.js
+++ b/src/components/structures/UserView.js
@@ -17,14 +17,14 @@ limitations under the License.
import React from "react";
import PropTypes from "prop-types";
-import {MatrixClientPeg} from "../../MatrixClientPeg";
+import { MatrixClientPeg } from "../../MatrixClientPeg";
import * as sdk from "../../index";
import Modal from '../../Modal';
import { _t } from '../../languageHandler';
import HomePage from "./HomePage";
-import {replaceableComponent} from "../../utils/replaceableComponent";
-import {MatrixEvent} from "matrix-js-sdk/src/models/event";
-import {RoomMember} from "matrix-js-sdk/src/models/room-member";
+import { replaceableComponent } from "../../utils/replaceableComponent";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
@replaceableComponent("structures.UserView")
export default class UserView extends React.Component {
@@ -56,7 +56,7 @@ export default class UserView extends React.Component {
async _loadProfileInfo() {
const cli = MatrixClientPeg.get();
- this.setState({loading: true});
+ this.setState({ loading: true });
let profileInfo;
try {
profileInfo = await cli.getProfileInfo(this.props.userId);
@@ -66,13 +66,13 @@ export default class UserView extends React.Component {
title: _t('Could not load user profile'),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
- this.setState({loading: false});
+ this.setState({ loading: false });
return;
}
- const fakeEvent = new MatrixEvent({type: "m.room.member", content: profileInfo});
+ const fakeEvent = new MatrixEvent({ type: "m.room.member", content: profileInfo });
const member = new RoomMember(null, this.props.userId);
member.setMembershipEvent(fakeEvent);
- this.setState({member, loading: false});
+ this.setState({ member, loading: false });
}
render() {
diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js
index 654dd9b6c8..d691f6034b 100644
--- a/src/components/structures/auth/CompleteSecurity.js
+++ b/src/components/structures/auth/CompleteSecurity.js
@@ -20,7 +20,7 @@ import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
import SetupEncryptionBody from "./SetupEncryptionBody";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("structures.auth.CompleteSecurity")
export default class CompleteSecurity extends React.Component {
@@ -33,12 +33,12 @@ export default class CompleteSecurity extends React.Component {
const store = SetupEncryptionStore.sharedInstance();
store.on("update", this._onStoreUpdate);
store.start();
- this.state = {phase: store.phase};
+ this.state = { phase: store.phase };
}
_onStoreUpdate = () => {
const store = SetupEncryptionStore.sharedInstance();
- this.setState({phase: store.phase});
+ this.setState({ phase: store.phase });
};
componentWillUnmount() {
@@ -50,7 +50,7 @@ export default class CompleteSecurity extends React.Component {
render() {
const AuthPage = sdk.getComponent("auth.AuthPage");
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
- const {phase} = this.state;
+ const { phase } = this.state;
let icon;
let title;
diff --git a/src/components/structures/auth/E2eSetup.js b/src/components/structures/auth/E2eSetup.js
index 4e51ae828c..9b627449bc 100644
--- a/src/components/structures/auth/E2eSetup.js
+++ b/src/components/structures/auth/E2eSetup.js
@@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
import AuthPage from '../../views/auth/AuthPage';
import CompleteSecurityBody from '../../views/auth/CompleteSecurityBody';
import CreateCrossSigningDialog from '../../views/dialogs/security/CreateCrossSigningDialog';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("structures.auth.E2eSetup")
export default class E2eSetup extends React.Component {
diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js
index 6188fdb5e4..9f2ac9deed 100644
--- a/src/components/structures/auth/ForgotPassword.js
+++ b/src/components/structures/auth/ForgotPassword.js
@@ -22,13 +22,13 @@ import { _t, _td } from '../../../languageHandler';
import * as sdk from '../../../index';
import Modal from "../../../Modal";
import PasswordReset from "../../../PasswordReset";
-import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
+import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
import classNames from 'classnames';
import AuthPage from "../../views/auth/AuthPage";
import CountlyAnalytics from "../../../CountlyAnalytics";
import ServerPicker from "../../views/elements/ServerPicker";
import PassphraseField from '../../views/auth/PassphraseField';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm';
// Phases
diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index d34582b0c3..61d3759dee 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -14,28 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {ReactNode} from 'react';
-import {MatrixError} from "matrix-js-sdk/src/http-api";
+import React, { ReactNode } from 'react';
+import { MatrixError } from "matrix-js-sdk/src/http-api";
-import {_t, _td} from '../../../languageHandler';
+import { _t, _td } from '../../../languageHandler';
import * as sdk from '../../../index';
-import Login, {ISSOFlow, LoginFlow} from '../../../Login';
+import Login, { ISSOFlow, LoginFlow } from '../../../Login';
import SdkConfig from '../../../SdkConfig';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
-import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
+import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
import classNames from "classnames";
import AuthPage from "../../views/auth/AuthPage";
import PlatformPeg from '../../../PlatformPeg';
import SettingsStore from "../../../settings/SettingsStore";
-import {UIFeature} from "../../../settings/UIFeature";
+import { UIFeature } from "../../../settings/UIFeature";
import CountlyAnalytics from "../../../CountlyAnalytics";
-import {IMatrixClientCreds} from "../../../MatrixClientPeg";
+import { IMatrixClientCreds } from "../../../MatrixClientPeg";
import PasswordLogin from "../../views/auth/PasswordLogin";
import InlineSpinner from "../../views/elements/InlineSpinner";
import Spinner from "../../views/elements/Spinner";
import SSOButtons from "../../views/elements/SSOButtons";
import ServerPicker from "../../views/elements/ServerPicker";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
// These are used in several places, and come from the js-sdk's autodiscovery
// stuff. We define them here so that they'll be picked up by i18n.
@@ -166,7 +166,7 @@ export default class LoginComponent extends React.PureComponent
onPasswordLogin = async (username, phoneCountry, phoneNumber, password) => {
if (!this.state.serverIsAlive) {
- this.setState({busy: true});
+ this.setState({ busy: true });
// Do a quick liveliness check on the URLs
let aliveAgain = true;
try {
@@ -174,7 +174,7 @@ export default class LoginComponent extends React.PureComponent
this.props.serverConfig.hsUrl,
this.props.serverConfig.isUrl,
);
- this.setState({serverIsAlive: true, errorText: ""});
+ this.setState({ serverIsAlive: true, errorText: "" });
} catch (e) {
const componentState = AutoDiscoveryUtils.authComponentStateForError(e);
this.setState({
@@ -201,7 +201,7 @@ export default class LoginComponent extends React.PureComponent
this.loginLogic.loginViaPassword(
username, phoneCountry, phoneNumber, password,
).then((data) => {
- this.setState({serverIsAlive: true}); // it must be, we logged in.
+ this.setState({ serverIsAlive: true }); // it must be, we logged in.
this.props.onLoggedIn(data, password);
}, (error) => {
if (this.unmounted) {
@@ -252,7 +252,7 @@ export default class LoginComponent extends React.PureComponent
{_t(
'Please note you are logging into the %(hs)s server, not matrix.org.',
- {hs: this.props.serverConfig.hsName},
+ { hs: this.props.serverConfig.hsName },
)}
@@ -363,7 +363,7 @@ export default class LoginComponent extends React.PureComponent
}
};
- private async initLoginLogic({hsUrl, isUrl}: ValidatedServerConfig) {
+ private async initLoginLogic({ hsUrl, isUrl }: ValidatedServerConfig) {
let isDefaultServer = false;
if (this.props.serverConfig.isDefault
&& hsUrl === this.props.serverConfig.hsUrl
@@ -501,9 +501,9 @@ export default class LoginComponent extends React.PureComponent
return
{ flows.map(flow => {
const stepRenderer = this.stepRendererMap[flow.type];
- return { stepRenderer() }
+ return { stepRenderer() };
}) }
-
+ ;
}
private renderPasswordStep = () => {
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index 3a4be6f0d6..add536da03 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -14,23 +14,23 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {createClient} from 'matrix-js-sdk/src/matrix';
-import React, {ReactNode} from 'react';
-import {MatrixClient} from "matrix-js-sdk/src/client";
+import { createClient } from 'matrix-js-sdk/src/matrix';
+import React, { ReactNode } from 'react';
+import { MatrixClient } from "matrix-js-sdk/src/client";
import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
-import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
+import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
import classNames from "classnames";
import * as Lifecycle from '../../../Lifecycle';
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
import AuthPage from "../../views/auth/AuthPage";
-import Login, {ISSOFlow} from "../../../Login";
+import Login, { ISSOFlow } from "../../../Login";
import dis from "../../../dispatcher/dispatcher";
import SSOButtons from "../../views/elements/SSOButtons";
import ServerPicker from '../../views/elements/ServerPicker';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
serverConfig: ValidatedServerConfig;
@@ -131,7 +131,7 @@ export default class Registration extends React.Component {
serverDeadError: "",
};
- const {hsUrl, isUrl} = this.props.serverConfig;
+ const { hsUrl, isUrl } = this.props.serverConfig;
this.loginLogic = new Login(hsUrl, isUrl, null, {
defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used
});
@@ -180,7 +180,7 @@ export default class Registration extends React.Component {
}
}
- const {hsUrl, isUrl} = serverConfig;
+ const { hsUrl, isUrl } = serverConfig;
const cli = createClient({
baseUrl: hsUrl,
idBaseUrl: isUrl,
@@ -230,7 +230,7 @@ export default class Registration extends React.Component {
// the user off to the login page to figure their account out.
if (ssoFlow) {
// Redirect to login page - server probably expects SSO only
- dis.dispatch({action: 'start_login'});
+ dis.dispatch({ action: 'start_login' });
} else {
this.setState({
serverErrorIsFatal: true, // fatal because user cannot continue on this server
@@ -267,7 +267,7 @@ export default class Registration extends React.Component {
session_id: sessionId,
}),
);
- }
+ };
private onUIAuthFinished = async (success: boolean, response: any) => {
if (!success) {
@@ -432,7 +432,7 @@ export default class Registration extends React.Component {
private onLoginClickWithCheck = async ev => {
ev.preventDefault();
- const sessionLoaded = await Lifecycle.loadSession({ignoreGuest: true});
+ const sessionLoaded = await Lifecycle.loadSession({ ignoreGuest: true });
if (!sessionLoaded) {
// ok fine, there's still no session: really go to the login page
this.props.onLoginClick();
@@ -487,7 +487,7 @@ export default class Registration extends React.Component {
fragmentAfterLogin={this.props.fragmentAfterLogin}
/>