Merge branch 'dev'

This commit is contained in:
N-Pex 2024-07-12 13:16:46 +02:00
commit 0068dd9dff
43 changed files with 1013 additions and 177 deletions

View file

@ -1,5 +1,5 @@
build: build:
image: node:20 image: node:20.8.1
stage: build stage: build
before_script: before_script:
# - ./update_version.sh # - ./update_version.sh

View file

@ -1,3 +1,5 @@
var periodicSyncNewMsgReminderText;
// Notification click event listener // Notification click event listener
self.addEventListener("notificationclick", (e) => { self.addEventListener("notificationclick", (e) => {
e.notification.close(); e.notification.close();
@ -19,6 +21,10 @@ self.addEventListener("notificationclick", (e) => {
); );
}); });
self.addEventListener("message", (event) => {
periodicSyncNewMsgReminderText = event.data;
});
async function checkNewMessages() { async function checkNewMessages() {
const cachedCredentials = await caches.open('cachedCredentials'); const cachedCredentials = await caches.open('cachedCredentials');
// Todo... // Todo...
@ -28,7 +34,8 @@ async function checkNewMessages() {
// see browser compatibility: https://developer.mozilla.org/en-US/docs/Web/API/Web_Periodic_Background_Synchronization_API#browser_compatibility // see browser compatibility: https://developer.mozilla.org/en-US/docs/Web/API/Web_Periodic_Background_Synchronization_API#browser_compatibility
self.addEventListener('periodicsync', (event) => { self.addEventListener('periodicsync', (event) => {
if (event.tag === 'check-new-messages') { if (event.tag === 'check-new-messages') {
self.registration.showNotification("Notification via periodicSync"); let notificationTitle = periodicSyncNewMsgReminderText || "You may have new messages";
self.registration.showNotification(notificationTitle);
event.waitUntil(checkNewMessages()); event.waitUntil(checkNewMessages());
} }

View file

@ -49,7 +49,7 @@ export default {
this.setDefaultLanguage(); this.setDefaultLanguage();
}, },
mounted() { mounted() {
registerServiceWorker(); registerServiceWorker(this.$t('notification.periodicSync_new_msg_reminder'));
/** /**
if ( if (
window.location.protocol == "http" && window.location.protocol == "http" &&

View file

@ -19,3 +19,5 @@ $voice-recorded-color: #3ae17d;
$poll-hilite-color: #6360f0; $poll-hilite-color: #6360f0;
$poll-hilite-color-bg: #d6d5fc; $poll-hilite-color-bg: #d6d5fc;
$alert-bg-color: #FF3300; $alert-bg-color: #FF3300;
$min-touch-target: 48px;

View file

@ -286,7 +286,7 @@ body {
.input-area-button { .input-area-button {
margin: 0; margin: 0;
padding: 0; padding: 0;
min-width: 48px; min-width: $min-touch-target;
&.input-more-icon { &.input-more-icon {
svg { svg {
@ -430,6 +430,10 @@ body {
display: inline-block; display: inline-block;
position: relative; position: relative;
max-width: 70%; max-width: 70%;
@media #{map-get($display-breakpoints, 'sm-and-down')} {
min-height: $min-touch-target;
}
} }
&.from-admin .bubble { &.from-admin .bubble {
background-color: rgba($admin-bg,0.8); background-color: rgba($admin-bg,0.8);
@ -520,6 +524,10 @@ body {
display: inline-block; display: inline-block;
position: relative; position: relative;
max-width: 70%; max-width: 70%;
@media #{map-get($display-breakpoints, 'sm-and-down')} {
min-height: $min-touch-target;
}
} }
.audio-bubble { .audio-bubble {
background-color: rgba(#e5e5e5,0.8); background-color: rgba(#e5e5e5,0.8);
@ -1409,7 +1417,7 @@ body {
bottom: 68px; bottom: 68px;
left: 8px; left: 8px;
right: 8px; right: 8px;
height: 48px; height: $min-touch-target;
background: rgba(0, 0, 0, 0.69); background: rgba(0, 0, 0, 0.69);
border: 1px solid #000000; border: 1px solid #000000;
border-radius: 5px; border-radius: 5px;

View file

@ -37,7 +37,7 @@
border-radius: 4px; border-radius: 4px;
padding: 15px 14px; padding: 15px 14px;
margin: 0px; margin: 0px;
height: 48px; height: $min-touch-target;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
} }
@ -225,7 +225,7 @@
.add-answer-button { .add-answer-button {
border-radius: 4px; border-radius: 4px;
height: 48px; height: $min-touch-target;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
border: 1px solid #242424; border: 1px solid #242424;

View file

@ -1,4 +1,4 @@
$large-button-height: 48px; $large-button-height: $min-touch-target;
$small-button-height: 36px; $small-button-height: 36px;
.file-drop-root { .file-drop-root {
@ -295,11 +295,11 @@ $small-button-height: 36px;
position: relative; position: relative;
padding: 8px; padding: 8px;
.v-image { .v-image {
width: 48px; width: $min-touch-target;
height: 48px; height: $min-touch-target;
border-radius: 8px; border-radius: 8px;
object-fit: cover; object-fit: cover;
flex: 0 0 48px; flex: 0 0 $min-touch-target;
margin-right: 8px; margin-right: 8px;
} }
margin-bottom: 8px; margin-bottom: 8px;

BIN
src/assets/heart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -19,7 +19,11 @@
"loading": "{appName} wird geladen", "loading": "{appName} wird geladen",
"done": "Fertig", "done": "Fertig",
"user_kick_and_ban": "Hinauswerfen", "user_kick_and_ban": "Hinauswerfen",
"direct_chat": "Privater Chat" "direct_chat": "Privater Chat",
"delete_now": "Jetzt löschen",
"user_make_admin": "Admin erstellen",
"user_revoke_moderator": "Moderator widerrufen",
"user_make_moderator": "Moderator erstellen"
}, },
"message": { "message": {
"you": "Du", "you": "Du",
@ -68,7 +72,18 @@
"someone": "Jemand", "someone": "Jemand",
"file": "Datei", "file": "Datei",
"files": "Dateien", "files": "Dateien",
"download_all": "Alle herunterladen" "download_all": "Alle herunterladen",
"outgoing_message_deleted_text": "Du hast diese Nachricht gelöscht.",
"upload_file_too_large": "Datei ist zu groß zum Hochladen!",
"user_was_kicked": "{user} wurde aus dem Chat geworfen.",
"user_was_kicked_by_you": "Du hast {user} aus dem Chat geworfen.",
"user_was_kicked_you": "Du wurdest aus dem Chat geworfen.",
"incoming_message_deleted_text": "Diese Nachricht wurde gelöscht.",
"sent_media": "{count} Medienelemente gesendet.",
"user_was_banned_by_you": "Du hast {user} aus dem Chat geworfen und gesperrt.",
"user_was_banned_you": "Du wurdest aus dem Chat rausgeschmissen und gesperrt.",
"user_was_banned": "{user} wurde aus dem Chat geworfen und gesperrt.",
"preparing_to_upload": "Vorbereitungen zum Hochladen..."
}, },
"room": { "room": {
"leave": "Verlassen", "leave": "Verlassen",
@ -89,7 +104,9 @@
"room_history_is": "Der Raumverlauf ist {type}.", "room_history_is": "Der Raumverlauf ist {type}.",
"room_history_joined": "Die Teilnehmer können nur die Nachrichten sehen, die nach ihrem Beitritt gesendet wurden.", "room_history_joined": "Die Teilnehmer können nur die Nachrichten sehen, die nach ihrem Beitritt gesendet wurden.",
"got_it": "Verstanden", "got_it": "Verstanden",
"encrypted": "Die Nachrichten werden Ende-zu-Ende verschlüsselt." "encrypted": "Die Nachrichten werden Ende-zu-Ende verschlüsselt.",
"change": "Ändern",
"direct_private_chat": "Direktnachricht"
}, },
"new_room": { "new_room": {
"join_permissions": "Beitrittsberechtigungen", "join_permissions": "Beitrittsberechtigungen",
@ -130,7 +147,10 @@
"invalid_message": "Benutzername oder Passwort falsch", "invalid_message": "Benutzername oder Passwort falsch",
"send_verification": "Sende Bestätigungs-E-Mail", "send_verification": "Sende Bestätigungs-E-Mail",
"resend_verification": "Bestätigungsmail erneut senden", "resend_verification": "Bestätigungsmail erneut senden",
"accept_terms": "Akzeptiere." "accept_terms": "Akzeptiere.",
"token_not_valid": "Ungültiges Token",
"email_not_valid": "E-Mail-Adresse ist nicht gültig",
"send_token": "Token senden"
}, },
"profile": { "profile": {
"title": "Mein Profil", "title": "Mein Profil",
@ -146,7 +166,8 @@
"set_language": "Stelle deine Sprache ein", "set_language": "Stelle deine Sprache ein",
"language_description": "Convene ist in vielen Sprachen verfügbar.", "language_description": "Convene ist in vielen Sprachen verfügbar.",
"tell_us": "Teile uns mit.", "tell_us": "Teile uns mit.",
"notification_label": "Benachrichtigung" "notification_label": "Benachrichtigung",
"display_name_required": "Anzeigename ist erforderlich"
}, },
"profile_info_popup": { "profile_info_popup": {
"you_are": "Du bist", "you_are": "Du bist",
@ -154,7 +175,7 @@
"edit_profile": "Profil bearbeiten", "edit_profile": "Profil bearbeiten",
"logout": "Abmelden", "logout": "Abmelden",
"powered_by": "Dieser Raum wird von {product} betrieben. Erfahre mehr unter {productLink} oder erstelle einen weiteren Raum!", "powered_by": "Dieser Raum wird von {product} betrieben. Erfahre mehr unter {productLink} oder erstelle einen weiteren Raum!",
"new_room": "+ neuer Raum", "new_room": "Neuer Raum",
"identity_temporary": "{displayName}", "identity_temporary": "{displayName}",
"want_more": "Willst du mehr?" "want_more": "Willst du mehr?"
}, },
@ -168,7 +189,9 @@
"title": "Willkommen in {roomName}", "title": "Willkommen in {roomName}",
"enter_room": "Raum betreten", "enter_room": "Raum betreten",
"remember_me": "Mich merken", "remember_me": "Mich merken",
"choose_name": "Wähle einen zu nutzenden Namen" "choose_name": "Wähle einen zu nutzenden Namen",
"join_user": "Chat starten",
"enter_room_user": "Chat starten"
}, },
"invite": { "invite": {
"title": "Freunde hinzufügen", "title": "Freunde hinzufügen",
@ -228,7 +251,19 @@
"message_retention_1_week": "1 Woche", "message_retention_1_week": "1 Woche",
"message_retention_1_day": "1 Tag", "message_retention_1_day": "1 Tag",
"message_retention_8_hours": "8 Stunden", "message_retention_8_hours": "8 Stunden",
"message_retention_1_hour": "1 Stunde" "message_retention_1_hour": "1 Stunde",
"read_only_room": "Schreibgeschützt",
"moderation": "Moderation",
"experimental_features": "Experimentelle Funktionen",
"file_mode": "Dateimodus",
"message_retention_4_week": "4 Wochen",
"make_public": "Öffentlich machen",
"direct_link": "Mein Direktlink",
"voice_mode": "Sprachmodus",
"download_chat": "Chat herunterladen",
"message_retention": "Nachrichtenverlauf",
"message_retention_none": "Aus",
"room_type": "Raumart"
}, },
"room_info_sheet": { "room_info_sheet": {
"this_room": "Dieser Raum", "this_room": "Dieser Raum",
@ -278,7 +313,9 @@
"poll_status_open_not_voted": "Umfrage ist offen stimme ab, um die Ergebnisse zu sehen", "poll_status_open_not_voted": "Umfrage ist offen stimme ab, um die Ergebnisse zu sehen",
"close_poll": "Umfrage schließen", "close_poll": "Umfrage schließen",
"answer_label_n": "Antwort", "answer_label_n": "Antwort",
"view_results": "Ergebnisse ansehen" "view_results": "Ergebnisse ansehen",
"answer_label_1": "Antwort*",
"please_complete": "Bitte vervollständigen"
}, },
"export": { "export": {
"fetched_n_of_total_events": "{count} von {total} Ereignissen geladen", "fetched_n_of_total_events": "{count} von {total} Ereignissen geladen",
@ -294,10 +331,15 @@
"add_reaction": "Reaktion hinzufügen", "add_reaction": "Reaktion hinzufügen",
"show_less": "Weniger anzeigen", "show_less": "Weniger anzeigen",
"time": { "time": {
"recently": "im Moment" "recently": "im Moment",
"hours": "vor 1 Stunde | vor {n} Stunden",
"days": "vor 1 Tag | vor {n} Tagen",
"minutes": "vor 1 Minute | vor {n} Minuten"
}, },
"notify": "Benachrichtigung", "notify": "Benachrichtigung",
"close": "schlieen" "close": "schlieen",
"click_to_remove": "Zum Entfernen klicken",
"password_hint": "Mindestens 12 Zeichen, davon mindestens eine Ziffer, ein Großbuchstabe und ein Kleinbuchstabe"
}, },
"emoji": { "emoji": {
"categories": { "categories": {
@ -306,7 +348,8 @@
"flags": "Kennzeichnungen", "flags": "Kennzeichnungen",
"objects": "Objekte", "objects": "Objekte",
"symbols": "Symbole", "symbols": "Symbole",
"places": "Orte" "places": "Orte",
"frequently": "Häufig genutzt"
}, },
"search": "Suche ..." "search": "Suche ..."
}, },
@ -314,15 +357,25 @@
"sending": "Sende", "sending": "Sende",
"sending_progress": "Senden…", "sending_progress": "Senden…",
"close": "Schließen", "close": "Schließen",
"files": "Dateien" "files": "Dateien",
"files_sent": "1 Datei gesendet! | {count} Dateien gesendet!",
"choose_files": "Dateien auswählen",
"send_more_files": "Weitere Dateien senden"
}, },
"notification": { "notification": {
"dialog": { "dialog": {
"enable": "Aktiviere" "enable": "Aktiviere"
} },
"title": "Neue Nachricht erhalten"
}, },
"getlink": { "getlink": {
"next": "Nächster", "next": "Nächster",
"continue": "Fortfahren" "continue": "Fortfahren",
"hello": "Hallo {user},\nhier ist dein Direktlink",
"share_qr": "QR teilen",
"qr_image_copied": "Bild in die Zwischenablage kopiert"
},
"createchannel": {
"name_required": "Kanalname ist erforderlich"
} }
} }

View file

@ -109,7 +109,8 @@
"files": "Files", "files": "Files",
"images": "Images", "images": "Images",
"send_attachements_dialog_title": "Do you want to send following attachments ?", "send_attachements_dialog_title": "Do you want to send following attachments ?",
"download_all": "Download all" "download_all": "Download all",
"failed_to_render": "Failed to render event"
}, },
"room": { "room": {
"invitations": "You have no invitations | You have 1 invitation | You have {count} invitations", "invitations": "You have no invitations | You have 1 invitation | You have {count} invitations",
@ -140,6 +141,7 @@
"direct_private_chat": "Direct Message", "direct_private_chat": "Direct Message",
"join_channel": "All set! Invite people to join you: {link}", "join_channel": "All set! Invite people to join you: {link}",
"info_retention": "🕓 Messages sent within {time} are viewable by anyone with the link.", "info_retention": "🕓 Messages sent within {time} are viewable by anyone with the link.",
"info_retention_user": "🕓 Messages older than {time} will be deleted from the history.",
"change": "Change" "change": "Change"
}, },
"new_room": { "new_room": {
@ -406,7 +408,8 @@
"enable": "Enable" "enable": "Enable"
}, },
"blocked_message": "Notification is blocked. Go to your device or browser settings to enable Notification", "blocked_message": "Notification is blocked. Go to your device or browser settings to enable Notification",
"not_supported": "Notification is not yet supported in Mobile" "not_supported": "Notification is not yet supported in Mobile",
"periodicSync_new_msg_reminder": "You may have new messages"
}, },
"emoji": { "emoji": {
"search": "Search...", "search": "Search...",

View file

@ -26,13 +26,13 @@
"room_type_default": "Por Defecto", "room_type_default": "Por Defecto",
"copy_link": "Copiar enlace", "copy_link": "Copiar enlace",
"direct_link": "Mi enlace directo", "direct_link": "Mi enlace directo",
"read_only_room": "Sala de sólo lectura", "read_only_room": "Solo lectura",
"voice_mode": "Por voz", "voice_mode": "Por voz",
"make_public_warning": "Advertencia: El historial completo de mensajes será visible para los nuevos participantes", "make_public_warning": "Advertencia: El historial completo de mensajes será visible para los nuevos participantes",
"room_type": "Genero de la sala", "room_type": "Genero de la sala",
"experimental_features": "Funciones experimentales", "experimental_features": "Funciones experimentales",
"file_mode_info": "Cambia la interfaz del chat al modo \"soltar archivos\"", "file_mode_info": "Cambia la interfaz del chat al modo \"soltar archivos\"",
"read_only_room_info": "Sólo los administradores y moderadores pueden escribir en la sala", "read_only_room_info": "Solo los administradores y moderadores pueden enviar mensajes a la sala.",
"file_mode": "Modo del archivo", "file_mode": "Modo del archivo",
"direct_link_desc": "¡Ya está listo para compartir! Se abrirá una nueva sala privada cada vez que alguien abra el enlace.", "direct_link_desc": "¡Ya está listo para compartir! Se abrirá una nueva sala privada cada vez que alguien abra el enlace.",
"download_chat": "Descargar el chat", "download_chat": "Descargar el chat",
@ -49,7 +49,8 @@
"shared_room_number": "Compartes {count} salas con {name}", "shared_room_number": "Compartes {count} salas con {name}",
"shared_room_number_more": "Compartes más de {count} y salas con {name}", "shared_room_number_more": "Compartes más de {count} y salas con {name}",
"message_retention": "Historial de mensajes", "message_retention": "Historial de mensajes",
"message_retention_info": "Los mensajes enviados dentro de este plazo pueden ser vistos por cualquiera que tenga el enlace." "message_retention_info": "Los mensajes enviados dentro de este plazo pueden ser vistos por cualquiera que tenga el enlace.",
"moderation": "Moderado"
}, },
"purge_room": { "purge_room": {
"button": "Borrar", "button": "Borrar",
@ -173,7 +174,11 @@
"room_history_joined": "La gente sólo puede ver los mensajes enviados después de unirse.", "room_history_joined": "La gente sólo puede ver los mensajes enviados después de unirse.",
"no_past_messages": "¡Bienvenido! Por su seguridad, los mensajes anteriores no están disponibles.", "no_past_messages": "¡Bienvenido! Por su seguridad, los mensajes anteriores no están disponibles.",
"direct_info": "Hola, {you}. Estás en un chat privado con {user}.", "direct_info": "Hola, {you}. Estás en un chat privado con {user}.",
"direct_private_chat": "Mensaje directo" "direct_private_chat": "Mensaje directo",
"change": "Cambiar",
"join_channel": "¡Listo! Invita a la gente a unirse a: {link}",
"info_retention": "🕓 Los mensajes enviados dentro de {time} pueden ser vistos por cualquiera que tenga el enlace.",
"info_retention_user": "🕓 Los mensajes anteriores a {time} serán eliminados del historial."
}, },
"room": { "room": {
"leave": "Salir", "leave": "Salir",
@ -253,7 +258,8 @@
"time_ago": "Hoy | Ayer | Hace {count} días", "time_ago": "Hoy | Ayer | Hace {count} días",
"upload_exceeded_file_limit": "Se ha superado el tamaño máximo para el archivo ({configFormattedUploadSize}). ", "upload_exceeded_file_limit": "Se ha superado el tamaño máximo para el archivo ({configFormattedUploadSize}). ",
"someone": "Alguien", "someone": "Alguien",
"sent_media": "Enviados {count} elementos multimedia." "sent_media": "Enviados {count} elementos multimedia.",
"failed_to_render": "No se pudo representar el evento"
}, },
"menu": { "menu": {
"login": "Iniciar sesión", "login": "Iniciar sesión",
@ -411,7 +417,8 @@
}, },
"title": "Nuevo mensaje recibido", "title": "Nuevo mensaje recibido",
"blocked_message": "La notificación está bloqueada. Vaya a la configuración de su dispositivo o navegador para habilitar la Notificación", "blocked_message": "La notificación está bloqueada. Vaya a la configuración de su dispositivo o navegador para habilitar la Notificación",
"not_supported": "La notificación aún no es compatible con dispositivos móviles" "not_supported": "La notificación aún no es compatible con dispositivos móviles",
"periodicSync_new_msg_reminder": "Es posible que tengas nuevos mensajes"
}, },
"export": { "export": {
"fetched_n_of_total_events": "{count} de {total} eventos recuperados", "fetched_n_of_total_events": "{count} de {total} eventos recuperados",
@ -422,5 +429,13 @@
}, },
"logout": { "logout": {
"confirm_text": "¿Seguro que quieres cerrar la sesión?" "confirm_text": "¿Seguro que quieres cerrar la sesión?"
},
"createchannel": {
"title": "Crear un canal",
"info": "Difunde noticias o conocimientos en cualquier formato: vídeo, podcast, texto, imágenes o PDF.",
"channel_name": "Nombre de tu canal",
"channel_topic": "Descríbelo",
"name_required": "El nombre del canal es obligatorio",
"error_channel": "Error al crear el canal"
} }
} }

View file

@ -64,7 +64,8 @@
"password": "Anna salasana", "password": "Anna salasana",
"password_required": "Salasana vaaditaan", "password_required": "Salasana vaaditaan",
"create_room": "Rekisteröidy ja luo huone", "create_room": "Rekisteröidy ja luo huone",
"accept_terms": "Hyväksy" "accept_terms": "Hyväksy",
"token_not_valid": "Virheellinen koodi"
}, },
"join": { "join": {
"title": "Tervetuloa huoneen {roomName}", "title": "Tervetuloa huoneen {roomName}",
@ -106,7 +107,8 @@
"reply_poll": "Kysely", "reply_poll": "Kysely",
"file": "Tiedosto", "file": "Tiedosto",
"someone": "Joku", "someone": "Joku",
"images": "Kuvat" "images": "Kuvat",
"room_joinrule_public": "julkinen"
}, },
"room": { "room": {
"leave": "Poistu", "leave": "Poistu",
@ -118,7 +120,8 @@
"info": "Tervetuloa! Seuraavassa on muutamia asioita, jotka sinun on hyvä tietää huoneestasi:", "info": "Tervetuloa! Seuraavassa on muutamia asioita, jotka sinun on hyvä tietää huoneestasi:",
"info_permissions": "Voit muuttaa liittymisoikeuksia milloin tahansa huoneen asetuksissa.", "info_permissions": "Voit muuttaa liittymisoikeuksia milloin tahansa huoneen asetuksissa.",
"encrypted": "Viestit ovat päästä päähän salattuja.", "encrypted": "Viestit ovat päästä päähän salattuja.",
"got_it": "Selvä" "got_it": "Selvä",
"change": "Vaihda"
}, },
"profile": { "profile": {
"temporary_identity": "Tämä identiteetti on väliaikainen. Aseta salasana käyttääksesi sitä uudelleen", "temporary_identity": "Tämä identiteetti on väliaikainen. Aseta salasana käyttääksesi sitä uudelleen",
@ -170,7 +173,8 @@
"message_retention_none": "Pois päältä", "message_retention_none": "Pois päältä",
"message_retention_1_day": "1 päivä", "message_retention_1_day": "1 päivä",
"message_retention_8_hours": "8 tuntia", "message_retention_8_hours": "8 tuntia",
"message_retention_1_hour": "1 tunti" "message_retention_1_hour": "1 tunti",
"read_only_room": "Lue Ainoastaan"
}, },
"power_level": { "power_level": {
"restricted": "rajoitettu", "restricted": "rajoitettu",

View file

@ -85,7 +85,8 @@
"join_invite": "Seules les personnes que vous invitez peuvent sy joindre.", "join_invite": "Seules les personnes que vous invitez peuvent sy joindre.",
"info_permissions": "Vous pouvez modifier les « autorisations de participation » à tout moment dans les paramètres du salon.", "info_permissions": "Vous pouvez modifier les « autorisations de participation » à tout moment dans les paramètres du salon.",
"room_history_joined": "Les personnes ne peuvent voir que les messages envoyés après leur adhésion.", "room_history_joined": "Les personnes ne peuvent voir que les messages envoyés après leur adhésion.",
"got_it": "Compris" "got_it": "Compris",
"change": "Changer"
}, },
"new_room": { "new_room": {
"next": "Suivant", "next": "Suivant",
@ -220,7 +221,8 @@
"message_retention_1_week": "1 semaine", "message_retention_1_week": "1 semaine",
"message_retention_1_day": "1 jour", "message_retention_1_day": "1 jour",
"message_retention_8_hours": "8 heures", "message_retention_8_hours": "8 heures",
"message_retention_1_hour": "1 heure" "message_retention_1_hour": "1 heure",
"read_only_room": "Lecture seule"
}, },
"room_info_sheet": { "room_info_sheet": {
"this_room": "Ce salon", "this_room": "Ce salon",

View file

@ -84,7 +84,8 @@
"join_invite": "Solo le persone che inviti possono partecipare.", "join_invite": "Solo le persone che inviti possono partecipare.",
"info_permissions": "Puoi cambiare i «permessi di adesione» in qualsiasi momento nelle impostazioni della stanza.", "info_permissions": "Puoi cambiare i «permessi di adesione» in qualsiasi momento nelle impostazioni della stanza.",
"got_it": "Capito", "got_it": "Capito",
"room_history_joined": "Le persone possono vedere solo i messaggi inviati dopo la loro adesione." "room_history_joined": "Le persone possono vedere solo i messaggi inviati dopo la loro adesione.",
"change": "Cambia"
}, },
"new_room": { "new_room": {
"next": "Successivo", "next": "Successivo",
@ -216,7 +217,8 @@
"message_retention_none": "Off", "message_retention_none": "Off",
"message_retention_1_day": "1 giorno", "message_retention_1_day": "1 giorno",
"message_retention_8_hours": "8 ore", "message_retention_8_hours": "8 ore",
"message_retention_1_hour": "1 ora" "message_retention_1_hour": "1 ora",
"read_only_room": "Sola lettura"
}, },
"voice_recorder": { "voice_recorder": {
"failed_to_record": "Impossibile registrare laudio", "failed_to_record": "Impossibile registrare laudio",

View file

@ -80,7 +80,8 @@
"message_retention_1_week": "1 uke", "message_retention_1_week": "1 uke",
"message_retention_1_day": "1 dag", "message_retention_1_day": "1 dag",
"message_retention_8_hours": "åtte timer", "message_retention_8_hours": "åtte timer",
"message_retention_1_hour": "Én time" "message_retention_1_hour": "Én time",
"read_only_room": "Skrivebeskyttet"
}, },
"goodbye": { "goodbye": {
"view_other_rooms": "Vis andre rom", "view_other_rooms": "Vis andre rom",
@ -137,7 +138,8 @@
"room_history_is": "Romhistorikken er {type}.", "room_history_is": "Romhistorikken er {type}.",
"encrypted": "Meldinger er ende-til-ende -kryptert.", "encrypted": "Meldinger er ende-til-ende -kryptert.",
"got_it": "Skjønner", "got_it": "Skjønner",
"join_invite": "Kun folk du inviterer kan ta del." "join_invite": "Kun folk du inviterer kan ta del.",
"change": "Endre"
}, },
"room": { "room": {
"room_list_rooms": "Rom", "room_list_rooms": "Rom",

View file

@ -122,7 +122,8 @@
"upload_exceeded_file_limit": "O tamanho máximo do arquivo ({configFormattedUploadSize}) foi excedido. ", "upload_exceeded_file_limit": "O tamanho máximo do arquivo ({configFormattedUploadSize}) foi excedido. ",
"preparing_to_upload": "Preparando para enviar...", "preparing_to_upload": "Preparando para enviar...",
"someone": "Alguém", "someone": "Alguém",
"sent_media": "Enviou {count} itens de mídia." "sent_media": "Enviou {count} itens de mídia.",
"failed_to_render": "Houve uma falha ao renderizar o evento"
}, },
"room": { "room": {
"members": "sem membros | 1 membro | {count} membros", "members": "sem membros | 1 membro | {count} membros",
@ -150,7 +151,11 @@
"got_it": "Entendi", "got_it": "Entendi",
"no_past_messages": "Bem-vindo! Para a sua segurança, as mensagens anteriores não estão disponíveis.", "no_past_messages": "Bem-vindo! Para a sua segurança, as mensagens anteriores não estão disponíveis.",
"direct_info": "Olá {you}. Você está num chat privado com {user}.", "direct_info": "Olá {you}. Você está num chat privado com {user}.",
"direct_private_chat": "Mensagem direta" "direct_private_chat": "Mensagem direta",
"change": "Mudança",
"join_channel": "Tudo pronto! Convide pessoas para se juntarem a você: {link}",
"info_retention": "🕓 As mensagens enviadas dentro de {time} podem ser visualizadas por qualquer pessoa com o link.",
"info_retention_user": "As mensagens mais antigas que {time} serão excluídas do histórico."
}, },
"new_room": { "new_room": {
"new_room": "Nova sala", "new_room": "Nova sala",
@ -295,11 +300,11 @@
"voice_mode": "Modo de voz", "voice_mode": "Modo de voz",
"user_admin": "Administrador", "user_admin": "Administrador",
"voice_mode_info": "Alterna a interface de chat para um modo de 'ouvir e gravar'", "voice_mode_info": "Alterna a interface de chat para um modo de 'ouvir e gravar'",
"read_only_room": "Sala somente leitura", "read_only_room": "Somente leitura",
"user_moderator": "Moderador", "user_moderator": "Moderador",
"experimental_features": "Recursos experimentais", "experimental_features": "Recursos experimentais",
"download_chat": "Baixar o chat", "download_chat": "Baixar o chat",
"read_only_room_info": "Apenas administradores e moderadores podem postar na sala", "read_only_room_info": "Apenas os administradores e os moderadores podem postar na sala.",
"copy_link": "Copiar o link", "copy_link": "Copiar o link",
"make_public": "Tornar público", "make_public": "Tornar público",
"room_type": "Tipo de sala", "room_type": "Tipo de sala",
@ -319,7 +324,8 @@
"message_retention_info": "As mensagens enviadas dentro desse período podem ser visualizadas por qualquer pessoa que tenha o link.", "message_retention_info": "As mensagens enviadas dentro desse período podem ser visualizadas por qualquer pessoa que tenha o link.",
"message_retention_4_week": "4 semanas", "message_retention_4_week": "4 semanas",
"shared_room_number": "Você compartilha {count} quartos com {name}", "shared_room_number": "Você compartilha {count} quartos com {name}",
"shared_room_number_more": "Você compartilha mais de {count} quartos com {name}" "shared_room_number_more": "Você compartilha mais de {count} quartos com {name}",
"moderation": "Moderação"
}, },
"room_info_sheet": { "room_info_sheet": {
"this_room": "Esta sala", "this_room": "Esta sala",
@ -408,7 +414,8 @@
"enable": "Ativar" "enable": "Ativar"
}, },
"blocked_message": "A notificação está bloqueada. Vá para as configurações do dispositivo ou do navegador para ativar a notificação", "blocked_message": "A notificação está bloqueada. Vá para as configurações do dispositivo ou do navegador para ativar a notificação",
"not_supported": "A notificação ainda não é suportada no mobile" "not_supported": "A notificação ainda não é suportada no mobile",
"periodicSync_new_msg_reminder": "Talvez você tenha novas mensagens"
}, },
"getlink": { "getlink": {
"title": "Obter um link direto", "title": "Obter um link direto",
@ -422,5 +429,13 @@
"username": "Insira um nome (ex: waku)", "username": "Insira um nome (ex: waku)",
"different_link": "Obter um link diferente", "different_link": "Obter um link diferente",
"next": "Próximo" "next": "Próximo"
},
"createchannel": {
"channel_topic": "Descreva-o",
"name_required": "O nome do canal é obrigatório",
"info": "Transmita notícias ou conhecimento em qualquer formato: vídeo, podcast, texto, imagens ou PDFs.",
"error_channel": "Houve uma falha ao criar o canal",
"title": "Criar um canal",
"channel_name": "Dê um nome ao seu canal"
} }
} }

View file

@ -63,7 +63,8 @@
"room_type_default": "Predefinit", "room_type_default": "Predefinit",
"message_retention_none": "Dezactivat", "message_retention_none": "Dezactivat",
"message_retention_1_day": "1 zi", "message_retention_1_day": "1 zi",
"message_retention_1_hour": "O oră" "message_retention_1_hour": "O oră",
"message_retention_8_hours": "8 ore"
}, },
"goodbye": { "goodbye": {
"view_other_rooms": "Vezi alte camere", "view_other_rooms": "Vezi alte camere",
@ -181,7 +182,8 @@
"room_history_joined": "Oamenii pot vedea doar mesajele trimise după ce se înscriu.", "room_history_joined": "Oamenii pot vedea doar mesajele trimise după ce se înscriu.",
"room_history_is": "Istoricul camerei este {type}.", "room_history_is": "Istoricul camerei este {type}.",
"encrypted": "Mesajele sunt criptate de la un capăt la altul.", "encrypted": "Mesajele sunt criptate de la un capăt la altul.",
"info": "Bine ați venit! Iată câteva lucruri pe care trebuie să le știți despre camera dumneavoastră:" "info": "Bine ați venit! Iată câteva lucruri pe care trebuie să le știți despre camera dumneavoastră:",
"change": "Schimbă"
}, },
"room": { "room": {
"room_list_rooms": "Camere", "room_list_rooms": "Camere",

View file

@ -0,0 +1,441 @@
{
"login": {
"invalid_message": "Неверное имя пользователя или пароль",
"title": "Имя пользователя",
"password": "Введите пароль",
"login": "Имя пользователя",
"resend_verification": "Отправить электронное письмо-подтверждение снова",
"token_not_valid": "Недействительный токен",
"accept_terms": "Принять",
"username": "Имя пользователя (например: marta)",
"username_required": "Имя пользователя обязательно",
"password_required": "Требуется пароль",
"create_room": "Регистрация и создание комнаты",
"no_supported_flow": "Приложение не может войти на указанный сервер",
"email_not_valid": "Адрес электронной почты недействителен",
"or": "ИЛИ",
"email": "Вам необходимо подтвердить свой адрес электронной почты",
"send_verification": "Отправить письмо с подтверждением",
"send_token": "Отправить токен",
"terms": "Домашний сервер требует, чтобы вы ознакомились со следующими правилами и приняли их:",
"sent_verification": "На адрес {email} отправлено письмо. Пожалуйста, используйте свой обычный почтовый клиент для проверки адреса.",
"registration_token": "Пожалуйста, введите регистрационный токен"
},
"room_info": {
"copy_link": "Копировать ссылку",
"message_retention_1_day": "1 день",
"message_retention_8_hours": "8 часов",
"message_retention_1_hour": "1 час",
"hide_all": "Скрыть",
"leave_room": "Оставить",
"user_admin": "Администратор",
"user_moderator": "Модератор",
"room_type_default": "По умолчанию",
"members": "Элементов",
"read_only_room": "Только чтение",
"message_retention_none": "Выключить",
"message_retention_2_week": "2 недели",
"message_retention_1_week": "1 неделя",
"join_public": "У кого-нибудь есть ссылка",
"copy_invite_link": "Скопируйте ссылку на приглашение",
"scan_code": "Сканировать, чтобы присоединиться к комнате",
"message_retention": "История сообщений",
"message_retention_info": "Сообщения, отправленные в течение этого срока, могут просматривать все, у кого есть ссылка.",
"direct_link": "Моя прямая ссылка",
"title": "Детали комнаты",
"created_by": "Создано {user}",
"permissions": "Разрешения на присоединение",
"join_invite": "Только добавленные люди",
"link_copied": "Ссылка скопирована!",
"purge": "Удалить комнату",
"user": "{user}",
"user_you": "{user} (вы)",
"show_all": "Показать все >",
"export_room": "Экспорт чата",
"moderation": "Модерация",
"room_type": "Тип комнаты",
"voice_mode": "Голосовой режим",
"voice_mode_info": "Переключает интерфейс чата в режим \"слушать и записывать\"",
"file_mode": "Файловый режим",
"download_chat": "Скачать чат",
"read_only_room_info": "Отправлять сообщения в комнату могут только администраторы и модераторы.",
"message_retention_4_week": "4 недели",
"make_public_warning": "предупреждение: Полная история сообщений будет видна новым участникам",
"shared_room_number": "Вы делите {count} комнат с {name}",
"file_mode_info": "Переключает интерфейс чата в режим \"сброса файлов\"",
"make_public": "Сделать публичным",
"direct_link_desc": "Он готов к обмену! Каждый раз, когда кто-то открывает ссылку, будет открываться новая прямая комната.",
"version_info": "Работает под управлением Guardian Project. Версия: {version}",
"shared_room_number_more": "Вы делите более {count} комнат с {name}",
"experimental_features": "Экспериментальная функция"
},
"file_mode": {
"sending": "Отправляем",
"sending_progress": "Отправка...",
"close": "Закрыть",
"files": "Файлы",
"files_sent": "Отправлен 1 файл! | {count} отправленных файлов!",
"any_file_format_accepted": "Принимаются файлы любого формата",
"send_more_files": "Отправить больше файлов",
"secure_file_send": "безопасная отправка файлов",
"add_a_message": "Добавить сообщение",
"files_sent_with_note": "1 файл отправлен с примечанием! | {count} файлов, отправленных с примечанием!",
"choose_files": "Выбрать файл"
},
"global": {
"save": "Сохранить",
"show_less": "Показать меньше",
"show_more": "Показать больше",
"time": {
"recently": "только что",
"minutes": "1 минуту назад | {n} минут назад",
"hours": "1 час назад | {n} часов назад",
"days": "1 день назад | {n} дней назад"
},
"notify": "Оповестить",
"password_didnot_match": "Пароль не совпадает",
"password_hint": "Минимум 12 символов, содержащих как минимум одну цифру, одну заглавную и одну строчную буквы",
"add_reaction": "Добавить реакцию",
"click_to_remove": "Нажмите, чтобы удалить",
"close": "закрыть"
},
"message": {
"download_all": "Скачать все",
"someone": "Кто-то",
"file_prefix": "Файл: ",
"edited": "(отредактировано)",
"reply_image": "Изображение",
"reply_video": "Видео",
"reply_poll": "Опрос",
"seen_by": "Увиденное",
"file": "Файл",
"files": "Файлы",
"images": "Изображения",
"you": "Вы",
"user_aliased_room": "{user} создал псевдоним {alias} для комнаты",
"user_changed_display_name": "{user} изменил отображаемое имя на {displayName}",
"user_changed_avatar": "{user} изменил аватар",
"user_changed_room_avatar": "{user} изменил аватар комнаты",
"user_was_invited": "{user} был приглашен в чат...",
"user_was_kicked": "{user} был удален из чата.",
"user_was_banned": "{user} был удален и заблокирован в чате.",
"user_was_banned_you": "Вы были удалены из чата и заблокированы.",
"user_joined": "{user} присоединился к чату",
"user_left": "{user} покинул чат",
"user_said": "{user} сказал:",
"sent_media": "Отправлено {count} медиаэлементов.",
"preparing_to_upload": "Подготовка к загрузке...",
"upload_file_too_large": "Файл слишком велик для загрузки!",
"upload_progress": "Загружено {count}",
"upload_progress_with_total": "Загружено {count} из {total}",
"user_changed_room_history": "{user} сделал историю номера {type}",
"room_history_world_readable": "читаемый всеми",
"room_history_shared": "читаемый всеми участниками в комнате",
"user_changed_join_rules": "{user} сделал комнату {type}",
"room_joinrule_invite": "только по приглашению",
"user_changed_room_topic": "{user} изменил тему комнаты на {topic}",
"unread_messages": "Непрочитанные сообщения",
"user_is_typing": "{user} набирает",
"users_are_typing": "{count} членов набирают",
"room_powerlevel_change": "{user} изменил уровень мощности {changes}",
"user_powerlevel_change_from_to": "{user} из {powerOld} в {powerNew}",
"user_changed_guest_access_closed": "{user} запретил гостям входить в комнату",
"user_changed_guest_access_open": "{user} разрешил гостям присоединиться к комнате",
"reply_audio_message": "Аудиосообщение",
"outgoing_message_deleted_text": "Вы удалили это сообщение.",
"incoming_message_deleted_text": "Это сообщение было удалено.",
"user_created_room": "{user} создал комнату",
"user_encrypted_room": "{user} сделал комнату зашифрованной",
"user_was_kicked_by_you": "Вы выгнали {user} из чата.",
"user_was_kicked_you": "Вы были удалены из чата.",
"user_was_banned_by_you": "Вы выгнали и забанили {user} из чата.",
"download_progress": "{percentage}% скачанных",
"upload_exceeded_file_limit": "Превышен максимальный размер файла ({configFormattedUploadSize}). ",
"room_history_invited": "доступны для чтения членам клуба с того момента, когда они были приглашены",
"room_history_joined": "доступны для чтения членам клуба с момента их присоединения",
"room_joinrule_public": "публичный",
"user_changed_room_name": "{user} изменил название комнаты на {name}",
"replying_to": "Отвечая на {user}",
"your_message": "Ваше сообщение...",
"scale_image": "Масштаб изображения",
"time_ago": "Сегодня | Вчера | {count} дней назад",
"not_allowed_to_send": "Только администраторы и модераторы могут отправлять сообщения в комнату",
"reaction_count_more": "{reactionCount} больше",
"seen_by_count": "Видели ни один участник | Видел 1 участник | Видели {count} участников",
"send_attachements_dialog_title": "Вы хотите отправить следующие вложения?",
"failed_to_render": "Не удалось отрендерить событие"
},
"new_room": {
"create": "Создание",
"next": "Вперёд",
"options": "Параметры",
"new_room": "Новая комната",
"name_room": "Название комнаты",
"join_permissions_info": "Эти разрешения определяют, как люди могут присоединиться к комнате и как легко пригласить других. Они могут быть изменены в любое время.",
"set_join_permissions": "Установка разрешений на присоединение",
"room_topic": "Добавьте описание, если хотите",
"join_permissions": "Разрешения на присоединение",
"get_link": "Получить ссылку",
"add_people": "Добавить людей",
"link_copied": "Ссылка скопирована!",
"colon_not_allowed": "Двоеточие запрещено",
"public_info": "У кого-нибудь есть ссылка",
"public_description": "Получите ссылку, чтобы поделиться",
"invite_info": "Добавлены только люди",
"invite_description": "Выберите из списка или выполните поиск по идентификатору счета",
"status_creating": "Создание комнаты",
"status_avatar_total": "Загрузка аватара: {count} из {total}",
"status_avatar": "Загрузка аватара: {count}",
"room_name_limit_error_msg": "Разрешено не более 50 символов"
},
"room_info_sheet": {
"view_details": "Подробнее",
"this_room": "Эта комната"
},
"power_level": {
"default": "по умолчанию",
"admin": "администратор",
"moderator": "модератор",
"custom": "пользовательский ({level})",
"restricted": "Ограничено"
},
"emoji": {
"categories": {
"places": "Места",
"activity": "Активность",
"flags": "Метки",
"objects": "Объекты",
"nature": "Природа",
"symbols": "Символы",
"foods": "Продукция",
"peoples": "Люди",
"frequently": "Часто используемое"
},
"search": "Найти..."
},
"menu": {
"direct_chat": "Личный чат",
"reply": "Ответная",
"edit": "Редактировать",
"delete": "Удалить",
"download": "Загрузить",
"ok": "Ок",
"send": "Отправить",
"login": "Имя пользователя",
"logout": "Выйти из системы",
"undo": "Отменить",
"done": "Готово",
"cancel": "Отмена",
"join": "Присоединяйтесь к",
"ignore": "Игнорировать",
"user_kick_and_ban": "Выгнать",
"start_private_chat": "Прямое сообщение с этим пользователем",
"back": "НАЗАД",
"new_room": "Новая комната",
"delete_now": "Удалить сейчас",
"loading": "Загрузка {appName}",
"user_revoke_moderator": "Отозвать модератора",
"user_make_moderator": "Сделать модератором",
"user_make_admin": "Сделать администратором"
},
"room": {
"leave": "Оставить",
"room_list_invites": "Приглашения",
"room_list_rooms": "Комнаты",
"unseen_messages": "У вас нет непросмотренных сообщений | У вас 1 непросмотренное сообщение | У вас {count} непросмотренных сообщений",
"members": "нет членов | 1 член | {count} членов",
"purge_set_room_state": "Установка состояния комнаты",
"purge_removing_members": "Удаление членов ({count} из {total})",
"purge_failed": "Не удалось очистить комнату!",
"room_name_required": "Название комнаты обязательно",
"room_topic_required": "Описание номера обязательно",
"invitations": "У вас нет приглашений | У вас есть 1 приглашение | У вас есть {count} приглашений",
"purge_redacting_events": "Сокращение событий ({count} из {total})",
"room_list_new_messages": "{count} новых сообщений"
},
"room_welcome": {
"got_it": "Есть",
"change": "Изменить",
"encrypted": "Сообщения шифруются из конца в конец.",
"room_history_is": "История комнаты - {type}.",
"join_invite": "Присоединиться могут только те, кого вы пригласили.",
"no_past_messages": "Добро пожаловать! Для вашей безопасности прошлые сообщения недоступны.",
"direct_info": "Привет, {you}. Вы находитесь в приватном чате с {user}.",
"join_channel": "Все готово! Пригласите людей присоединиться к вам: {link}",
"info_retention": "🕓 Сообщения, отправленные в течение {time}, могут просматривать все, у кого есть ссылка.",
"room_history_joined": "Люди могут видеть сообщения, отправленные только после того, как они присоединились.",
"join_public": "Любой желающий может присоединиться, открыв эту ссылку: {link}.",
"info_permissions": "Вы можете в любой момент изменить \"разрешение на присоединение\" в настройках комнаты.",
"info": "Добро пожаловать! Вот несколько вещей, которые нужно знать о вашей комнате:",
"direct_private_chat": "Личное сообщение",
"info_retention_user": "🕓 Сообщения старше {time} будут удалены из истории."
},
"device_list": {
"blocked": "Заблокированный",
"verified": "Проверенные",
"not_verified": "Не проверенные"
},
"getlink": {
"next": "Вперёд",
"continue": "Продолжить",
"hello": "Здравствуйте {user},\nВот ваша прямая ссылка",
"scan_title": "Отсканируйте этот код, чтобы начать прямой разговор",
"different_link": "Получите другую ссылку",
"share_qr": "Поделиться QR",
"qr_image_copied": "Изображение копируется в буфер обмена",
"title": "Получить прямую ссылку",
"info": "Прямые ссылки дают людям возможность безопасно общаться с вами. Для начала выберите экранное имя, которое будет отображаться при входе в чат с вами.",
"ready_to_share": "Он готов к обмену! Каждый раз, когда кто-то открывает ссылку, будет открываться новая прямая комната.",
"username": "Введите имя экрана (например: waku)"
},
"profile": {
"password_old": "Старый пароль",
"password_new": "Новый пароль",
"password_repeat": "Повторите новый пароль",
"display_name": "Отображаемое имя",
"notification_label": "Уведомления",
"set_password": "Задать пароль",
"change_password": "Изменить пароль",
"select_language": "Язык",
"title": "Мой профиль",
"temporary_identity": "Этот идентификатор является временным. Установите пароль, чтобы использовать ее снова",
"change_name": "Изменить имя",
"set_language": "Установите свой язык",
"language_description": "Convene доступен на многих языках.",
"dont_see_yours": "Не видите своего?",
"tell_us": "Расскажите нам.",
"display_name_required": "Отображаемое имя обязательно"
},
"join": {
"user_name_label": "Имя пользователя",
"remember_me": "Запомнить меня",
"join": "Присоединиться к комнате",
"enter_room": "Войти в комнату",
"joining_as": "Вы присоединяетесь как:",
"join_user": "Начать общение",
"you_have_been_banned": "Вам запрещено посещать эту комнату.",
"title": "Добро пожаловать, вы приглашены присоединиться",
"title_user": "Добро пожаловать, вас пригласили пообщаться с",
"enter_room_user": "Начать общение",
"status_joining": "Присоединяюсь к комнате...",
"join_failed": "Не удалось присоединиться к комнате.",
"choose_name": "Выберите имя для использования",
"status_logging_in": "Вход…"
},
"leave": {
"title_invite": "Вы уверены, что хотите выйти?",
"leave": "Оставить",
"go_back": "Назад",
"text_invite": "Эта комната заперта. Вы не сможете войти в нее без специального разрешения.",
"create_account": "создать аккаунт",
"title_public": "До свидания, {user}",
"text_public": "Вы всегда можете присоединиться к этой комнате снова, если знаете ссылку.",
"text_public_lastroom": "Если вы хотите присоединиться к этой комнате снова, вы можете присоединиться под новым именем. Чтобы сохранить {user}, {action}."
},
"purge_room": {
"button": "Удалить",
"n_seconds": "{seconds} секунды",
"title": "Удалить комнату?",
"info": "Все пользователи и сообщения будут удалены. Это действие нельзя отменить.",
"self_destruct": "Ваша комната самоуничтожится в считанные секунды.",
"deleting": "Удаление комнаты:",
"notified": "Мы уведомили членов клуба.",
"room_deletion_notice": "Время прощаться! Эта комната была удалена {user}. Она самоуничтожится через несколько секунд."
},
"poll_create": {
"poll_submit": "Отправить",
"create": "Опубликовать",
"answer_label_n": "Ответ",
"title": "Создать новый опрос",
"answer_label_1": "Ответ*",
"tip_title": "СОВЕТ ПРОФЕССИОНАЛА",
"creating": "Создайте опрос",
"poll_disclosed": "Открыто - текущие результаты отображаются постоянно.",
"poll_undisclosed": "Закрыто - пользователи увидят результаты, когда опрос будет закрыт.",
"add_answer": "Добавить ответ",
"failed": "Не удалось создать опрос, повторите попытку позже.",
"question_label": "Задайте свой вопрос*",
"please_complete": "Пожалуйста, заполните",
"create_poll_menu_option": "Создать опрос",
"poll_status_closed": "Опрос закрыт",
"poll_status_disclosed": "Результаты будут показаны после закрытия опроса.",
"poll_status_open": "Опрос открыт",
"poll_status_open_not_voted": "Опрос открыт - проголосуйте, чтобы увидеть результаты",
"view_results": "Посмотреть результаты",
"close_poll": "Закрытый опрос",
"question_required": "Вам нужно ввести вопрос!",
"answer_required": "Ответ не может быть пустым. Пожалуйста, введите текст или удалите этот вариант.",
"tip_text": "Участники увидят результаты опроса после того, как ответят. Закройте опрос, когда закончите, чтобы показать результаты всем присутствующим в комнате.",
"num_answered": "{count} ответов",
"results_shared": "Результаты переданы в зал."
},
"profile_info_popup": {
"edit_profile": "Редактировать профиль",
"logout": "Выйти из системы",
"you_are": "Вы",
"identity": "{displayName}",
"identity_temporary": "{displayName}",
"want_more": "Хотите больше?",
"powered_by": "В этой комнате используется {product}. Узнайте больше на {productLink} или создайте другую комнату!",
"new_room": "Новая комната"
},
"invite": {
"title": "Добавить друзей",
"done": "Готово",
"send_invites_to": "Присылайте приглашения по адресу",
"status_error": "Не удалось пригласить одного или нескольких друзей!",
"status_inviting": "Приглашение друга {index} из {count}"
},
"voice_recorder": {
"not_supported_title": "Не поддерживается",
"not_supported_text": "К сожалению, этот браузер не поддерживает запись звука.",
"swipe_to_cancel": "Проведите пальцем, чтобы отменить",
"release_to_cancel": "Отпустить, чтобы отменить",
"failed_to_record": "Не удалось записать звук"
},
"fallbacks": {
"download_name": "Загрузить",
"original_text": "<Оригинальный текст>",
"audio_file": "Аудиофайлы",
"video_file": "Видеофайлы"
},
"notification": {
"dialog": {
"enable": "Включить",
"body": "Никогда больше не пропустите ни одного сообщения или важного разговора! Получайте уведомления, когда кто-то отправляет вам сообщение или отвечает в вашем чате.",
"title": "Оставайтесь на связи с уведомлениями в чате!"
},
"blocked_message": "Уведомление заблокировано. Перейдите в настройки устройства или браузера, чтобы включить уведомление",
"not_supported": "Уведомления пока не поддерживаются в мобильной версии",
"title": "Получено новое сообщение",
"periodicSync_new_msg_reminder": "У вас могут появиться новые сообщения"
},
"language_display_name": "Русский",
"project": {
"name": "Созыв",
"tag_line": "Просто подключите"
},
"logout": {
"confirm_text": "Вы уверены, что хотите выйти из системы?"
},
"goodbye": {
"room_deleted": "Комната удалена.",
"close_tab": "Закрыть вкладку браузера",
"view_other_rooms": "Посмотреть другие комнаты"
},
"createchannel": {
"title": "Создать канал",
"info": "Транслируйте новости или знания в любом формате - видео, подкаст, текст, картинки или PDF-файлы.",
"channel_name": "Назовите свой канал",
"channel_topic": "Опишите его",
"name_required": "Имя канала обязательно",
"error_channel": "Не удалось создать канал"
},
"export": {
"fetched_n_events": "Найдено {count} событий",
"fetched_n_of_total_events": "Получено {count} из {total} событий",
"export_filename": "Экспортированный чат {date}",
"processed_n_of_total_events": "Обработка носителей для {count} из {total} событий",
"exported_date": "Экспортировано в {date}"
}
}

View file

@ -113,5 +113,8 @@
"dialog": { "dialog": {
"enable": "සක්‍රිය කරන්න" "enable": "සක්‍රිය කරන්න"
} }
},
"room_welcome": {
"change": "වෙනස් කරන්න"
} }
} }

View file

@ -30,7 +30,7 @@
"voice_mode": "语音模块", "voice_mode": "语音模块",
"voice_mode_info": "将聊天界面切换到“收听和录音”模式", "voice_mode_info": "将聊天界面切换到“收听和录音”模式",
"download_chat": "下载聊天", "download_chat": "下载聊天",
"read_only_room": "只读聊天室", "read_only_room": "只读",
"read_only_room_info": "只允许管理员和版主发送到聊天室", "read_only_room_info": "只允许管理员和版主发送到聊天室",
"export_room": "导出聊天", "export_room": "导出聊天",
"user_moderator": "版主", "user_moderator": "版主",
@ -47,7 +47,9 @@
"message_retention_none": "关闭", "message_retention_none": "关闭",
"message_retention_1_day": "1 天", "message_retention_1_day": "1 天",
"message_retention_8_hours": "8 小时", "message_retention_8_hours": "8 小时",
"message_retention_1_hour": "1 小时" "message_retention_1_hour": "1 小时",
"message_retention_2_week": "2周",
"message_retention_1_week": "1周"
}, },
"leave": { "leave": {
"leave": "离开", "leave": "离开",
@ -292,7 +294,8 @@
"encrypted": "信息是端到端加密的。", "encrypted": "信息是端到端加密的。",
"no_past_messages": "欢迎! 为了您的安全,过去的信息不提供。", "no_past_messages": "欢迎! 为了您的安全,过去的信息不提供。",
"direct_info": "你好,{you}.正在与 {user} 进行私人聊天。", "direct_info": "你好,{you}.正在与 {user} 进行私人聊天。",
"direct_private_chat": "私信" "direct_private_chat": "私信",
"change": "更改"
}, },
"profile_info_popup": { "profile_info_popup": {
"new_room": "新的聊天室", "new_room": "新的聊天室",

View file

@ -54,39 +54,45 @@
<component :is="roomWelcomeHeader" v-on:close="closeRoomWelcomeHeader"></component> <component :is="roomWelcomeHeader" v-on:close="closeRoomWelcomeHeader"></component>
<!-- If we have a retention timer, it means we have active message retention. Show header. -->
<WelcomeHeaderChannelUser v-if="retentionTimer && !roomWelcomeHeader" />
<div v-for="(event, index) in filteredEvents" :key="event.getId()" :eventId="event.getId()"> <div v-for="(event, index) in filteredEvents" :key="event.getId()" :eventId="event.getId()">
<!-- DAY Marker, shown for every new day in the timeline --> <!-- DAY Marker, shown for every new day in the timeline -->
<div v-if="showDayMarkerBeforeEvent(event) && !!componentForEvent(event, isForExport = false)" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div> <div v-if="showDayMarkerBeforeEvent(event) && !!componentForEvent(event, isForExport = false)" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div>
<div v-if="!event.isRelation() && !event.isRedaction()" :ref="event.getId()"> <div v-if="!event.isRelation() && !event.isRedaction()" :ref="event.getId()">
<div class="message-wrapper" v-on:touchstart=" <MessageErrorHandler>
(e) => { <div class="message-wrapper" v-on:touchstart="
touchStart(e, event); (e) => {
} touchStart(e, event);
" v-on:touchend="touchEnd" v-on:touchcancel="touchCancel" v-on:touchmove="touchMove"> }
<!-- Note: For threaded media messages, IF there is only one item we show that media item as a single component. " v-on:touchend="touchEnd" v-on:touchcancel="touchCancel" v-on:touchmove="touchMove">
We might therefore get calls to v-on:context-menu that has the event set to that single media item, not the top level thread event <!-- Note: For threaded media messages, IF there is only one item we show that media item as a single component.
that is really displayed in the flow. Therefore, we rewrite these events with "{event: event, anchor: $event.anchor}", We might therefore get calls to v-on:context-menu that has the event set to that single media item, not the top level thread event
see below. Otherwise things like context menus won't work as designed. that is really displayed in the flow. Therefore, we rewrite these events with "{event: event, anchor: $event.anchor}",
--> see below. Otherwise things like context menus won't work as designed.
<component :is="componentForEvent(event)" :room="room" :originalEvent="event" :nextEvent="filteredEvents[index + 1]" -->
:timelineSet="timelineSet" v-on:send-quick-reaction.stop="sendQuickReaction" <component :is="componentForEvent(event)" :room="room" :originalEvent="event" :nextEvent="filteredEvents[index + 1]"
:componentFn="componentForEvent" :timelineSet="timelineSet" v-on:send-quick-reaction.stop="sendQuickReaction"
v-on:context-menu="showContextMenuForEvent({event: event, anchor: $event.anchor})" :componentFn="componentForEvent"
v-on:own-avatar-clicked="viewProfile" v-on:context-menu="showContextMenuForEvent({event: event, anchor: $event.anchor})"
v-on:other-avatar-clicked="showAvatarMenuForEvent({event: event, anchor: $event.anchor})" v-on:own-avatar-clicked="viewProfile"
v-on:download="download(event)" v-on:other-avatar-clicked="showAvatarMenuForEvent({event: event, anchor: $event.anchor})"
v-on:poll-closed="pollWasClosed(event)" v-on:download="download(event)"
v-on:more=" v-on:poll-closed="pollWasClosed(event)"
isEmojiQuickReaction = true v-on:more="
showMoreMessageOperations({event: event, anchor: $event.anchor}) isEmojiQuickReaction = true
" showMoreMessageOperations({event: event, anchor: $event.anchor})
v-on:layout-change="onLayoutChange" "
/> v-on:layout-change="onLayoutChange"
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> --> v-on:addQuickHeartReaction="addQuickHeartReaction({event, position: $event.position})"
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> --> />
<div v-if="event.getId() == readMarker && index < filteredEvents.length - 1" class="read-marker"><div class="line"></div><div class="text">{{ $t('message.unread_messages') }}</div><div class="line"></div></div> <!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
</div> <!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}<br /><br /></div> -->
<div v-if="event.getId() == readMarker && index < filteredEvents.length - 1" class="read-marker"><div class="line"></div><div class="text">{{ $t('message.unread_messages') }}</div><div class="line"></div></div>
</div>
</MessageErrorHandler>
</div> </div>
</div> </div>
@ -109,7 +115,7 @@
<div v-if="replyToContentType === 'm.text'" class="reply-text" :title="replyToEvent.getContent().body"> <div v-if="replyToContentType === 'm.text'" class="reply-text" :title="replyToEvent.getContent().body">
{{ replyToEvent.getContent().body | latestReply }} {{ replyToEvent.getContent().body | latestReply }}
</div> </div>
<div v-if="replyToContentType === 'm.thread'">{{ replyToThreadMessage }}</div> <div v-if="replyToContentType === 'm.thread' || replyToContentType === 'io.element.thread'">{{ replyToThreadMessage }}</div>
<div v-if="replyToContentType === 'm.image'">{{ $t("message.reply_image") }}</div> <div v-if="replyToContentType === 'm.image'">{{ $t("message.reply_image") }}</div>
<div v-if="replyToContentType === 'm.audio'">{{ $t("message.reply_audio_message") }}</div> <div v-if="replyToContentType === 'm.audio'">{{ $t("message.reply_audio_message") }}</div>
<div v-if="replyToContentType === 'm.video'">{{ $t("message.reply_video") }}</div> <div v-if="replyToContentType === 'm.video'">{{ $t("message.reply_video") }}</div>
@ -209,7 +215,7 @@
</v-container> </v-container>
<input ref="attachment" type="file" name="attachment" @change="handlePickedAttachment($event)" <input ref="attachment" type="file" name="attachment" @change="handlePickedAttachment($event)"
accept="image/*,audio/*,video/*,.pdf,application/pdf,.apk,application/vnd.android.package-archive,.ipa,.zip,application/zip,application/x-zip-compressed,multipart/x-zip" class="d-none" multiple/> accept="image/*,audio/*,video/*,.mp3,.mp4,.wav,.m4a,.pdf,application/pdf,.apk,application/vnd.android.package-archive,.ipa,.zip,application/zip,application/x-zip-compressed,multipart/x-zip" class="d-none" multiple/>
<div v-if="currentFileInputsDialog && !useFileModeNonAdmin"> <div v-if="currentFileInputsDialog && !useFileModeNonAdmin">
<v-dialog v-model="currentFileInputsDialog" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'" persistent scrollable> <v-dialog v-model="currentFileInputsDialog" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'" persistent scrollable>
@ -251,7 +257,7 @@
</span> </span>
<v-switch v-if="currentImageInput && currentImageInput.scaled" :label="$t('message.scale_image')" <v-switch v-if="currentImageInput && currentImageInput.scaled" :label="$t('message.scale_image')"
v-model="currentImageInput.useScaled" :disabled="currentImageInput.sendInfo" /> v-model="currentImageInput.useScaled" :disabled="currentImageInput && currentImageInput.sendInfo !== undefined" />
</div> </div>
</div> </div>
</v-card-text> </v-card-text>
@ -277,7 +283,7 @@
{{ $t("menu.cancel") }} {{ $t("menu.cancel") }}
</v-btn> </v-btn>
<v-btn id="btn-attachment-send" color="primary" text @click="sendAttachment(undefined)" <v-btn id="btn-attachment-send" color="primary" text @click="sendAttachment(undefined)"
v-if="currentSendShowSendButton" :disabled="sendingStatus != sendStatuses.INITIAL">{{ $t("menu.send") }}</v-btn> v-if="currentSendShowSendButton" :disabled="currentSendShowSendButton && sendingStatus != sendStatuses.INITIAL">{{ $t("menu.send") }}</v-btn>
</v-card-actions> </v-card-actions>
</template> </template>
</v-card> </v-card>
@ -327,6 +333,11 @@
<!-- PURGE ROOM POPUP --> <!-- PURGE ROOM POPUP -->
<PurgeRoomDialog :show="showPurgeConfirmation" :room="room" @close="showPurgeConfirmation = false" /> <PurgeRoomDialog :show="showPurgeConfirmation" :room="room" @close="showPurgeConfirmation = false" />
<!-- Heart animation -->
<div :class="['heart-wrapper', { 'is-active': heartAnimation }]" :style="hearAnimationPosition">
<div :class="['heart', { 'is-active': heartAnimation }]" />
</div>
</div> </div>
</template> </template>
@ -342,6 +353,7 @@ import RoomInfoBottomSheet from "./RoomInfoBottomSheet";
import WelcomeHeaderRoom from "./welcome_headers/WelcomeHeaderRoom"; import WelcomeHeaderRoom from "./welcome_headers/WelcomeHeaderRoom";
import WelcomeHeaderDirectChat from "./welcome_headers/WelcomeHeaderDirectChat"; import WelcomeHeaderDirectChat from "./welcome_headers/WelcomeHeaderDirectChat";
import WelcomeHeaderChannel from "./welcome_headers/WelcomeHeaderChannel"; import WelcomeHeaderChannel from "./welcome_headers/WelcomeHeaderChannel";
import WelcomeHeaderChannelUser from "./welcome_headers/WelcomeHeaderChannelUser";
import NoHistoryRoomWelcomeHeader from "./NoHistoryRoomWelcomeHeader.vue"; import NoHistoryRoomWelcomeHeader from "./NoHistoryRoomWelcomeHeader.vue";
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet"; import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet";
import StickerPickerBottomSheet from "./StickerPickerBottomSheet"; import StickerPickerBottomSheet from "./StickerPickerBottomSheet";
@ -356,6 +368,7 @@ import FileDropLayout from "./file_mode/FileDropLayout";
import roomTypeMixin from "./roomTypeMixin"; import roomTypeMixin from "./roomTypeMixin";
import roomMembersMixin from "./roomMembersMixin"; import roomMembersMixin from "./roomMembersMixin";
import PurgeRoomDialog from "../components/PurgeRoomDialog"; import PurgeRoomDialog from "../components/PurgeRoomDialog";
import MessageErrorHandler from "./MessageErrorHandler";
const sizeOf = require("image-size"); const sizeOf = require("image-size");
const dataUriToBuffer = require("data-uri-to-buffer"); const dataUriToBuffer = require("data-uri-to-buffer");
@ -409,6 +422,8 @@ export default {
FileDropLayout, FileDropLayout,
UserProfileDialog, UserProfileDialog,
PurgeRoomDialog, PurgeRoomDialog,
WelcomeHeaderChannelUser,
MessageErrorHandler
}, },
data() { data() {
@ -493,6 +508,11 @@ export default {
retentionTimer: null, retentionTimer: null,
showProfileDialog: false, showProfileDialog: false,
showPurgeConfirmation: false, showPurgeConfirmation: false,
heartAnimation: false,
heartPosition: {
top: 0,
left: 0
}
}; };
}, },
@ -533,6 +553,9 @@ export default {
}, },
computed: { computed: {
heartEmoji() {
return this.$refs.emojiPicker.mapEmojis["Symbols"].find(({ aliases }) => aliases.includes('heart')).data;
},
compActiveMember() { compActiveMember() {
const currentUserId= this.selectedEvent?.sender.userId || this.$matrix.currentUserId const currentUserId= this.selectedEvent?.sender.userId || this.$matrix.currentUserId
return this.joinedAndInvitedMembers.find(({userId}) => userId === currentUserId) return this.joinedAndInvitedMembers.find(({userId}) => userId === currentUserId)
@ -764,6 +787,12 @@ export default {
.filter((e) => util.downloadableTypes().includes(e.getContent().msgtype)).length}); .filter((e) => util.downloadableTypes().includes(e.getContent().msgtype)).length});
} }
return ""; return "";
},
hearAnimationPosition() {
return {
'--top': this.heartPosition.top,
'--left': this.heartPosition.left
};
} }
}, },
@ -869,13 +898,10 @@ export default {
methods: { methods: {
/** /**
* Set initialLoadDone to 'true'. First process all events, setting threadParent and replyEvent if needed. * Set initialLoadDone to 'true'. This will process all events, setting threadParent and replyEvent if needed (see watcher for "initialLoadDone")
*/ */
setInitialLoadDone() { setInitialLoadDone() {
this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => this.setParentThread(event));
this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => this.setReplyToEvent(event));
this.initialLoadDone = true; this.initialLoadDone = true;
console.log("Loading finished!");
}, },
/** /**
@ -998,7 +1024,7 @@ export default {
}) })
.catch((err) => { .catch((err) => {
console.log("Error fetching events!", err, this); console.log("Error fetching events!", err, this);
if (err.errcode == "M_UNKNOWN" && initialEventId) { if (initialEventId) {
// Try again without initial event! // Try again without initial event!
this.onRoomJoined(null); this.onRoomJoined(null);
} else { } else {
@ -1146,7 +1172,8 @@ export default {
Vue.set(event, "parentThread", parentEvent); Vue.set(event, "parentThread", parentEvent);
} else { } else {
// Try to load from server. // Try to load from server.
this.$matrix.matrixClient.getEventTimeline(this.timelineSet, event.threadRootId).then((tl) => { this.$matrix.matrixClient.getEventTimeline(this.timelineSet, event.threadRootId)
.then((tl) => {
if (tl) { if (tl) {
const parentEvent = tl.getEvents().find((e) => e.getId() === event.threadRootId); const parentEvent = tl.getEvents().find((e) => e.getId() === event.threadRootId);
if (parentEvent) { if (parentEvent) {
@ -1168,7 +1195,7 @@ export default {
} }
} }
} }
}); }).catch(e => console.error(e));
} }
}, },
@ -1569,6 +1596,11 @@ export default {
this.sendQuickReaction({ reaction: e.emoji, event: e.event }); this.sendQuickReaction({ reaction: e.emoji, event: e.event });
}, },
addQuickHeartReaction(e) {
this.heartPosition = e.position
this.sendQuickReaction({ reaction: this.heartEmoji, event: e.event }, true);
},
setReplyToImage(event) { setReplyToImage(event) {
util util
.getThumbnail(this.$matrix.matrixClient, event, this.$config) .getThumbnail(this.$matrix.matrixClient, event, this.$config)
@ -1584,7 +1616,7 @@ export default {
this.replyToEvent = event; this.replyToEvent = event;
this.$refs.messageInput.focus(); this.$refs.messageInput.focus();
if (event.parentThread || event.isThreadRoot || event.isMxThread) { if (event.parentThread || event.isThreadRoot || event.isMxThread) {
this.replyToContentType = 'm.thread'; this.replyToContentType = util.threadMessageType();
} else { } else {
this.replyToContentType = event.getContent().msgtype || 'm.poll'; this.replyToContentType = event.getContent().msgtype || 'm.poll';
} }
@ -1598,8 +1630,16 @@ export default {
}, },
redact(event) { redact(event) {
this.$matrix.matrixClient let promises = [];
.redactEvent(event.getRoomId(), event.getId()) if ((event.isThreadRoot || event.isMxThread) && this.timelineSet) {
// If this is a thread message, make sure to redact all children as well.
const children = this.timelineSet.relations.getAllChildEventsForEvent(event.getId()).filter(e => util.downloadableTypes().includes(e.getContent().msgtype));
promises = children.map((c) => {
return this.$matrix.matrixClient.redactEvent(c.getRoomId(), c.getId(), undefined, { reason: "redactedMedia"});
});
}
promises.push(this.$matrix.matrixClient.redactEvent(event.getRoomId(), event.getId(), undefined, { reason: "redactedThread"}));
Promise.allSettled(promises)
.then(() => { .then(() => {
console.log("Message redacted"); console.log("Message redacted");
}) })
@ -1652,7 +1692,15 @@ export default {
}); });
}, },
sendQuickReaction(e) { showHeartAnimation() {
const self = this;
this.heartAnimation = true;
setTimeout(() => {
self.heartAnimation = false;
}, 1000)
},
sendQuickReaction(e, heartAnimationFlag = false) {
let previousReaction = null; let previousReaction = null;
// Figure out if we have already sent this emoji, in that case redact it again (toggle) // Figure out if we have already sent this emoji, in that case redact it again (toggle)
@ -1679,6 +1727,10 @@ export default {
.catch((err) => { .catch((err) => {
console.log("Failed to send quick reaction:", err); console.log("Failed to send quick reaction:", err);
}); });
if(heartAnimationFlag) {
this.showHeartAnimation();
}
} }
}, },
@ -1909,4 +1961,32 @@ export default {
<style lang="scss"> <style lang="scss">
@import "@/assets/css/chat.scss"; @import "@/assets/css/chat.scss";
.heart-wrapper {
position: fixed;
z-index: -1;
.heart {
width: 100px;
height: 100px;
background: url("../assets/heart.png") no-repeat;
background-position: 0 0;
cursor: pointer;
transition: background-position 1s steps(28);
transition-duration: 0s;
visibility: hidden;
&.is-active {
transition-duration: 1s;
background-position: -2800px 0;
visibility: visible;
z-index: 10000;
}
}
&.is-active {
z-index: 1000;
top: var(--top);
left: var(--left);
}
}
</style> </style>

View file

@ -145,18 +145,18 @@ export default {
preset: "public_chat", preset: "public_chat",
initial_state: initial_state:
[ [
{ // {
type: "m.room.encryption", // type: "m.room.encryption",
state_key: "", // state_key: "",
content: { // content: {
algorithm: "m.megolm.v1.aes-sha2", // algorithm: "m.megolm.v1.aes-sha2",
}, // },
}, // },
{ {
type: "m.room.history_visibility", type: "m.room.history_visibility",
state_key: "", state_key: "",
content: { content: {
history_visibility: "joined" history_visibility: "shared"
} }
}, },
{ {

View file

@ -73,7 +73,7 @@ export default {
width: 100%; width: 100%;
display: flex; display: flex;
position: relative; position: relative;
min-height: 48px; min-height: $min-touch-target;
background: #f5f5f5; background: #f5f5f5;
border-radius: 4px; border-radius: 4px;

View file

@ -0,0 +1,29 @@
<template>
<div>
<slot
v-if="err"
name="error"
v-bind:err="err"
><div class="text-center">{{ $t('message.failed_to_render') }}</div></slot>
<slot v-else></slot>
</div>
</template>
<script>
export default {
name: "MessageErrorHandler",
data() {
return {
err: false,
};
},
errorCaptured(err, ignoredvm, ignoredinfo) {
this.err = err;
return false;
}
};
</script>
<style lang="scss" scoped>
</style>

View file

@ -117,6 +117,6 @@ export default {
} }
.v-radio.flex-row-reverse { .v-radio.flex-row-reverse {
height: 48px; height: $min-touch-target;
} }
</style> </style>

View file

@ -125,13 +125,6 @@ export default {
}, },
componentForEvent(event, isForExport = false) { componentForEvent(event, isForExport = false) {
if (!event.isRelation() && !event.isRedaction() && event.isRedacted()) {
const redaction = event.getRedactionEvent();
if (redaction && redaction.content && redaction.content.reason === "cancel") {
return null; // Show nothing, it was canceled!
}
}
switch (event.getType()) { switch (event.getType()) {
case "m.room.member": case "m.room.member":
if (event.getContent().membership == "join") { if (event.getContent().membership == "join") {
@ -161,6 +154,13 @@ export default {
case "m.room.message": case "m.room.message":
if (event.getSender() != this.$matrix.currentUserId) { if (event.getSender() != this.$matrix.currentUserId) {
if (event.isRedacted()) {
// Redacted thread, show as text (and hide all media)!
if (event.getUnsigned().redacted_because.content.reason == "redactedThread") {
return MessageIncomingText;
}
return null;
}
if (event.isMxThread) { if (event.isMxThread) {
// Incoming thread, e.g. a file drop! // Incoming thread, e.g. a file drop!
return isForExport ? MessageIncomingThreadExport : MessageIncomingThread; return isForExport ? MessageIncomingThreadExport : MessageIncomingThread;
@ -201,6 +201,13 @@ export default {
} }
return MessageIncomingText; return MessageIncomingText;
} else { } else {
if (event.isRedacted()) {
// Redacted thread, show as text (and hide all media)!
if (event.getUnsigned().redacted_because.content.reason == "redactedThread") {
return MessageOutgoingText;
}
return null;
}
if (event.isMxThread) { if (event.isMxThread) {
// Outgoing thread // Outgoing thread
return isForExport ? MessageOutgoingThreadExport : MessageOutgoingThread; return isForExport ? MessageOutgoingThreadExport : MessageOutgoingThread;

View file

@ -1,16 +1,16 @@
<template> <template>
<v-responsive v-if="item.event.getContent().msgtype == 'm.video' && item.src" :class="{'thumbnail-item': true, 'preview': previewOnly}" <div ref="thumbnailRef">
@click.stop="$emit('itemclick', {item: item})"> <v-responsive v-if="item.event.getContent().msgtype == 'm.video' && item.src" :class="{'thumbnail-item': true, 'preview': previewOnly}">
<video :src="item.src" :controls="!previewOnly" class="w-100 h-100"> <video :src="item.src" :controls="!previewOnly" class="w-100 h-100">
{{ $t('fallbacks.video_file') }} {{ $t('fallbacks.video_file') }}
</video> </video>
</v-responsive> </v-responsive>
<v-img v-else-if="item.event.getContent().msgtype == 'm.image' && item.src" :aspect-ratio="previewOnly ? (16 / 9) : undefined" :class="{'thumbnail-item': true, 'preview': previewOnly}" :src="item.src" :contain="!previewOnly" :cover="previewOnly" <v-img v-else-if="item.event.getContent().msgtype == 'm.image' && item.src" :aspect-ratio="previewOnly ? (16 / 9) : undefined" :class="{'thumbnail-item': true, 'preview': previewOnly}" :src="item.src" :contain="!previewOnly" :cover="previewOnly" />
@click.stop="$emit('itemclick', {item: item})" /> <div v-else :class="{'thumbnail-item': true, 'preview': previewOnly, 'file-item': true}" >
<div v-else :class="{'thumbnail-item': true, 'preview': previewOnly, 'file-item': true}" @click.stop="$emit('itemclick', {item: item})"> <v-icon>{{ fileTypeIcon }}</v-icon>
<v-icon>{{ fileTypeIcon }}</v-icon> <b>{{ $sanitize(fileName) }}</b>
<b>{{ $sanitize(fileName) }}</b> <div>{{ fileSize }}</div>
<div>{{ fileSize }}</div> </div>
</div> </div>
</template> </template>
<script> <script>
@ -54,7 +54,24 @@ export default {
fileSize() { fileSize() {
return util.getFileSizeFormatted(this.item.event); return util.getFileSizeFormatted(this.item.event);
} }
} },
methods: {
// listen for custom hammerJs singletab click to differentiate it from double click(heart animation).
initThumbnailHammerJs(element) {
const hammerInstance = util.singleOrDoubleTabRecognizer(element)
hammerInstance.on("singletap doubletap", (ev) => {
if(ev.type === 'singletap') {
this.$emit('itemclick', { item: this.item })
}
});
}
},
mounted() {
if(this.$refs.thumbnailRef) {
this.initThumbnailHammerJs(this.$refs.thumbnailRef);
}
},
} }
</script> </script>

View file

@ -14,7 +14,9 @@
}}</span> }}</span>
</v-avatar> </v-avatar>
<!-- SLOT FOR CONTENT --> <!-- SLOT FOR CONTENT -->
<slot></slot> <span ref="messageInOutRef">
<slot></slot>
</span>
<div class="op-button" ref="opbutton" v-if="!event.isRedacted()"> <div class="op-button" ref="opbutton" v-if="!event.isRedacted()">
<v-btn id="btn-more" icon @click.stop="showContextMenu($refs.opbutton)"> <v-btn id="btn-more" icon @click.stop="showContextMenu($refs.opbutton)">
<v-icon>more_vert</v-icon> <v-icon>more_vert</v-icon>
@ -28,10 +30,16 @@
<script> <script>
import SeenBy from "./SeenBy.vue"; import SeenBy from "./SeenBy.vue";
import messageMixin from "./messageMixin"; import messageMixin from "./messageMixin";
import util from "../../plugins/utils";
export default { export default {
mixins: [messageMixin], mixins: [messageMixin],
components: { SeenBy } components: { SeenBy },
mounted() {
if(util.isMobileOrTabletBrowser() && this.$refs.messageInOutRef) {
this.initMsgHammerJs(this.$refs.messageInOutRef);
}
}
}; };
</script> </script>

View file

@ -1,13 +1,12 @@
<template> <template>
<message-incoming v-bind="{...$props, ...$attrs}" v-on="$listeners"> <message-incoming v-bind="{...$props, ...$attrs}" v-on="$listeners">
<div class="bubble image-bubble"> <div class="bubble image-bubble" ref="imageRef">
<v-img <v-img
:aspect-ratio="16 / 9" :aspect-ratio="16 / 9"
ref="image" ref="image"
:src="src" :src="src"
:cover="cover" :cover="cover"
:contain="contain" :contain="contain"
@click.stop="dialog = true"
/> />
</div> </div>
<v-dialog <v-dialog
@ -28,12 +27,24 @@ export default {
components: { MessageIncoming }, components: { MessageIncoming },
data() { data() {
return { return {
src: null, src: undefined,
cover: true, cover: true,
contain: false, contain: false,
dialog: false dialog: false
}; };
}, },
methods: {
// listen for custom hammerJs singletab click to differentiate it from double click(heart animation).
initMessageInImageHammerJs(element) {
const hammerInstance = util.singleOrDoubleTabRecognizer(element);
hammerInstance.on("singletap doubletap", (ev) => {
if(ev.type === 'singletap') {
this.dialog = true;
}
});
}
},
mounted() { mounted() {
//console.log("Mounted with event:", JSON.stringify(this.event.getContent())); //console.log("Mounted with event:", JSON.stringify(this.event.getContent()));
const width = this.$refs.image.$el.clientWidth; const width = this.$refs.image.$el.clientWidth;
@ -52,6 +63,9 @@ export default {
this.contain = true; this.contain = true;
} }
this.src = url; this.src = url;
if(this.$refs.imageRef) {
this.initMessageInImageHammerJs(this.$refs.imageRef);
}
}) })
.catch((err) => { .catch((err) => {
console.log("Failed to fetch thumbnail: ", err); console.log("Failed to fetch thumbnail: ", err);

View file

@ -12,7 +12,7 @@
<div class="message"> <div class="message">
<i v-if="event.isRedacted()" class="deleted-text"> <i v-if="event.isRedacted()" class="deleted-text">
<v-icon :color="this.senderIsAdminOrModerator(this.event)?'white':''" size="small">block</v-icon> <v-icon :color="this.senderIsAdminOrModerator(this.event)?'white':''" size="small">block</v-icon>
{{ $t('message.incoming_message_deleted_text')}} {{ redactedBySomeoneElse(event) ? $t('message.incoming_message_deleted_text') : $t('message.outgoing_message_deleted_text')}}
</i> </i>
<span v-html="linkify($sanitize(messageText))" v-else/> <span v-html="linkify($sanitize(messageText))" v-else/>
<span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()"> <span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()">

View file

@ -1,5 +1,5 @@
<template> <template>
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners" v-if="items.length > 1"> <message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners" v-if="items.length > 1 || event.isRedacted()">
<div class="bubble"> <div class="bubble">
<div class="original-message" v-if="inReplyToText"> <div class="original-message" v-if="inReplyToText">
<div class="original-message-sender">{{ inReplyToSender }}</div> <div class="original-message-sender">{{ inReplyToSender }}</div>
@ -10,7 +10,7 @@
</div> </div>
<div class="message"> <div class="message">
<v-container fluid class="imageCollection"> <v-container v-if="!event.isRedacted()" fluid class="imageCollection">
<v-row wrap> <v-row wrap>
<v-col v-for="({ size, item }) in layoutedItems()" :key="item.event.getId()" :cols="size"> <v-col v-for="({ size, item }) in layoutedItems()" :key="item.event.getId()" :cols="size">
<ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" /> <ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
@ -19,7 +19,7 @@
</v-container> </v-container>
<i v-if="event.isRedacted()" class="deleted-text"> <i v-if="event.isRedacted()" class="deleted-text">
<v-icon :color="this.senderIsAdminOrModerator(this.event) ? 'white' : ''" size="small">block</v-icon> <v-icon :color="this.senderIsAdminOrModerator(this.event) ? 'white' : ''" size="small">block</v-icon>
{{ $t('message.incoming_message_deleted_text') }} {{ redactedBySomeoneElse(event) ? $t('message.incoming_message_deleted_text') : $t('message.outgoing_message_deleted_text')}}
</i> </i>
<span v-html="linkify($sanitize(messageText))" v-else /> <span v-html="linkify($sanitize(messageText))" v-else />
<span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()"> <span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()">
@ -53,7 +53,7 @@ export default {
} }
}, },
mounted() { mounted() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message"); this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message");
if (!this.thread) { if (!this.thread) {
this.event.on("Event.relationsCreated", this.onRelationsCreated); this.event.on("Event.relationsCreated", this.onRelationsCreated);
} }
@ -63,7 +63,7 @@ export default {
}, },
methods: { methods: {
onRelationsCreated() { onRelationsCreated() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message"); this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message");
this.event.off("Event.relationsCreated", this.onRelationsCreated); this.event.off("Event.relationsCreated", this.onRelationsCreated);
}, },
onItemClick(event) { onItemClick(event) {
@ -72,7 +72,7 @@ export default {
processThread() { processThread() {
this.$emit('layout-change', () => { this.$emit('layout-change', () => {
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId()) this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId())
.filter(e => util.downloadableTypes().includes(e.getContent().msgtype)) .filter(e => !e.isRedacted() && util.downloadableTypes().includes(e.getContent().msgtype))
.map(e => { .map(e => {
let ret = { let ret = {
event: e, event: e,

View file

@ -14,7 +14,9 @@
</v-btn> </v-btn>
</div> </div>
<!-- SLOT FOR CONTENT --> <!-- SLOT FOR CONTENT -->
<slot></slot> <span ref="messageInOutRef">
<slot></slot>
</span>
<v-avatar <v-avatar
class="avatar" class="avatar"
size="32" size="32"
@ -32,10 +34,16 @@
<script> <script>
import SeenBy from "./SeenBy.vue"; import SeenBy from "./SeenBy.vue";
import messageMixin from "./messageMixin"; import messageMixin from "./messageMixin";
import util from "../../plugins/utils";
export default { export default {
mixins: [messageMixin], mixins: [messageMixin],
components: { SeenBy } components: { SeenBy },
mounted() {
if(util.isMobileOrTabletBrowser() && this.$refs.messageInOutRef) {
this.initMsgHammerJs(this.$refs.messageInOutRef);
}
}
}; };
</script> </script>
<style lang="scss"> <style lang="scss">

View file

@ -1,13 +1,12 @@
<template> <template>
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners"> <message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<div class="bubble image-bubble"> <div class="bubble image-bubble" ref="imageRef">
<v-img <v-img
:aspect-ratio="16 / 9" :aspect-ratio="16 / 9"
ref="image" ref="image"
:src="src" :src="src"
:cover="cover" :cover="cover"
:contain="contain" :contain="contain"
@click.stop="dialog = true"
/> />
</div> </div>
<v-dialog <v-dialog
@ -28,12 +27,24 @@ export default {
components: { MessageOutgoing }, components: { MessageOutgoing },
data() { data() {
return { return {
src: null, src: undefined,
cover: true, cover: true,
contain: false, contain: false,
dialog: false dialog: false
}; };
}, },
methods: {
// listen for custom hammerJs singletab click to differentiate it from double click(heart animation).
initMessageOutImageHammerJs(element) {
const hammerInstance = util.singleOrDoubleTabRecognizer(element);
hammerInstance.on("singletap doubletap", (ev) => {
if(ev.type === 'singletap') {
this.dialog = true;
}
});
}
},
mounted() { mounted() {
const width = this.$refs.image.$el.clientWidth; const width = this.$refs.image.$el.clientWidth;
const height = (width * 9) / 16; const height = (width * 9) / 16;
@ -51,6 +62,9 @@ export default {
this.contain = true; this.contain = true;
} }
this.src = url; this.src = url;
if(this.$refs.imageRef) {
this.initMessageOutImageHammerJs(this.$refs.imageRef);
}
}) })
.catch((err) => { .catch((err) => {
console.log("Failed to fetch thumbnail: ", err); console.log("Failed to fetch thumbnail: ", err);

View file

@ -12,7 +12,7 @@
<div class="message"> <div class="message">
<i v-if="event.isRedacted()" class="deleted-text"> <i v-if="event.isRedacted()" class="deleted-text">
<v-icon size="small">block</v-icon> <v-icon size="small">block</v-icon>
{{ $t('message.outgoing_message_deleted_text')}} {{ redactedBySomeoneElse(event) ? $t('message.incoming_message_deleted_text') : $t('message.outgoing_message_deleted_text')}}
</i> </i>
<span v-html="linkify($sanitize(messageText))" v-else/> <span v-html="linkify($sanitize(messageText))" v-else/>
<span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()"> <span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()">

View file

@ -1,5 +1,5 @@
<template> <template>
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners" v-if="items.length > 1"> <message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners" v-if="items.length > 1 || event.isRedacted()">
<div class="bubble"> <div class="bubble">
<div class="original-message" v-if="inReplyToText"> <div class="original-message" v-if="inReplyToText">
<div class="original-message-sender">{{ inReplyToSender }}</div> <div class="original-message-sender">{{ inReplyToSender }}</div>
@ -11,7 +11,7 @@
<div class="message"> <div class="message">
<v-container fluid class="imageCollection"> <v-container v-if="!event.isRedacted()" fluid class="imageCollection">
<v-row wrap> <v-row wrap>
<v-col v-for="({ size, item }) in layoutedItems()" :key="item.event.getId()" :cols="size"> <v-col v-for="({ size, item }) in layoutedItems()" :key="item.event.getId()" :cols="size">
<ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" /> <ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
@ -20,7 +20,7 @@
</v-container> </v-container>
<i v-if="event.isRedacted()" class="deleted-text"> <i v-if="event.isRedacted()" class="deleted-text">
<v-icon size="small">block</v-icon> <v-icon size="small">block</v-icon>
{{ $t('message.outgoing_message_deleted_text') }} {{ redactedBySomeoneElse(event) ? $t('message.incoming_message_deleted_text') : $t('message.outgoing_message_deleted_text')}}
</i> </i>
<span v-html="linkify($sanitize(messageText))" v-else /> <span v-html="linkify($sanitize(messageText))" v-else />
<span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()"> <span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()">
@ -54,7 +54,7 @@ export default {
} }
}, },
mounted() { mounted() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message"); this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message");
if (!this.thread) { if (!this.thread) {
this.event.on("Event.relationsCreated", this.onRelationsCreated); this.event.on("Event.relationsCreated", this.onRelationsCreated);
} }
@ -64,7 +64,7 @@ export default {
}, },
methods: { methods: {
onRelationsCreated() { onRelationsCreated() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message"); this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message");
this.event.off("Event.relationsCreated", this.onRelationsCreated); this.event.off("Event.relationsCreated", this.onRelationsCreated);
}, },
onItemClick(event) { onItemClick(event) {
@ -73,7 +73,7 @@ export default {
processThread() { processThread() {
this.$emit('layout-change', () => { this.$emit('layout-change', () => {
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId()) this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId())
.filter(e => util.downloadableTypes().includes(e.getContent().msgtype)) .filter(e => !e.isRedacted() && util.downloadableTypes().includes(e.getContent().msgtype))
.map(e => { .map(e => {
let ret = { let ret = {
event: e, event: e,

View file

@ -20,7 +20,7 @@ export default {
} }
}, },
mounted() { mounted() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message"); this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message");
if (!this.thread) { if (!this.thread) {
this.event.on("Event.relationsCreated", this.onRelationsCreated); this.event.on("Event.relationsCreated", this.onRelationsCreated);
} }
@ -30,7 +30,7 @@ export default {
}, },
methods: { methods: {
onRelationsCreated() { onRelationsCreated() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message"); this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message");
this.event.off("Event.relationsCreated", this.onRelationsCreated); this.event.off("Event.relationsCreated", this.onRelationsCreated);
}, },
processThread() { processThread() {

View file

@ -20,7 +20,7 @@ export default {
} }
}, },
mounted() { mounted() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message"); this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message");
if (!this.thread) { if (!this.thread) {
this.event.on("Event.relationsCreated", this.onRelationsCreated); this.event.on("Event.relationsCreated", this.onRelationsCreated);
} }
@ -30,7 +30,7 @@ export default {
}, },
methods: { methods: {
onRelationsCreated() { onRelationsCreated() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message"); this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message");
this.event.off("Event.relationsCreated", this.onRelationsCreated); this.event.off("Event.relationsCreated", this.onRelationsCreated);
}, },
processThread() { processThread() {

View file

@ -3,6 +3,7 @@ import * as linkify from 'linkifyjs';
import linkifyHtml from 'linkify-html'; import linkifyHtml from 'linkify-html';
import utils from "../../plugins/utils" import utils from "../../plugins/utils"
import util from "../../plugins/utils"; import util from "../../plugins/utils";
import Hammer from "hammerjs";
linkify.options.defaults.className = "link"; linkify.options.defaults.className = "link";
linkify.options.defaults.target = { url: "_blank" }; linkify.options.defaults.target = { url: "_blank" };
@ -48,10 +49,10 @@ export default {
event: {}, event: {},
thread: null, thread: null,
utils, utils,
mc: null,
mcCustom: null
}; };
}, },
mounted() {
},
beforeDestroy() { beforeDestroy() {
this.thread = null; this.thread = null;
}, },
@ -278,6 +279,15 @@ export default {
return false; return false;
}, },
redactedBySomeoneElse(event) {
if (!event.isRedacted()) return false;
const redactionEvent = event.getUnsigned().redacted_because;
if (redactionEvent) {
return redactionEvent.sender !== this.$matrix.currentUserId;
}
return false;
},
formatTimeAgo(time) { formatTimeAgo(time) {
const date = new Date(); const date = new Date();
date.setTime(time); date.setTime(time);
@ -326,5 +336,22 @@ export default {
* Override this to handle updates to (the) message thread. * Override this to handle updates to (the) message thread.
*/ */
processThread() {}, processThread() {},
initMsgHammerJs(element) {
this.mc = new Hammer(element);
this.mcCustom = new Hammer.Manager(element);
this.mcCustom.add(new Hammer.Tap({ event: 'doubletap', taps: 2 }));
this.mcCustom.on("doubletap", (evt) => {
var { top, left } = evt.target.getBoundingClientRect();
var position = { top: `${top}px`, left: `${left}px`};
this.$emit("addQuickHeartReaction", { position });
});
this.mc.on("press", () => {
this.showContextMenu(this.$refs.opbutton);
});
}
}, },
}; };

View file

@ -51,6 +51,7 @@ export default {
id: attachment.name, id: attachment.name,
status: this.sendStatuses.INITIAL, status: this.sendStatuses.INITIAL,
statusDate: Date.now, statusDate: Date.now,
mediaEventId: undefined,
attachment: file, attachment: file,
preview: attachment.image, preview: attachment.image,
progress: 0, progress: 0,
@ -74,6 +75,7 @@ export default {
if (item.status !== this.sendStatuses.INITIAL) { if (item.status !== this.sendStatuses.INITIAL) {
return getItemPromise(++index); return getItemPromise(++index);
} }
item.status = this.sendStatuses.SENDING;
const itemPromise = util.sendFile(this.$matrix.matrixClient, this.room.roomId, item.attachment, ({ loaded, total }) => { const itemPromise = util.sendFile(this.$matrix.matrixClient, this.room.roomId, item.attachment, ({ loaded, total }) => {
if (loaded == total) { if (loaded == total) {
item.progress = 100; item.progress = 100;
@ -81,7 +83,7 @@ export default {
item.progress = 100 * loaded / total; item.progress = 100 * loaded / total;
} }
}, eventId) }, eventId)
.then(() => { .then((mediaEventId) => {
// Look at last item rotation, flipping the sign on this, so looks more like a true stack // Look at last item rotation, flipping the sign on this, so looks more like a true stack
let signR = 1; let signR = 1;
let signX = 1; let signX = 1;
@ -100,6 +102,7 @@ export default {
item.randomRotation = signR * (2 + Math.random() * 10); item.randomRotation = signR * (2 + Math.random() * 10);
item.randomTranslationX = signX * Math.random() * 20; item.randomTranslationX = signX * Math.random() * 20;
item.randomTranslationY = signY * Math.random() * 20; item.randomTranslationY = signY * Math.random() * 20;
item.mediaEventId = mediaEventId;
item.status = this.sendStatuses.SENT; item.status = this.sendStatuses.SENT;
item.statusDate = Date.now; item.statusDate = Date.now;
}).catch(ignorederr => { }).catch(ignorederr => {
@ -133,15 +136,17 @@ export default {
}); });
this.sendingStatus = this.sendStatuses.CANCELED; this.sendingStatus = this.sendStatuses.CANCELED;
if (this.sendingRootEventId && this.room) { if (this.sendingRootEventId && this.room) {
// Redact the root event.
this.$matrix.matrixClient // Redact all media we already sent, plus the root event
.redactEvent(this.room.roomId, this.sendingRootEventId, undefined, { reason: "cancel" }) let promises = this.sendingAttachments.filter((item) => item.mediaEventId !== undefined).map((item) => this.$matrix.matrixClient.redactEvent(this.room.roomId, item.mediaEventId, undefined, { reason: "cancel" }));
.then(() => { promises.push(this.$matrix.matrixClient.redactEvent(this.room.roomId, this.sendingRootEventId, undefined, { reason: "cancel" }));
console.log("Message redacted"); Promise.allSettled(promises)
}) .then(() => {
.catch((err) => { console.log("Message redacted");
console.log("Redaction failed: ", err); })
}); .catch((err) => {
console.log("Redaction failed: ", err);
});
} }
}, },

View file

@ -0,0 +1,37 @@
<template>
<div class="created-room-welcome-header">
<div class="mt-2" v-if="roomMessageRetention() > 0">
<i18n path="room_welcome.info_retention_user" tag="span">
<template v-slot:time>
<b>{{ messageRetentionDisplay }}</b>
</template>
</i18n>
</div>
</div>
</template>
<script>
import roomInfoMixin from "../roomInfoMixin";
export default {
name: "WelcomeHeaderChannelUser",
mixins: [roomInfoMixin],
components: {
},
computed: {
},
data() {
return {
}
},
methods: {
onMessageRetention(ignoredretention) {
this.updateMessageRetention();
},
}
};
</script>
<style lang="scss">
@import "@/assets/css/chat.scss";
</style>

View file

@ -33,11 +33,12 @@ const registerPeriodicBackgroundSync = async (registration) => {
} }
} }
export function registerServiceWorker() { export function registerServiceWorker(periodicSyncNewMsgReminderTxt) {
if("serviceWorker" in navigator) { if("serviceWorker" in navigator) {
navigator.serviceWorker.register("./sw.js") navigator.serviceWorker.register("./sw.js")
.then(async registration => { .then(async registration => {
console.log('Service Worker registered with scope:', registration.scope); console.log('Service Worker registered with scope:', registration.scope);
registration.active.postMessage(periodicSyncNewMsgReminderTxt);
await registerPeriodicBackgroundSync(registration); await registerPeriodicBackgroundSync(registration);
}) })
.catch(error => { .catch(error => {

View file

@ -5,6 +5,8 @@ import ImageResize from "image-resize";
import { AutoDiscovery } from 'matrix-js-sdk'; import { AutoDiscovery } from 'matrix-js-sdk';
import User from '../models/user'; import User from '../models/user';
const prettyBytes = require("pretty-bytes"); const prettyBytes = require("pretty-bytes");
import Hammer from "hammerjs";
import { Thread } from 'matrix-js-sdk/lib/models/thread';
export const STATE_EVENT_ROOM_DELETION_NOTICE = "im.keanu.room_deletion_notice"; export const STATE_EVENT_ROOM_DELETION_NOTICE = "im.keanu.room_deletion_notice";
export const STATE_EVENT_ROOM_DELETED = "im.keanu.room_deleted"; export const STATE_EVENT_ROOM_DELETED = "im.keanu.room_deleted";
@ -66,6 +68,11 @@ class UploadPromise {
} }
class Util { class Util {
threadMessageType() {
return Thread.hasServerSideSupport ? "m.thread" : "io.element.thread"
}
getAttachmentUrlAndDuration(event) { getAttachmentUrlAndDuration(event) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const content = event.getContent(); const content = event.getContent();
@ -398,7 +405,7 @@ class Util {
// If thread root (an eventId) is set, add that here // If thread root (an eventId) is set, add that here
if (threadRoot) { if (threadRoot) {
messageContent["m.relates_to"] = { messageContent["m.relates_to"] = {
"rel_type": "m.thread", "rel_type": this.threadMessageType(),
"event_id": threadRoot "event_id": threadRoot
}; };
} }
@ -669,7 +676,8 @@ class Util {
name = name.slice(0, name.indexOf(".")); name = name.slice(0, name.indexOf("."));
name = name.charAt(0).toUpperCase() + name.slice(1); name = name.charAt(0).toUpperCase() + name.slice(1);
const image = r(res); const image = r(res);
images.push({ id: res, image: image, name: "Guest " + name }); const randomNumber = parseInt(this.randomString(4, "0123456789")).toFixed();
images.push({ id: res, image: image, name: "Guest " + name + " " + randomNumber});
}); });
return images; return images;
} }
@ -1014,6 +1022,25 @@ class Util {
} }
return false; return false;
} }
isMobileOrTabletBrowser() {
// Regular expression to match common mobile and tablet browser user agent strings
const mobileTabletPattern = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Tablet|Mobile|CriOS/i;
const userAgent = navigator.userAgent;
return mobileTabletPattern.test(userAgent);
}
singleOrDoubleTabRecognizer(element) {
// reference: https://codepen.io/jtangelder/pen/xxYyJQ
const hm = new Hammer.Manager(element);
hm.add(new Hammer.Tap({ event: 'doubletap', taps: 2 }));
hm.add(new Hammer.Tap({ event: 'singletap' }) );
hm.get('doubletap').recognizeWith('singletap');
hm.get('singletap').requireFailure('doubletap');
return hm
}
} }
export default new Util(); export default new Util();

View file

@ -68,7 +68,7 @@ const vuexPersistSessionStorage = new VuexPersist({
const defaultUseSessionStorage = (sessionStorage.getItem(USER) != null); const defaultUseSessionStorage = (sessionStorage.getItem(USER) != null);
export default new Vuex.Store({ export default new Vuex.Store({
state: { language: null, currentRoomId: null, auth: null, tempuser: null, useLocalStorage: !defaultUseSessionStorage }, state: { language: null, currentRoomId: null, auth: null, tempuser: null, useLocalStorage: !defaultUseSessionStorage, globalNotification: false },
mutations: { mutations: {
loginSuccess(state, user) { loginSuccess(state, user) {
state.auth.status.loggedIn = true; state.auth.status.loggedIn = true;