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:
image: node:20
image: node:20.8.1
stage: build
before_script:
# - ./update_version.sh

View file

@ -1,3 +1,5 @@
var periodicSyncNewMsgReminderText;
// Notification click event listener
self.addEventListener("notificationclick", (e) => {
e.notification.close();
@ -19,6 +21,10 @@ self.addEventListener("notificationclick", (e) => {
);
});
self.addEventListener("message", (event) => {
periodicSyncNewMsgReminderText = event.data;
});
async function checkNewMessages() {
const cachedCredentials = await caches.open('cachedCredentials');
// 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
self.addEventListener('periodicsync', (event) => {
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());
}

View file

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

View file

@ -18,4 +18,6 @@ $voice-recording-color: red;
$voice-recorded-color: #3ae17d;
$poll-hilite-color: #6360f0;
$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 {
margin: 0;
padding: 0;
min-width: 48px;
min-width: $min-touch-target;
&.input-more-icon {
svg {
@ -430,6 +430,10 @@ body {
display: inline-block;
position: relative;
max-width: 70%;
@media #{map-get($display-breakpoints, 'sm-and-down')} {
min-height: $min-touch-target;
}
}
&.from-admin .bubble {
background-color: rgba($admin-bg,0.8);
@ -520,6 +524,10 @@ body {
display: inline-block;
position: relative;
max-width: 70%;
@media #{map-get($display-breakpoints, 'sm-and-down')} {
min-height: $min-touch-target;
}
}
.audio-bubble {
background-color: rgba(#e5e5e5,0.8);
@ -1409,7 +1417,7 @@ body {
bottom: 68px;
left: 8px;
right: 8px;
height: 48px;
height: $min-touch-target;
background: rgba(0, 0, 0, 0.69);
border: 1px solid #000000;
border-radius: 5px;

View file

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

View file

@ -1,4 +1,4 @@
$large-button-height: 48px;
$large-button-height: $min-touch-target;
$small-button-height: 36px;
.file-drop-root {
@ -295,11 +295,11 @@ $small-button-height: 36px;
position: relative;
padding: 8px;
.v-image {
width: 48px;
height: 48px;
width: $min-touch-target;
height: $min-touch-target;
border-radius: 8px;
object-fit: cover;
flex: 0 0 48px;
flex: 0 0 $min-touch-target;
margin-right: 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",
"done": "Fertig",
"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": {
"you": "Du",
@ -68,7 +72,18 @@
"someone": "Jemand",
"file": "Datei",
"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": {
"leave": "Verlassen",
@ -89,7 +104,9 @@
"room_history_is": "Der Raumverlauf ist {type}.",
"room_history_joined": "Die Teilnehmer können nur die Nachrichten sehen, die nach ihrem Beitritt gesendet wurden.",
"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": {
"join_permissions": "Beitrittsberechtigungen",
@ -130,7 +147,10 @@
"invalid_message": "Benutzername oder Passwort falsch",
"send_verification": "Sende Bestätigungs-E-Mail",
"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": {
"title": "Mein Profil",
@ -146,7 +166,8 @@
"set_language": "Stelle deine Sprache ein",
"language_description": "Convene ist in vielen Sprachen verfügbar.",
"tell_us": "Teile uns mit.",
"notification_label": "Benachrichtigung"
"notification_label": "Benachrichtigung",
"display_name_required": "Anzeigename ist erforderlich"
},
"profile_info_popup": {
"you_are": "Du bist",
@ -154,7 +175,7 @@
"edit_profile": "Profil bearbeiten",
"logout": "Abmelden",
"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}",
"want_more": "Willst du mehr?"
},
@ -168,7 +189,9 @@
"title": "Willkommen in {roomName}",
"enter_room": "Raum betreten",
"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": {
"title": "Freunde hinzufügen",
@ -228,7 +251,19 @@
"message_retention_1_week": "1 Woche",
"message_retention_1_day": "1 Tag",
"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": {
"this_room": "Dieser Raum",
@ -278,7 +313,9 @@
"poll_status_open_not_voted": "Umfrage ist offen stimme ab, um die Ergebnisse zu sehen",
"close_poll": "Umfrage schließen",
"answer_label_n": "Antwort",
"view_results": "Ergebnisse ansehen"
"view_results": "Ergebnisse ansehen",
"answer_label_1": "Antwort*",
"please_complete": "Bitte vervollständigen"
},
"export": {
"fetched_n_of_total_events": "{count} von {total} Ereignissen geladen",
@ -294,10 +331,15 @@
"add_reaction": "Reaktion hinzufügen",
"show_less": "Weniger anzeigen",
"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",
"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": {
"categories": {
@ -306,7 +348,8 @@
"flags": "Kennzeichnungen",
"objects": "Objekte",
"symbols": "Symbole",
"places": "Orte"
"places": "Orte",
"frequently": "Häufig genutzt"
},
"search": "Suche ..."
},
@ -314,15 +357,25 @@
"sending": "Sende",
"sending_progress": "Senden…",
"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": {
"dialog": {
"enable": "Aktiviere"
}
},
"title": "Neue Nachricht erhalten"
},
"getlink": {
"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",
"images": "Images",
"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": {
"invitations": "You have no invitations | You have 1 invitation | You have {count} invitations",
@ -140,6 +141,7 @@
"direct_private_chat": "Direct Message",
"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_user": "🕓 Messages older than {time} will be deleted from the history.",
"change": "Change"
},
"new_room": {
@ -406,7 +408,8 @@
"enable": "Enable"
},
"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": {
"search": "Search...",

View file

@ -26,13 +26,13 @@
"room_type_default": "Por Defecto",
"copy_link": "Copiar enlace",
"direct_link": "Mi enlace directo",
"read_only_room": "Sala de sólo lectura",
"read_only_room": "Solo lectura",
"voice_mode": "Por voz",
"make_public_warning": "Advertencia: El historial completo de mensajes será visible para los nuevos participantes",
"room_type": "Genero de la sala",
"experimental_features": "Funciones experimentales",
"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",
"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",
@ -49,7 +49,8 @@
"shared_room_number": "Compartes {count} salas con {name}",
"shared_room_number_more": "Compartes más de {count} y salas con {name}",
"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": {
"button": "Borrar",
@ -173,7 +174,11 @@
"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.",
"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": {
"leave": "Salir",
@ -253,7 +258,8 @@
"time_ago": "Hoy | Ayer | Hace {count} días",
"upload_exceeded_file_limit": "Se ha superado el tamaño máximo para el archivo ({configFormattedUploadSize}). ",
"someone": "Alguien",
"sent_media": "Enviados {count} elementos multimedia."
"sent_media": "Enviados {count} elementos multimedia.",
"failed_to_render": "No se pudo representar el evento"
},
"menu": {
"login": "Iniciar sesión",
@ -411,7 +417,8 @@
},
"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",
"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": {
"fetched_n_of_total_events": "{count} de {total} eventos recuperados",
@ -422,5 +429,13 @@
},
"logout": {
"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_required": "Salasana vaaditaan",
"create_room": "Rekisteröidy ja luo huone",
"accept_terms": "Hyväksy"
"accept_terms": "Hyväksy",
"token_not_valid": "Virheellinen koodi"
},
"join": {
"title": "Tervetuloa huoneen {roomName}",
@ -106,7 +107,8 @@
"reply_poll": "Kysely",
"file": "Tiedosto",
"someone": "Joku",
"images": "Kuvat"
"images": "Kuvat",
"room_joinrule_public": "julkinen"
},
"room": {
"leave": "Poistu",
@ -118,7 +120,8 @@
"info": "Tervetuloa! Seuraavassa on muutamia asioita, jotka sinun on hyvä tietää huoneestasi:",
"info_permissions": "Voit muuttaa liittymisoikeuksia milloin tahansa huoneen asetuksissa.",
"encrypted": "Viestit ovat päästä päähän salattuja.",
"got_it": "Selvä"
"got_it": "Selvä",
"change": "Vaihda"
},
"profile": {
"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_1_day": "1 päivä",
"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": {
"restricted": "rajoitettu",

View file

@ -85,7 +85,8 @@
"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.",
"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": {
"next": "Suivant",
@ -220,7 +221,8 @@
"message_retention_1_week": "1 semaine",
"message_retention_1_day": "1 jour",
"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": {
"this_room": "Ce salon",

View file

@ -84,7 +84,8 @@
"join_invite": "Solo le persone che inviti possono partecipare.",
"info_permissions": "Puoi cambiare i «permessi di adesione» in qualsiasi momento nelle impostazioni della stanza.",
"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": {
"next": "Successivo",
@ -216,7 +217,8 @@
"message_retention_none": "Off",
"message_retention_1_day": "1 giorno",
"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": {
"failed_to_record": "Impossibile registrare laudio",

View file

@ -80,7 +80,8 @@
"message_retention_1_week": "1 uke",
"message_retention_1_day": "1 dag",
"message_retention_8_hours": "åtte timer",
"message_retention_1_hour": "Én time"
"message_retention_1_hour": "Én time",
"read_only_room": "Skrivebeskyttet"
},
"goodbye": {
"view_other_rooms": "Vis andre rom",
@ -137,7 +138,8 @@
"room_history_is": "Romhistorikken er {type}.",
"encrypted": "Meldinger er ende-til-ende -kryptert.",
"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_list_rooms": "Rom",

View file

@ -122,7 +122,8 @@
"upload_exceeded_file_limit": "O tamanho máximo do arquivo ({configFormattedUploadSize}) foi excedido. ",
"preparing_to_upload": "Preparando para enviar...",
"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": {
"members": "sem membros | 1 membro | {count} membros",
@ -150,7 +151,11 @@
"got_it": "Entendi",
"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_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": "Nova sala",
@ -295,11 +300,11 @@
"voice_mode": "Modo de voz",
"user_admin": "Administrador",
"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",
"experimental_features": "Recursos experimentais",
"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",
"make_public": "Tornar público",
"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_4_week": "4 semanas",
"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": {
"this_room": "Esta sala",
@ -408,7 +414,8 @@
"enable": "Ativar"
},
"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": {
"title": "Obter um link direto",
@ -422,5 +429,13 @@
"username": "Insira um nome (ex: waku)",
"different_link": "Obter um link diferente",
"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",
"message_retention_none": "Dezactivat",
"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": {
"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_is": "Istoricul camerei este {type}.",
"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_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": {
"enable": "සක්‍රිය කරන්න"
}
},
"room_welcome": {
"change": "වෙනස් කරන්න"
}
}

View file

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

View file

@ -54,39 +54,45 @@
<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()">
<!-- 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="!event.isRelation() && !event.isRedaction()" :ref="event.getId()">
<div class="message-wrapper" v-on:touchstart="
(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.
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
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"
:componentFn="componentForEvent"
v-on:context-menu="showContextMenuForEvent({event: event, anchor: $event.anchor})"
v-on:own-avatar-clicked="viewProfile"
v-on:other-avatar-clicked="showAvatarMenuForEvent({event: event, anchor: $event.anchor})"
v-on:download="download(event)"
v-on:poll-closed="pollWasClosed(event)"
v-on:more="
isEmojiQuickReaction = true
showMoreMessageOperations({event: event, anchor: $event.anchor})
"
v-on:layout-change="onLayoutChange"
/>
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
<!-- <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>
<MessageErrorHandler>
<div class="message-wrapper" v-on:touchstart="
(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.
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
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"
:componentFn="componentForEvent"
v-on:context-menu="showContextMenuForEvent({event: event, anchor: $event.anchor})"
v-on:own-avatar-clicked="viewProfile"
v-on:other-avatar-clicked="showAvatarMenuForEvent({event: event, anchor: $event.anchor})"
v-on:download="download(event)"
v-on:poll-closed="pollWasClosed(event)"
v-on:more="
isEmojiQuickReaction = true
showMoreMessageOperations({event: event, anchor: $event.anchor})
"
v-on:layout-change="onLayoutChange"
v-on:addQuickHeartReaction="addQuickHeartReaction({event, position: $event.position})"
/>
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</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>
@ -109,7 +115,7 @@
<div v-if="replyToContentType === 'm.text'" class="reply-text" :title="replyToEvent.getContent().body">
{{ replyToEvent.getContent().body | latestReply }}
</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.audio'">{{ $t("message.reply_audio_message") }}</div>
<div v-if="replyToContentType === 'm.video'">{{ $t("message.reply_video") }}</div>
@ -209,7 +215,7 @@
</v-container>
<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">
<v-dialog v-model="currentFileInputsDialog" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'" persistent scrollable>
@ -251,7 +257,7 @@
</span>
<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>
</v-card-text>
@ -277,7 +283,7 @@
{{ $t("menu.cancel") }}
</v-btn>
<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>
</template>
</v-card>
@ -327,6 +333,11 @@
<!-- PURGE ROOM POPUP -->
<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>
</template>
@ -342,6 +353,7 @@ import RoomInfoBottomSheet from "./RoomInfoBottomSheet";
import WelcomeHeaderRoom from "./welcome_headers/WelcomeHeaderRoom";
import WelcomeHeaderDirectChat from "./welcome_headers/WelcomeHeaderDirectChat";
import WelcomeHeaderChannel from "./welcome_headers/WelcomeHeaderChannel";
import WelcomeHeaderChannelUser from "./welcome_headers/WelcomeHeaderChannelUser";
import NoHistoryRoomWelcomeHeader from "./NoHistoryRoomWelcomeHeader.vue";
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet";
import StickerPickerBottomSheet from "./StickerPickerBottomSheet";
@ -356,6 +368,7 @@ import FileDropLayout from "./file_mode/FileDropLayout";
import roomTypeMixin from "./roomTypeMixin";
import roomMembersMixin from "./roomMembersMixin";
import PurgeRoomDialog from "../components/PurgeRoomDialog";
import MessageErrorHandler from "./MessageErrorHandler";
const sizeOf = require("image-size");
const dataUriToBuffer = require("data-uri-to-buffer");
@ -409,6 +422,8 @@ export default {
FileDropLayout,
UserProfileDialog,
PurgeRoomDialog,
WelcomeHeaderChannelUser,
MessageErrorHandler
},
data() {
@ -493,6 +508,11 @@ export default {
retentionTimer: null,
showProfileDialog: false,
showPurgeConfirmation: false,
heartAnimation: false,
heartPosition: {
top: 0,
left: 0
}
};
},
@ -533,6 +553,9 @@ export default {
},
computed: {
heartEmoji() {
return this.$refs.emojiPicker.mapEmojis["Symbols"].find(({ aliases }) => aliases.includes('heart')).data;
},
compActiveMember() {
const currentUserId= this.selectedEvent?.sender.userId || this.$matrix.currentUserId
return this.joinedAndInvitedMembers.find(({userId}) => userId === currentUserId)
@ -764,6 +787,12 @@ export default {
.filter((e) => util.downloadableTypes().includes(e.getContent().msgtype)).length});
}
return "";
},
hearAnimationPosition() {
return {
'--top': this.heartPosition.top,
'--left': this.heartPosition.left
};
}
},
@ -869,13 +898,10 @@ export default {
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() {
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;
console.log("Loading finished!");
},
/**
@ -998,7 +1024,7 @@ export default {
})
.catch((err) => {
console.log("Error fetching events!", err, this);
if (err.errcode == "M_UNKNOWN" && initialEventId) {
if (initialEventId) {
// Try again without initial event!
this.onRoomJoined(null);
} else {
@ -1146,7 +1172,8 @@ export default {
Vue.set(event, "parentThread", parentEvent);
} else {
// 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) {
const parentEvent = tl.getEvents().find((e) => e.getId() === event.threadRootId);
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 });
},
addQuickHeartReaction(e) {
this.heartPosition = e.position
this.sendQuickReaction({ reaction: this.heartEmoji, event: e.event }, true);
},
setReplyToImage(event) {
util
.getThumbnail(this.$matrix.matrixClient, event, this.$config)
@ -1584,7 +1616,7 @@ export default {
this.replyToEvent = event;
this.$refs.messageInput.focus();
if (event.parentThread || event.isThreadRoot || event.isMxThread) {
this.replyToContentType = 'm.thread';
this.replyToContentType = util.threadMessageType();
} else {
this.replyToContentType = event.getContent().msgtype || 'm.poll';
}
@ -1598,8 +1630,16 @@ export default {
},
redact(event) {
this.$matrix.matrixClient
.redactEvent(event.getRoomId(), event.getId())
let promises = [];
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(() => {
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;
// Figure out if we have already sent this emoji, in that case redact it again (toggle)
@ -1679,6 +1727,10 @@ export default {
.catch((err) => {
console.log("Failed to send quick reaction:", err);
});
if(heartAnimationFlag) {
this.showHeartAnimation();
}
}
},
@ -1909,4 +1961,32 @@ export default {
<style lang="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>

View file

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

View file

@ -73,7 +73,7 @@ export default {
width: 100%;
display: flex;
position: relative;
min-height: 48px;
min-height: $min-touch-target;
background: #f5f5f5;
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 {
height: 48px;
height: $min-touch-target;
}
</style>

View file

@ -125,13 +125,6 @@ export default {
},
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()) {
case "m.room.member":
if (event.getContent().membership == "join") {
@ -161,6 +154,13 @@ export default {
case "m.room.message":
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) {
// Incoming thread, e.g. a file drop!
return isForExport ? MessageIncomingThreadExport : MessageIncomingThread;
@ -201,6 +201,13 @@ export default {
}
return MessageIncomingText;
} 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) {
// Outgoing thread
return isForExport ? MessageOutgoingThreadExport : MessageOutgoingThread;

View file

@ -1,16 +1,16 @@
<template>
<v-responsive v-if="item.event.getContent().msgtype == 'm.video' && item.src" :class="{'thumbnail-item': true, 'preview': previewOnly}"
@click.stop="$emit('itemclick', {item: item})">
<video :src="item.src" :controls="!previewOnly" class="w-100 h-100">
{{ $t('fallbacks.video_file') }}
</video>
</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"
@click.stop="$emit('itemclick', {item: item})" />
<div v-else :class="{'thumbnail-item': true, 'preview': previewOnly, 'file-item': true}" @click.stop="$emit('itemclick', {item: item})">
<v-icon>{{ fileTypeIcon }}</v-icon>
<b>{{ $sanitize(fileName) }}</b>
<div>{{ fileSize }}</div>
<div ref="thumbnailRef">
<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">
{{ $t('fallbacks.video_file') }}
</video>
</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" />
<div v-else :class="{'thumbnail-item': true, 'preview': previewOnly, 'file-item': true}" >
<v-icon>{{ fileTypeIcon }}</v-icon>
<b>{{ $sanitize(fileName) }}</b>
<div>{{ fileSize }}</div>
</div>
</div>
</template>
<script>
@ -20,7 +20,7 @@ import util from "../../plugins/utils";
export default {
props: {
/**
* Item is an object of { event: MXEvent, src: URL }
* Item is an object of { event: MXEvent, src: URL }
*/
item: {
type: Object,
@ -54,7 +54,24 @@ export default {
fileSize() {
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>

View file

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

View file

@ -1,13 +1,12 @@
<template>
<message-incoming v-bind="{...$props, ...$attrs}" v-on="$listeners">
<div class="bubble image-bubble">
<div class="bubble image-bubble" ref="imageRef">
<v-img
:aspect-ratio="16 / 9"
ref="image"
:src="src"
:cover="cover"
:contain="contain"
@click.stop="dialog = true"
/>
</div>
<v-dialog
@ -28,12 +27,24 @@ export default {
components: { MessageIncoming },
data() {
return {
src: null,
src: undefined,
cover: true,
contain: 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() {
//console.log("Mounted with event:", JSON.stringify(this.event.getContent()));
const width = this.$refs.image.$el.clientWidth;
@ -52,6 +63,9 @@ export default {
this.contain = true;
}
this.src = url;
if(this.$refs.imageRef) {
this.initMessageInImageHammerJs(this.$refs.imageRef);
}
})
.catch((err) => {
console.log("Failed to fetch thumbnail: ", err);

View file

@ -12,7 +12,7 @@
<div class="message">
<i v-if="event.isRedacted()" class="deleted-text">
<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>
<span v-html="linkify($sanitize(messageText))" v-else/>
<span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()">

View file

@ -1,5 +1,5 @@
<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="original-message" v-if="inReplyToText">
<div class="original-message-sender">{{ inReplyToSender }}</div>
@ -10,7 +10,7 @@
</div>
<div class="message">
<v-container fluid class="imageCollection">
<v-container v-if="!event.isRedacted()" fluid class="imageCollection">
<v-row wrap>
<v-col v-for="({ size, item }) in layoutedItems()" :key="item.event.getId()" :cols="size">
<ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
@ -19,7 +19,7 @@
</v-container>
<i v-if="event.isRedacted()" class="deleted-text">
<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>
<span v-html="linkify($sanitize(messageText))" v-else />
<span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()">
@ -53,7 +53,7 @@ export default {
}
},
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) {
this.event.on("Event.relationsCreated", this.onRelationsCreated);
}
@ -63,7 +63,7 @@ export default {
},
methods: {
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);
},
onItemClick(event) {
@ -72,7 +72,7 @@ export default {
processThread() {
this.$emit('layout-change', () => {
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 => {
let ret = {
event: e,

View file

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

View file

@ -1,13 +1,12 @@
<template>
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<div class="bubble image-bubble">
<div class="bubble image-bubble" ref="imageRef">
<v-img
:aspect-ratio="16 / 9"
ref="image"
:src="src"
:cover="cover"
:contain="contain"
@click.stop="dialog = true"
/>
</div>
<v-dialog
@ -28,12 +27,24 @@ export default {
components: { MessageOutgoing },
data() {
return {
src: null,
src: undefined,
cover: true,
contain: 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() {
const width = this.$refs.image.$el.clientWidth;
const height = (width * 9) / 16;
@ -51,6 +62,9 @@ export default {
this.contain = true;
}
this.src = url;
if(this.$refs.imageRef) {
this.initMessageOutImageHammerJs(this.$refs.imageRef);
}
})
.catch((err) => {
console.log("Failed to fetch thumbnail: ", err);

View file

@ -12,7 +12,7 @@
<div class="message">
<i v-if="event.isRedacted()" class="deleted-text">
<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>
<span v-html="linkify($sanitize(messageText))" v-else/>
<span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()">

View file

@ -1,5 +1,5 @@
<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="original-message" v-if="inReplyToText">
<div class="original-message-sender">{{ inReplyToSender }}</div>
@ -11,16 +11,16 @@
<div class="message">
<v-container fluid class="imageCollection">
<v-container v-if="!event.isRedacted()" fluid class="imageCollection">
<v-row wrap>
<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)" />
</v-col>
</v-row>
</v-container>
<i v-if="event.isRedacted()" class="deleted-text">
<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>
<span v-html="linkify($sanitize(messageText))" v-else />
<span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()">
@ -30,7 +30,7 @@
</div>
<GalleryItemsView :originalEvent="originalEvent" :items="items" :initialItem="showItem" v-if="!!showItem" v-on:close="showItem = null" />
</message-outgoing>
<component v-else-if="items.length == 1" :is="componentFn(items[0].event)"
<component v-else-if="items.length == 1" :is="componentFn(items[0].event)"
:originalEvent="items[0].event"
v-bind="{...$props, ...$attrs}" v-on="$listeners"
/>
@ -54,7 +54,7 @@ export default {
}
},
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) {
this.event.on("Event.relationsCreated", this.onRelationsCreated);
}
@ -64,7 +64,7 @@ export default {
},
methods: {
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);
},
onItemClick(event) {
@ -73,7 +73,7 @@ export default {
processThread() {
this.$emit('layout-change', () => {
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 => {
let ret = {
event: e,
@ -147,7 +147,7 @@ export default {
.col {
padding: 2px;
}
.file-item {
display: flex;
align-items: center;

View file

@ -20,7 +20,7 @@ export default {
}
},
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) {
this.event.on("Event.relationsCreated", this.onRelationsCreated);
}
@ -30,7 +30,7 @@ export default {
},
methods: {
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);
},
processThread() {

View file

@ -20,7 +20,7 @@ export default {
}
},
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) {
this.event.on("Event.relationsCreated", this.onRelationsCreated);
}
@ -30,7 +30,7 @@ export default {
},
methods: {
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);
},
processThread() {

View file

@ -3,6 +3,7 @@ import * as linkify from 'linkifyjs';
import linkifyHtml from 'linkify-html';
import utils from "../../plugins/utils"
import util from "../../plugins/utils";
import Hammer from "hammerjs";
linkify.options.defaults.className = "link";
linkify.options.defaults.target = { url: "_blank" };
@ -48,10 +49,10 @@ export default {
event: {},
thread: null,
utils,
mc: null,
mcCustom: null
};
},
mounted() {
},
beforeDestroy() {
this.thread = null;
},
@ -278,6 +279,15 @@ export default {
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) {
const date = new Date();
date.setTime(time);
@ -326,5 +336,22 @@ export default {
* Override this to handle updates to (the) message thread.
*/
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,
status: this.sendStatuses.INITIAL,
statusDate: Date.now,
mediaEventId: undefined,
attachment: file,
preview: attachment.image,
progress: 0,
@ -74,6 +75,7 @@ export default {
if (item.status !== this.sendStatuses.INITIAL) {
return getItemPromise(++index);
}
item.status = this.sendStatuses.SENDING;
const itemPromise = util.sendFile(this.$matrix.matrixClient, this.room.roomId, item.attachment, ({ loaded, total }) => {
if (loaded == total) {
item.progress = 100;
@ -81,7 +83,7 @@ export default {
item.progress = 100 * loaded / total;
}
}, eventId)
.then(() => {
.then((mediaEventId) => {
// Look at last item rotation, flipping the sign on this, so looks more like a true stack
let signR = 1;
let signX = 1;
@ -100,6 +102,7 @@ export default {
item.randomRotation = signR * (2 + Math.random() * 10);
item.randomTranslationX = signX * Math.random() * 20;
item.randomTranslationY = signY * Math.random() * 20;
item.mediaEventId = mediaEventId;
item.status = this.sendStatuses.SENT;
item.statusDate = Date.now;
}).catch(ignorederr => {
@ -133,15 +136,17 @@ export default {
});
this.sendingStatus = this.sendStatuses.CANCELED;
if (this.sendingRootEventId && this.room) {
// Redact the root event.
this.$matrix.matrixClient
.redactEvent(this.room.roomId, this.sendingRootEventId, undefined, { reason: "cancel" })
.then(() => {
console.log("Message redacted");
})
.catch((err) => {
console.log("Redaction failed: ", err);
});
// Redact all media we already sent, plus the root event
let promises = this.sendingAttachments.filter((item) => item.mediaEventId !== undefined).map((item) => this.$matrix.matrixClient.redactEvent(this.room.roomId, item.mediaEventId, undefined, { reason: "cancel" }));
promises.push(this.$matrix.matrixClient.redactEvent(this.room.roomId, this.sendingRootEventId, undefined, { reason: "cancel" }));
Promise.allSettled(promises)
.then(() => {
console.log("Message redacted");
})
.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) {
navigator.serviceWorker.register("./sw.js")
.then(async registration => {
console.log('Service Worker registered with scope:', registration.scope);
registration.active.postMessage(periodicSyncNewMsgReminderTxt);
await registerPeriodicBackgroundSync(registration);
})
.catch(error => {

View file

@ -5,6 +5,8 @@ import ImageResize from "image-resize";
import { AutoDiscovery } from 'matrix-js-sdk';
import User from '../models/user';
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_DELETED = "im.keanu.room_deleted";
@ -66,6 +68,11 @@ class UploadPromise {
}
class Util {
threadMessageType() {
return Thread.hasServerSideSupport ? "m.thread" : "io.element.thread"
}
getAttachmentUrlAndDuration(event) {
return new Promise((resolve, reject) => {
const content = event.getContent();
@ -398,7 +405,7 @@ class Util {
// If thread root (an eventId) is set, add that here
if (threadRoot) {
messageContent["m.relates_to"] = {
"rel_type": "m.thread",
"rel_type": this.threadMessageType(),
"event_id": threadRoot
};
}
@ -669,7 +676,8 @@ class Util {
name = name.slice(0, name.indexOf("."));
name = name.charAt(0).toUpperCase() + name.slice(1);
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;
}
@ -1014,6 +1022,25 @@ class Util {
}
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();

View file

@ -68,7 +68,7 @@ const vuexPersistSessionStorage = new VuexPersist({
const defaultUseSessionStorage = (sessionStorage.getItem(USER) != null);
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: {
loginSuccess(state, user) {
state.auth.status.loggedIn = true;