Merge branch 'master' into chat-version-negotiation

This commit is contained in:
spaced4ndy 2023-09-06 10:41:06 +04:00
commit 0c4b843a3f
104 changed files with 5838 additions and 1268 deletions

View File

@ -3655,6 +3655,26 @@ SimpleX servers cannot see your profile.</source>
<target state="translated">%1$@ في %2$@:</target>
<note>copied message info, &lt;sender&gt; at &lt;time&gt;</note>
</trans-unit>
<trans-unit id="# %@" xml:space="preserve" approved="no">
<source># %@</source>
<target state="needs-translation"># %@</target>
<note>copied message info title, # &lt;title&gt;</note>
</trans-unit>
<trans-unit id="## History" xml:space="preserve" approved="no">
<source>## History</source>
<target state="translated">## السجل</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="## In reply to" xml:space="preserve" approved="no">
<source>## In reply to</source>
<target state="translated">## ردًا على</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="%@ and %@ connected" xml:space="preserve" approved="no">
<source>%@ and %@ connected</source>
<target state="translated">%@ و %@ متصل</target>
<note>No comment provided by engineer.</note>
</trans-unit>
</body>
</file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="ar" datatype="plaintext">

View File

@ -44,14 +44,17 @@
</trans-unit>
<trans-unit id="# %@" xml:space="preserve">
<source># %@</source>
<target># %@</target>
<note>copied message info title, # &lt;title&gt;</note>
</trans-unit>
<trans-unit id="## History" xml:space="preserve">
<source>## History</source>
<target>## Historie</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="## In reply to" xml:space="preserve">
<source>## In reply to</source>
<target>## Odpovídáno</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="#secret#" xml:space="preserve">
@ -86,6 +89,7 @@
</trans-unit>
<trans-unit id="%@ and %@ connected" xml:space="preserve">
<source>%@ and %@ connected</source>
<target>%@ a %@ připojen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ at %@:" xml:space="preserve">
@ -120,6 +124,7 @@
</trans-unit>
<trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve">
<source>%@, %@ and %lld other members connected</source>
<target>%@, %@ a %lld ostatní členové připojeni</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@:" xml:space="preserve">
@ -477,7 +482,7 @@
</trans-unit>
<trans-unit id="Accept connection request?" xml:space="preserve">
<source>Accept connection request?</source>
<target>Přijmout kontakt</target>
<target>Přijmout kontakt?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Accept contact request from %@?" xml:space="preserve">
@ -1063,15 +1068,17 @@
</trans-unit>
<trans-unit id="Connect directly" xml:space="preserve">
<source>Connect directly</source>
<target>Připojit přímo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect incognito" xml:space="preserve">
<source>Connect incognito</source>
<target>Spojit se inkognito</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via contact link" xml:space="preserve">
<source>Connect via contact link</source>
<target>Připojit se přes kontaktní odkaz?</target>
<target>Připojit se přes odkaz</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via group link?" xml:space="preserve">
@ -1091,7 +1098,7 @@
</trans-unit>
<trans-unit id="Connect via one-time link" xml:space="preserve">
<source>Connect via one-time link</source>
<target>Připojit se jednorázovým odkazem?</target>
<target>Připojit se jednorázovým odkazem</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting server…" xml:space="preserve">
@ -1569,6 +1576,7 @@
</trans-unit>
<trans-unit id="Delivery" xml:space="preserve">
<source>Delivery</source>
<target>Doručenka</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delivery receipts are disabled!" xml:space="preserve">
@ -2583,6 +2591,7 @@
</trans-unit>
<trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve">
<source>Incognito mode protects your privacy by using a new random profile for each contact.</source>
<target>Režim inkognito chrání vaše soukromí používáním nového náhodného profilu pro každý kontakt.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incoming audio call" xml:space="preserve">
@ -2659,6 +2668,7 @@
</trans-unit>
<trans-unit id="Invalid status" xml:space="preserve">
<source>Invalid status</source>
<target>Neplatný status</target>
<note>item status text</note>
</trans-unit>
<trans-unit id="Invitation expired!" xml:space="preserve">
@ -2744,12 +2754,12 @@
</trans-unit>
<trans-unit id="Join incognito" xml:space="preserve">
<source>Join incognito</source>
<target>Připojte se inkognito</target>
<target>Připojit se inkognito</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Joining group" xml:space="preserve">
<source>Joining group</source>
<target>Připojení ke skupině</target>
<target>Připojování ke skupině</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve">
@ -3009,6 +3019,7 @@
</trans-unit>
<trans-unit id="Most likely this connection is deleted." xml:space="preserve">
<source>Most likely this connection is deleted.</source>
<target>Pravděpodobně je toto spojení smazáno.</target>
<note>item status description</note>
</trans-unit>
<trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve">
@ -4623,7 +4634,7 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
</trans-unit>
<trans-unit id="They can be overridden in contact and group settings." xml:space="preserve">
<source>They can be overridden in contact and group settings.</source>
<target>Mohou být přepsány v nastavení kontaktů</target>
<target>Mohou být přepsány v nastavení kontaktů.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve">

View File

@ -466,7 +466,7 @@
</trans-unit>
<trans-unit id="About SimpleX address" xml:space="preserve">
<source>About SimpleX address</source>
<target>Acerca de dirección SimpleX</target>
<target>Acerca de la dirección SimpleX</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Accent color" xml:space="preserve">
@ -1208,12 +1208,12 @@
</trans-unit>
<trans-unit id="Create SimpleX address" xml:space="preserve">
<source>Create SimpleX address</source>
<target>Crear dirección SimpleX</target>
<target>Crear tu dirección SimpleX</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create an address to let people connect with you." xml:space="preserve">
<source>Create an address to let people connect with you.</source>
<target>Crear una dirección para que otras personas se puedan conectar contigo.</target>
<target>Crea una dirección para que otras personas puedan conectar contigo.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create file" xml:space="preserve">
@ -1248,7 +1248,7 @@
</trans-unit>
<trans-unit id="Create your profile" xml:space="preserve">
<source>Create your profile</source>
<target>Crear tu perfil</target>
<target>Crea tu perfil</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Created on %@" xml:space="preserve">
@ -1381,7 +1381,7 @@
</trans-unit>
<trans-unit id="Decentralized" xml:space="preserve">
<source>Decentralized</source>
<target>Descentralizado</target>
<target>Descentralizada</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Decryption error" xml:space="preserve">
@ -1706,7 +1706,7 @@
</trans-unit>
<trans-unit id="Don't create address" xml:space="preserve">
<source>Don't create address</source>
<target>No crear dirección</target>
<target>No crear dirección SimpleX</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't enable" xml:space="preserve">
@ -2899,7 +2899,7 @@
</trans-unit>
<trans-unit id="Markdown in messages" xml:space="preserve">
<source>Markdown in messages</source>
<target>Sintaxis markdown en los mensajes</target>
<target>Sintaxis Markdown</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Max 30 seconds, received instantly." xml:space="preserve">
@ -3663,7 +3663,7 @@
</trans-unit>
<trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve">
<source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source>
<target>La dirección de recepción se cambiará. El cambio se completará cuando el remitente esté en línea.</target>
<target>La dirección de recepción pasará a otro servidor. El cambio se completará cuando el remitente esté en línea.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Receiving file will be stopped." xml:space="preserve">
@ -4383,7 +4383,7 @@
</trans-unit>
<trans-unit id="Stop chat to enable database actions" xml:space="preserve">
<source>Stop chat to enable database actions</source>
<target>Para habilitar las acciones sobre la base de datos, previamente debes detener Chat</target>
<target>Detén SimpleX para habilitar las acciones sobre la base de datos</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve">
@ -4590,7 +4590,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target>
</trans-unit>
<trans-unit id="The next generation of private messaging" xml:space="preserve">
<source>The next generation of private messaging</source>
<target>La próxima generación de mensajería privada</target>
<target>La nueva generación de mensajería privada</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
@ -5130,7 +5130,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb
</trans-unit>
<trans-unit id="You can create it later" xml:space="preserve">
<source>You can create it later</source>
<target>Puedes crearlo más tarde</target>
<target>Puedes crearla más tarde</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can enable later via Settings" xml:space="preserve">
@ -5330,7 +5330,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb
</trans-unit>
<trans-unit id="Your chat database" xml:space="preserve">
<source>Your chat database</source>
<target>Base de datos Chat</target>
<target>Base de datos</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat database is not encrypted - set passphrase to encrypt it." xml:space="preserve">
@ -5411,7 +5411,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target>
</trans-unit>
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
<source>Your profile, contacts and delivered messages are stored on your device.</source>
<target>Tu perfil, contactos y mensajes entregados se almacenan en tu dispositivo.</target>
<target>Tu perfil, contactos y mensajes se almacenan en tu dispositivo.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your random profile" xml:space="preserve">

View File

@ -5725,7 +5725,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target>
</trans-unit>
<trans-unit id="encryption ok" xml:space="preserve">
<source>encryption ok</source>
<target>chiffrement ok</target>
<target>chiffrement OK</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption ok for %@" xml:space="preserve">

View File

@ -284,7 +284,7 @@ Available in v5.1</source>
</trans-unit>
<trans-unit id="." xml:space="preserve" approved="no">
<source>.</source>
<target state="needs-translation">.</target>
<target state="translated">.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="1 day" xml:space="preserve" approved="no">
@ -1971,8 +1971,9 @@ Available in v5.1</source>
<target state="translated">חברי הקבוצה יכולים לשלוח הודעות קוליות.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group message:" xml:space="preserve">
<trans-unit id="Group message:" xml:space="preserve" approved="no">
<source>Group message:</source>
<target state="translated">הודעה קבוצתית:</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Group moderation" xml:space="preserve" approved="no">
@ -2377,262 +2378,327 @@ Available in v5.1</source>
<target state="translated">נתוני פרופיל מקומיים בלבד</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Lock after" xml:space="preserve">
<trans-unit id="Lock after" xml:space="preserve" approved="no">
<source>Lock after</source>
<target state="translated">נעל אחרי</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Lock mode" xml:space="preserve">
<trans-unit id="Lock mode" xml:space="preserve" approved="no">
<source>Lock mode</source>
<target state="translated">מצב נעילה</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Make a private connection" xml:space="preserve">
<trans-unit id="Make a private connection" xml:space="preserve" approved="no">
<source>Make a private connection</source>
<target state="translated">צור חיבור פרטי</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Make profile private!" xml:space="preserve">
<trans-unit id="Make profile private!" xml:space="preserve" approved="no">
<source>Make profile private!</source>
<target state="translated">הפוך את הפרופיל לפרטי!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve">
<trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve" approved="no">
<source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source>
<target state="translated">ודא שכתובות השרת %@ הן בפורמט הנכון, מופרדות בשורה ואינן משוכפלות (%@).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve">
<trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve" approved="no">
<source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source>
<target state="translated">ודאו שכתובות שרתי ה־WebRTC ICE הן בפורמט הנכון, מופרדות בשורה ולא משוכפלות.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve">
<trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve" approved="no">
<source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source>
<target state="translated">אנשים רבים שאלו: *אם ל-SimpleX אין מזהי משתמש, איך הוא יכול לשלוח הודעות?*</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Mark deleted for everyone" xml:space="preserve">
<trans-unit id="Mark deleted for everyone" xml:space="preserve" approved="no">
<source>Mark deleted for everyone</source>
<target state="translated">לסמן נמחק לכולם</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Mark read" xml:space="preserve">
<trans-unit id="Mark read" xml:space="preserve" approved="no">
<source>Mark read</source>
<target state="translated">סמן כנקרא</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Mark verified" xml:space="preserve">
<trans-unit id="Mark verified" xml:space="preserve" approved="no">
<source>Mark verified</source>
<target state="translated">סמן מאומת</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Markdown in messages" xml:space="preserve">
<trans-unit id="Markdown in messages" xml:space="preserve" approved="no">
<source>Markdown in messages</source>
<target state="translated">מרקדאון בהודעות</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Max 30 seconds, received instantly." xml:space="preserve">
<trans-unit id="Max 30 seconds, received instantly." xml:space="preserve" approved="no">
<source>Max 30 seconds, received instantly.</source>
<target state="translated">מקסימום 30 שניות, התקבל באופן מיידי.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Member" xml:space="preserve">
<trans-unit id="Member" xml:space="preserve" approved="no">
<source>Member</source>
<target state="translated">חבר קבוצה</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Member role will be changed to &quot;%@&quot;. All group members will be notified." xml:space="preserve">
<trans-unit id="Member role will be changed to &quot;%@&quot;. All group members will be notified." xml:space="preserve" approved="no">
<source>Member role will be changed to "%@". All group members will be notified.</source>
<target state="translated">תפקיד חבר הקבוצה ישתנה ל-"%@". כל חברי הקבוצה יקבלו הודעה.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Member role will be changed to &quot;%@&quot;. The member will receive a new invitation." xml:space="preserve">
<trans-unit id="Member role will be changed to &quot;%@&quot;. The member will receive a new invitation." xml:space="preserve" approved="no">
<source>Member role will be changed to "%@". The member will receive a new invitation.</source>
<target state="translated">תפקיד חבר הקבוצה ישתנה ל-"%@". חבר הקבוצה יקבל הזמנה חדשה.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve">
<trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve" approved="no">
<source>Member will be removed from group - this cannot be undone!</source>
<target state="translated">חבר הקבוצה יוסר מהקבוצה לא ניתן לבטל זאת!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message delivery error" xml:space="preserve">
<trans-unit id="Message delivery error" xml:space="preserve" approved="no">
<source>Message delivery error</source>
<target state="translated">שגיאת מסירת הודעה</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message draft" xml:space="preserve">
<trans-unit id="Message draft" xml:space="preserve" approved="no">
<source>Message draft</source>
<target state="translated">טיוטת הודעה</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message text" xml:space="preserve">
<trans-unit id="Message text" xml:space="preserve" approved="no">
<source>Message text</source>
<target state="translated">טקסט הודעה</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Messages" xml:space="preserve">
<trans-unit id="Messages" xml:space="preserve" approved="no">
<source>Messages</source>
<target state="translated">הודעות</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Messages &amp; files" xml:space="preserve">
<trans-unit id="Messages &amp; files" xml:space="preserve" approved="no">
<source>Messages &amp; files</source>
<target state="translated">הודעות וקבצים</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Migrating database archive..." xml:space="preserve">
<source>Migrating database archive...</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Migration error:" xml:space="preserve">
<trans-unit id="Migration error:" xml:space="preserve" approved="no">
<source>Migration error:</source>
<target state="translated">שגיאת העברה:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." xml:space="preserve">
<trans-unit id="Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." xml:space="preserve" approved="no">
<source>Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat).</source>
<target state="translated">ההעברה נכשלה. הקש על **דלג** למטה כדי להמשיך להשתמש במסד הנתונים הנוכחי. אנא דווח על הבעיה למפתחי האפליקציה באמצעות צ'אט או דוא"ל [chat@simplex.chat](mailto:chat@simplex.chat).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Migration is completed" xml:space="preserve">
<trans-unit id="Migration is completed" xml:space="preserve" approved="no">
<source>Migration is completed</source>
<target state="translated">ההעברה הושלמה</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Migrations: %@" xml:space="preserve">
<trans-unit id="Migrations: %@" xml:space="preserve" approved="no">
<source>Migrations: %@</source>
<target state="translated">העברות: %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Moderate" xml:space="preserve">
<trans-unit id="Moderate" xml:space="preserve" approved="no">
<source>Moderate</source>
<target state="translated">חסימת הודעה</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="More improvements are coming soon!" xml:space="preserve">
<trans-unit id="More improvements are coming soon!" xml:space="preserve" approved="no">
<source>More improvements are coming soon!</source>
<target state="translated">שיפורים נוספים יגיעו בקרוב!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve">
<trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve" approved="no">
<source>Most likely this contact has deleted the connection with you.</source>
<target state="translated">ככל הנראה איש קשר זה מחק את החיבור איתך.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Multiple chat profiles" xml:space="preserve">
<trans-unit id="Multiple chat profiles" xml:space="preserve" approved="no">
<source>Multiple chat profiles</source>
<target state="translated">פרופילי צ׳אט מרובים</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Mute" xml:space="preserve">
<trans-unit id="Mute" xml:space="preserve" approved="no">
<source>Mute</source>
<target state="translated">השתק</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Muted when inactive!" xml:space="preserve">
<trans-unit id="Muted when inactive!" xml:space="preserve" approved="no">
<source>Muted when inactive!</source>
<target state="translated">מושתק כאשר אין פעילות!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Name" xml:space="preserve">
<trans-unit id="Name" xml:space="preserve" approved="no">
<source>Name</source>
<target state="translated">שם</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Network &amp; servers" xml:space="preserve">
<trans-unit id="Network &amp; servers" xml:space="preserve" approved="no">
<source>Network &amp; servers</source>
<target state="translated">רשת ושרתים</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Network settings" xml:space="preserve">
<trans-unit id="Network settings" xml:space="preserve" approved="no">
<source>Network settings</source>
<target state="translated">הגדרות רשת</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Network status" xml:space="preserve">
<trans-unit id="Network status" xml:space="preserve" approved="no">
<source>Network status</source>
<target state="translated">מצב רשת</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New Passcode" xml:space="preserve">
<trans-unit id="New Passcode" xml:space="preserve" approved="no">
<source>New Passcode</source>
<target state="translated">קוד גישה חדש</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New contact request" xml:space="preserve">
<trans-unit id="New contact request" xml:space="preserve" approved="no">
<source>New contact request</source>
<target state="translated">בקשה חדשה ליצירת קשר</target>
<note>notification</note>
</trans-unit>
<trans-unit id="New contact:" xml:space="preserve">
<trans-unit id="New contact:" xml:space="preserve" approved="no">
<source>New contact:</source>
<target state="translated">איש קשר חדש:</target>
<note>notification</note>
</trans-unit>
<trans-unit id="New database archive" xml:space="preserve">
<trans-unit id="New database archive" xml:space="preserve" approved="no">
<source>New database archive</source>
<target state="translated">ארכיון מסד נתונים חדש</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New in %@" xml:space="preserve">
<trans-unit id="New in %@" xml:space="preserve" approved="no">
<source>New in %@</source>
<target state="translated">חדש ב %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New member role" xml:space="preserve">
<trans-unit id="New member role" xml:space="preserve" approved="no">
<source>New member role</source>
<target state="translated">תפקיד חבר קבוצה חדש</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New message" xml:space="preserve">
<trans-unit id="New message" xml:space="preserve" approved="no">
<source>New message</source>
<target state="translated">הודעה חדשה</target>
<note>notification</note>
</trans-unit>
<trans-unit id="New passphrase…" xml:space="preserve">
<trans-unit id="New passphrase…" xml:space="preserve" approved="no">
<source>New passphrase…</source>
<target state="translated">סיסמה חדשה…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No" xml:space="preserve">
<trans-unit id="No" xml:space="preserve" approved="no">
<source>No</source>
<target state="translated">לא</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No app password" xml:space="preserve">
<trans-unit id="No app password" xml:space="preserve" approved="no">
<source>No app password</source>
<target state="translated">אין סיסמה לאפליקציה</target>
<note>Authentication unavailable</note>
</trans-unit>
<trans-unit id="No contacts selected" xml:space="preserve">
<trans-unit id="No contacts selected" xml:space="preserve" approved="no">
<source>No contacts selected</source>
<target state="translated">לא נבחרו אנשי קשר</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No contacts to add" xml:space="preserve">
<trans-unit id="No contacts to add" xml:space="preserve" approved="no">
<source>No contacts to add</source>
<target state="translated">אין אנשי קשר להוסיף</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No device token!" xml:space="preserve">
<trans-unit id="No device token!" xml:space="preserve" approved="no">
<source>No device token!</source>
<target state="translated">אין אסימון מכשיר!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No group!" xml:space="preserve">
<trans-unit id="No group!" xml:space="preserve" approved="no">
<source>Group not found!</source>
<target state="translated">קבוצה לא נמצאה!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record voice message" xml:space="preserve">
<trans-unit id="No permission to record voice message" xml:space="preserve" approved="no">
<source>No permission to record voice message</source>
<target state="translated">אין הרשאה להקליט הודעה קולית</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No received or sent files" xml:space="preserve">
<trans-unit id="No received or sent files" xml:space="preserve" approved="no">
<source>No received or sent files</source>
<target state="translated">לא התקבלו או נשלחו קבצים</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<trans-unit id="Notifications" xml:space="preserve" approved="no">
<source>Notifications</source>
<target state="translated">התראות</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Notifications are disabled!" xml:space="preserve">
<trans-unit id="Notifications are disabled!" xml:space="preserve" approved="no">
<source>Notifications are disabled!</source>
<target state="translated">ההתראות מושבתות!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Now admins can:&#10;- delete members' messages.&#10;- disable members (&quot;observer&quot; role)" xml:space="preserve">
<trans-unit id="Now admins can:&#10;- delete members' messages.&#10;- disable members (&quot;observer&quot; role)" xml:space="preserve" approved="no">
<source>Now admins can:
- delete members' messages.
- disable members ("observer" role)</source>
<target state="translated">כעת מנהלים יכולים:
- למחוק הודעות של חברי קבוצה.
- להשבית חברי קבוצה (תפקיד ”צופה”)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off" xml:space="preserve">
<trans-unit id="Off" xml:space="preserve" approved="no">
<source>Off</source>
<target state="translated">כבוי</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off (Local)" xml:space="preserve">
<trans-unit id="Off (Local)" xml:space="preserve" approved="no">
<source>Off (Local)</source>
<target state="translated">כבוי (מקומי)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Ok" xml:space="preserve">
<trans-unit id="Ok" xml:space="preserve" approved="no">
<source>Ok</source>
<target state="translated">אישור</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Old database" xml:space="preserve">
<trans-unit id="Old database" xml:space="preserve" approved="no">
<source>Old database</source>
<target state="translated">מסד נתונים ישן</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Old database archive" xml:space="preserve">
<trans-unit id="Old database archive" xml:space="preserve" approved="no">
<source>Old database archive</source>
<target state="translated">ארכיון מסד נתונים ישן</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="One-time invitation link" xml:space="preserve">
<trans-unit id="One-time invitation link" xml:space="preserve" approved="no">
<source>One-time invitation link</source>
<target state="translated">קישור הזמנה חד־פעמי</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve">
<trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve" approved="no">
<source>Onion hosts will be required for connection. Requires enabling VPN.</source>
<target state="translated">לחיבור יידרשו מארחי Onion. דורש הפעלת VPN.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve">
<trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve" approved="no">
<source>Onion hosts will be used when available. Requires enabling VPN.</source>
<target state="translated">מארחי Onion ישומשו כאשר יהיו זמינים. דורש הפעלת VPN.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Onion hosts will not be used." xml:space="preserve">
<trans-unit id="Onion hosts will not be used." xml:space="preserve" approved="no">
<source>Onion hosts will not be used.</source>
<target state="translated">לא ייעשה שימוש במארחי Onion.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
@ -4981,6 +5047,270 @@ SimpleX servers cannot see your profile.</source>
<target state="translated">%1$@ בזמן %2$@:</target>
<note>copied message info, &lt;sender&gt; at &lt;time&gt;</note>
</trans-unit>
<trans-unit id="# %@" xml:space="preserve" approved="no">
<source># %@</source>
<target state="translated"># %@</target>
<note>copied message info title, # &lt;title&gt;</note>
</trans-unit>
<trans-unit id="## History" xml:space="preserve" approved="no">
<source>## History</source>
<target state="translated">## היסטוריה</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="## In reply to" xml:space="preserve" approved="no">
<source>## In reply to</source>
<target state="translated">## בתגובה ל</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="- more stable message delivery.&#10;- a bit better groups.&#10;- and more!" xml:space="preserve" approved="no">
<source>- more stable message delivery.
- a bit better groups.
- and more!</source>
<target state="translated">- שליחת הודעות יציבה יותר.
- קבוצות קצת יותר טובות.
- ועוד!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="A few more things" xml:space="preserve" approved="no">
<source>A few more things</source>
<target state="translated">עוד כמה דברים</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="A new random profile will be shared." xml:space="preserve" approved="no">
<source>A new random profile will be shared.</source>
<target state="translated">ישותף פרופיל אקראי חדש.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Accept connection request?" xml:space="preserve" approved="no">
<source>Accept connection request?</source>
<target state="translated">לאשר בקשת חיבור?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect directly" xml:space="preserve" approved="no">
<source>Connect directly</source>
<target state="translated">התחבר ישירות</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect incognito" xml:space="preserve" approved="no">
<source>Connect incognito</source>
<target state="translated">התחבר בזהות נסתרת</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via one-time link" xml:space="preserve" approved="no">
<source>Connect via one-time link</source>
<target state="translated">התחבר באמצעות קישור חד־פעמי</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contacts" xml:space="preserve" approved="no">
<source>Contacts</source>
<target state="translated">אנשי קשר</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delivery" xml:space="preserve" approved="no">
<source>Delivery</source>
<target state="translated">מסירה</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error synchronizing connection" xml:space="preserve" approved="no">
<source>Error synchronizing connection</source>
<target state="translated">שגיאה בסנכרון החיבור</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix encryption after restoring backups." xml:space="preserve" approved="no">
<source>Fix encryption after restoring backups.</source>
<target state="translated">תקן הצפנה לאחר שחזור גיבויים.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix not supported by group member" xml:space="preserve" approved="no">
<source>Fix not supported by group member</source>
<target state="translated">תיקון אינו נתמך על ידי חבר הקבוצה</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid status" xml:space="preserve" approved="no">
<source>Invalid status</source>
<target state="translated">סטטוס לא חוקי</target>
<note>item status text</note>
</trans-unit>
<trans-unit id="Migrating database archive…" xml:space="preserve" approved="no">
<source>Migrating database archive…</source>
<target state="translated">מעביר את ארכיון מסד הנתונים…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Moderated at: %@" xml:space="preserve" approved="no">
<source>Moderated at: %@</source>
<target state="translated">נחסם ב: %@</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Most likely this connection is deleted." xml:space="preserve" approved="no">
<source>Most likely this connection is deleted.</source>
<target state="translated">סביר להניח שהחיבור הזה נמחק.</target>
<note>item status description</note>
</trans-unit>
<trans-unit id="%@ and %@ connected" xml:space="preserve" approved="no">
<source>%@ and %@ connected</source>
<target state="translated">%@ ו-%@ מחוברים</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via contact link" xml:space="preserve" approved="no">
<source>Connect via contact link</source>
<target state="translated">התחבר באמצעות קישור איש קשר</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delivery receipts!" xml:space="preserve" approved="no">
<source>Delivery receipts!</source>
<target state="translated">קבלות על המשלוח!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Disable for all" xml:space="preserve" approved="no">
<source>Disable for all</source>
<target state="translated">השבת לכולם</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error enabling delivery receipts!" xml:space="preserve" approved="no">
<source>Error enabling delivery receipts!</source>
<target state="translated">שגיאה בהפעלת קבלות משלוח!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Even when disabled in the conversation." xml:space="preserve" approved="no">
<source>Even when disabled in the conversation.</source>
<target state="translated">גם אם הוא מושבת בשיחה.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix" xml:space="preserve" approved="no">
<source>Fix</source>
<target state="translated">תקן</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix connection" xml:space="preserve" approved="no">
<source>Fix connection</source>
<target state="translated">תקן את החיבור</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Find chats faster" xml:space="preserve" approved="no">
<source>Find chats faster</source>
<target state="translated">מצא צ'אטים מהר יותר</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix connection?" xml:space="preserve" approved="no">
<source>Fix connection?</source>
<target state="translated">לתקן את החיבור?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Make one message disappear" xml:space="preserve" approved="no">
<source>Make one message disappear</source>
<target state="translated">העלם הודעה אחת</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix not supported by contact" xml:space="preserve" approved="no">
<source>Fix not supported by contact</source>
<target state="translated">תיקון לא נתמך על ידי איש קשר</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve" approved="no">
<source>Incognito mode protects your privacy by using a new random profile for each contact.</source>
<target state="translated">מצב זהות נסתרת מגן על הפרטיות שלך על ידי שימוש בפרופיל אקראי חדש עבור כל איש קשר.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="In reply to" xml:space="preserve" approved="no">
<source>In reply to</source>
<target state="translated">בתגובה ל</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve" approved="no">
<source>Keep your connections</source>
<target state="translated">שימרו על הקשרים שלכם</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message delivery receipts!" xml:space="preserve" approved="no">
<source>Message delivery receipts!</source>
<target state="translated">קבלות על הודעות!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reactions are prohibited in this chat." xml:space="preserve" approved="no">
<source>Message reactions are prohibited in this chat.</source>
<target state="translated">תגובות אמוג׳י להודעות אסורות בצ׳אט זה.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reactions are prohibited in this group." xml:space="preserve" approved="no">
<source>Message reactions are prohibited in this group.</source>
<target state="translated">תגובות אמוג׳י להודעות אסורות בקבוצה זו.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delivery receipts are disabled!" xml:space="preserve" approved="no">
<source>Delivery receipts are disabled!</source>
<target state="translated">קבלות על משלוח מושבתות!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Disable (keep overrides)" xml:space="preserve" approved="no">
<source>Disable (keep overrides)</source>
<target state="translated">השבת (שמור עקיפות)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't enable" xml:space="preserve" approved="no">
<source>Don't enable</source>
<target state="translated">אל תפעיל</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable (keep overrides)" xml:space="preserve" approved="no">
<source>Enable (keep overrides)</source>
<target state="translated">הפעל (שמור עקיפות)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable for all" xml:space="preserve" approved="no">
<source>Enable for all</source>
<target state="translated">הפעל עבור כולם</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error setting delivery receipts!" xml:space="preserve" approved="no">
<source>Error setting delivery receipts!</source>
<target state="translated">שגיאה בהגדרת קבלות משלוח!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter unread and favorite chats." xml:space="preserve" approved="no">
<source>Filter unread and favorite chats.</source>
<target state="translated">סנן צ'אטים שלא נקראו וצ'אטים מועדפים.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Moderated at" xml:space="preserve" approved="no">
<source>Moderated at</source>
<target state="translated">נחסם</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message reactions" xml:space="preserve" approved="no">
<source>Message reactions</source>
<target state="translated">תגובות אמוג׳י להודעות</target>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Exporting database archive…" xml:space="preserve" approved="no">
<source>Exporting database archive…</source>
<target state="translated">מייצא את ארכיון מסד הנתונים…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve" approved="no">
<source>%@, %@ and %lld other members connected</source>
<target state="translated">%@, %@ ו-%lld חברים אחרים מחוברים</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New display name" xml:space="preserve" approved="no">
<source>New display name</source>
<target state="translated">שם תצוגה חדש</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No delivery information" xml:space="preserve" approved="no">
<source>No delivery information</source>
<target state="translated">אין מידע על מסירה</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No filtered chats" xml:space="preserve" approved="no">
<source>No filtered chats</source>
<target state="translated">אין צ'אטים מסוננים</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No history" xml:space="preserve" approved="no">
<source>No history</source>
<target state="translated">ללא היסטוריה</target>
<note>No comment provided by engineer.</note>
</trans-unit>
</body>
</file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="he" datatype="plaintext">

View File

@ -44,14 +44,17 @@
</trans-unit>
<trans-unit id="# %@" xml:space="preserve">
<source># %@</source>
<target># %@</target>
<note>copied message info title, # &lt;title&gt;</note>
</trans-unit>
<trans-unit id="## History" xml:space="preserve">
<source>## History</source>
<target>## 履歴</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="## In reply to" xml:space="preserve">
<source>## In reply to</source>
<target>## 返信先</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="#secret#" xml:space="preserve">
@ -86,10 +89,12 @@
</trans-unit>
<trans-unit id="%@ and %@ connected" xml:space="preserve">
<source>%@ and %@ connected</source>
<target>%@ と %@ は接続中</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ at %@:" xml:space="preserve">
<source>%1$@ at %2$@:</source>
<target>%1$@ at %2$@:</target>
<note>copied message info, &lt;sender&gt; at &lt;time&gt;</note>
</trans-unit>
<trans-unit id="%@ is connected!" xml:space="preserve">
@ -119,6 +124,7 @@
</trans-unit>
<trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve">
<source>%@, %@ and %lld other members connected</source>
<target>%@, %@ および %lld 人のメンバーが接続中</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@:" xml:space="preserve">
@ -325,6 +331,9 @@
<source>- more stable message delivery.
- a bit better groups.
- and more!</source>
<target>- より安定したメッセージ配信。
- 改良されたグループ。
- などなど!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve">
@ -405,6 +414,7 @@
</trans-unit>
<trans-unit id="A few more things" xml:space="preserve">
<source>A few more things</source>
<target>その他</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="A new contact" xml:space="preserve">
@ -414,6 +424,7 @@
</trans-unit>
<trans-unit id="A new random profile will be shared." xml:space="preserve">
<source>A new random profile will be shared.</source>
<target>新しいランダムなプロファイルが共有されます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve">
@ -430,14 +441,17 @@
</trans-unit>
<trans-unit id="Abort" xml:space="preserve">
<source>Abort</source>
<target>中止</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Abort changing address" xml:space="preserve">
<source>Abort changing address</source>
<target>アドレス変更の中止</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Abort changing address?" xml:space="preserve">
<source>Abort changing address?</source>
<target>アドレス変更を中止しますか?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="About SimpleX" xml:space="preserve">
@ -523,6 +537,7 @@
</trans-unit>
<trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve">
<source>Address change will be aborted. Old receiving address will be used.</source>
<target>アドレス変更は中止されます。古い受信アドレスが使用されます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Admins can create the links to join groups." xml:space="preserve">
@ -617,6 +632,7 @@
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve">
<source>Allow to send files and media.</source>
<target>ファイルやメディアの送信を許可する。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send voice messages." xml:space="preserve">
@ -791,6 +807,7 @@
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>より良いメッセージ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
@ -1051,10 +1068,12 @@
</trans-unit>
<trans-unit id="Connect directly" xml:space="preserve">
<source>Connect directly</source>
<target>直接接続する</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect incognito" xml:space="preserve">
<source>Connect incognito</source>
<target>シークレットモードで接続</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via contact link" xml:space="preserve">
@ -1159,6 +1178,7 @@
</trans-unit>
<trans-unit id="Contacts" xml:space="preserve">
<source>Contacts</source>
<target>連絡先</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve">
@ -1556,10 +1576,12 @@
</trans-unit>
<trans-unit id="Delivery" xml:space="preserve">
<source>Delivery</source>
<target>Delivery</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delivery receipts are disabled!" xml:space="preserve">
<source>Delivery receipts are disabled!</source>
<target>Delivery receipts are disabled!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delivery receipts!" xml:space="preserve">
@ -1613,6 +1635,7 @@
</trans-unit>
<trans-unit id="Disable (keep overrides)" xml:space="preserve">
<source>Disable (keep overrides)</source>
<target>無効にする(設定の優先を維持)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
@ -1622,6 +1645,7 @@
</trans-unit>
<trans-unit id="Disable for all" xml:space="preserve">
<source>Disable for all</source>
<target>すべて無効</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Disappearing message" xml:space="preserve">
@ -1686,6 +1710,7 @@
</trans-unit>
<trans-unit id="Don't enable" xml:space="preserve">
<source>Don't enable</source>
<target>有効にしない</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't show again" xml:space="preserve">
@ -1730,6 +1755,7 @@
</trans-unit>
<trans-unit id="Enable (keep overrides)" xml:space="preserve">
<source>Enable (keep overrides)</source>
<target>有効にする(設定の優先を維持)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable SimpleX Lock" xml:space="preserve">
@ -1749,6 +1775,7 @@
</trans-unit>
<trans-unit id="Enable for all" xml:space="preserve">
<source>Enable for all</source>
<target>すべて有効</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable instant notifications?" xml:space="preserve">
@ -1868,6 +1895,7 @@
</trans-unit>
<trans-unit id="Error aborting address change" xml:space="preserve">
<source>Error aborting address change</source>
<target>アドレス変更中止エラー</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error accepting contact request" xml:space="preserve">
@ -2065,6 +2093,7 @@
</trans-unit>
<trans-unit id="Error synchronizing connection" xml:space="preserve">
<source>Error synchronizing connection</source>
<target>接続の同期エラー</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error updating group link" xml:space="preserve">
@ -2109,6 +2138,7 @@
</trans-unit>
<trans-unit id="Even when disabled in the conversation." xml:space="preserve">
<source>Even when disabled in the conversation.</source>
<target>会話中に無効になっている場合でも。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Exit without saving" xml:space="preserve">
@ -2148,6 +2178,7 @@
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source>
<target>お気に入り</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File will be deleted from servers." xml:space="preserve">
@ -2177,50 +2208,62 @@
</trans-unit>
<trans-unit id="Files and media" xml:space="preserve">
<source>Files and media</source>
<target>ファイルとメディア</target>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Files and media are prohibited in this group." xml:space="preserve">
<source>Files and media are prohibited in this group.</source>
<target>このグループでは、ファイルとメディアは禁止されています。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media prohibited!" xml:space="preserve">
<source>Files and media prohibited!</source>
<target>ファイルとメディアは禁止されています!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter unread and favorite chats." xml:space="preserve">
<source>Filter unread and favorite chats.</source>
<target>未読とお気に入りをフィルターします。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
<source>Finally, we have them! 🚀</source>
<target>ついに、私たちはそれらを手に入れました! 🚀</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Find chats faster" xml:space="preserve">
<source>Find chats faster</source>
<target>チャットを素早く検索</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix" xml:space="preserve">
<source>Fix</source>
<target>修正</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix connection" xml:space="preserve">
<source>Fix connection</source>
<target>接続を修正</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix connection?" xml:space="preserve">
<source>Fix connection?</source>
<target>接続を修正しますか?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix encryption after restoring backups." xml:space="preserve">
<source>Fix encryption after restoring backups.</source>
<target>バックアップの復元後に暗号化を修正します。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix not supported by contact" xml:space="preserve">
<source>Fix not supported by contact</source>
<target>連絡先による修正はサポートされていません</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fix not supported by group member" xml:space="preserve">
<source>Fix not supported by group member</source>
<target>グループメンバーによる修正はサポートされていません</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="For console" xml:space="preserve">
@ -2330,6 +2373,7 @@
</trans-unit>
<trans-unit id="Group members can send files and media." xml:space="preserve">
<source>Group members can send files and media.</source>
<target>グループメンバーはファイルやメディアを送信できます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send voice messages." xml:space="preserve">
@ -2529,6 +2573,7 @@
</trans-unit>
<trans-unit id="In reply to" xml:space="preserve">
<source>In reply to</source>
<target>返信先</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito" xml:space="preserve">
@ -2543,6 +2588,7 @@
</trans-unit>
<trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve">
<source>Incognito mode protects your privacy by using a new random profile for each contact.</source>
<target>シークレットモードとは、メインのプロフィールとプロフィール画像を守るために、新しい連絡先を追加する時に、その連絡先に対してランダムなプロフィールが作成されるという対策です。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incoming audio call" xml:space="preserve">
@ -2619,6 +2665,7 @@
</trans-unit>
<trans-unit id="Invalid status" xml:space="preserve">
<source>Invalid status</source>
<target>無効なステータス</target>
<note>item status text</note>
</trans-unit>
<trans-unit id="Invitation expired!" xml:space="preserve">
@ -2714,6 +2761,7 @@
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source>
<target>接続を維持</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="KeyChain error" xml:space="preserve">
@ -2808,6 +2856,7 @@
</trans-unit>
<trans-unit id="Make one message disappear" xml:space="preserve">
<source>Make one message disappear</source>
<target>メッセージを1つ消す</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Make profile private!" xml:space="preserve">
@ -2966,6 +3015,7 @@
</trans-unit>
<trans-unit id="Most likely this connection is deleted." xml:space="preserve">
<source>Most likely this connection is deleted.</source>
<target>おそらく、この接続は削除されています。</target>
<note>item status description</note>
</trans-unit>
<trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve">
@ -3075,6 +3125,7 @@
</trans-unit>
<trans-unit id="No delivery information" xml:space="preserve">
<source>No delivery information</source>
<target>送信情報なし</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No device token!" xml:space="preserve">
@ -3084,6 +3135,7 @@
</trans-unit>
<trans-unit id="No filtered chats" xml:space="preserve">
<source>No filtered chats</source>
<target>フィルタされたチャットはありません</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No group!" xml:space="preserve">
@ -3093,6 +3145,7 @@
</trans-unit>
<trans-unit id="No history" xml:space="preserve">
<source>No history</source>
<target>履歴はありません</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record voice message" xml:space="preserve">
@ -3181,6 +3234,7 @@
</trans-unit>
<trans-unit id="Only group owners can enable files and media." xml:space="preserve">
<source>Only group owners can enable files and media.</source>
<target>ファイルやメディアを有効にできるのは、グループオーナーだけです。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable voice messages." xml:space="preserve">
@ -3505,6 +3559,7 @@
</trans-unit>
<trans-unit id="Prohibit sending files and media." xml:space="preserve">
<source>Prohibit sending files and media.</source>
<target>ファイルやメディアの送信を禁止します。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending voice messages." xml:space="preserve">
@ -3529,6 +3584,7 @@
</trans-unit>
<trans-unit id="Protocol timeout per KB" xml:space="preserve">
<source>Protocol timeout per KB</source>
<target>KB あたりのプロトコル タイムアウト</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Push notifications" xml:space="preserve">
@ -3543,6 +3599,7 @@
</trans-unit>
<trans-unit id="React…" xml:space="preserve">
<source>React…</source>
<target>反応する…</target>
<note>chat item menu</note>
</trans-unit>
<trans-unit id="Read" xml:space="preserve">
@ -3601,6 +3658,7 @@
</trans-unit>
<trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve">
<source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source>
<target>開発中の機能です相手のクライアントが4.2でなければ機能しません。アドレス変更が完了すると、会話にメッセージが出ます。連絡相手 (またはグループのメンバー) からメッセージを受信できないかをご確認ください。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Receiving file will be stopped." xml:space="preserve">
@ -3620,10 +3678,12 @@
</trans-unit>
<trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve">
<source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source>
<target>接続されているすべてのサーバーを再接続して、メッセージを強制的に配信します。 追加のトラフィックを使用します。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reconnect servers?" xml:space="preserve">
<source>Reconnect servers?</source>
<target>サーバーに再接続しますか?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Record updated at" xml:space="preserve">
@ -3688,14 +3748,17 @@
</trans-unit>
<trans-unit id="Renegotiate" xml:space="preserve">
<source>Renegotiate</source>
<target>再ネゴシエート</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Renegotiate encryption" xml:space="preserve">
<source>Renegotiate encryption</source>
<target>暗号化の再ネゴシエート</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Renegotiate encryption?" xml:space="preserve">
<source>Renegotiate encryption?</source>
<target>暗号化を再ネゴシエートしますか?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reply" xml:space="preserve">
@ -4182,6 +4245,7 @@
</trans-unit>
<trans-unit id="Show last messages" xml:space="preserve">
<source>Show last messages</source>
<target>最新のメッセージを表示</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show preview" xml:space="preserve">
@ -4266,10 +4330,12 @@
</trans-unit>
<trans-unit id="Small groups (max 20)" xml:space="preserve">
<source>Small groups (max 20)</source>
<target>小グループ最大20名</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve">
<source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source>
<target>インポート中に致命的でないエラーが発生しました - 詳細はチャットコンソールを参照してください。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Somebody" xml:space="preserve">
@ -4486,6 +4552,7 @@ It can happen because of some bug or when the connection is compromised.</source
</trans-unit>
<trans-unit id="The encryption is working and the new encryption agreement is not required. It may result in connection errors!" xml:space="preserve">
<source>The encryption is working and the new encryption agreement is not required. It may result in connection errors!</source>
<target>暗号化は機能しており、新しい暗号化への同意は必要ありません。接続エラーが発生する可能性があります!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The group is fully decentralized it is visible only to the members." xml:space="preserve">
@ -4525,6 +4592,7 @@ It can happen because of some bug or when the connection is compromised.</source
</trans-unit>
<trans-unit id="The second tick we missed! ✅" xml:space="preserve">
<source>The second tick we missed! ✅</source>
<target>長らくお待たせしました! ✅</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The sender will NOT be notified" xml:space="preserve">
@ -4554,10 +4622,12 @@ It can happen because of some bug or when the connection is compromised.</source
</trans-unit>
<trans-unit id="These settings are for your current profile **%@**." xml:space="preserve">
<source>These settings are for your current profile **%@**.</source>
<target>これらの設定は現在のプロファイル **%@** 用です。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="They can be overridden in contact and group settings." xml:space="preserve">
<source>They can be overridden in contact and group settings.</source>
<target>これらは連絡先の設定が優先します。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve">
@ -4688,6 +4758,7 @@ You will be prompted to complete authentication before this feature is enabled.<
</trans-unit>
<trans-unit id="Unfav." xml:space="preserve">
<source>Unfav.</source>
<target>お気に入りを取り消す。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unhide" xml:space="preserve">
@ -4819,6 +4890,7 @@ To connect, please ask your contact to create another connection link and check
</trans-unit>
<trans-unit id="Use current profile" xml:space="preserve">
<source>Use current profile</source>
<target>現在のプロファイルを使用する</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use for new connections" xml:space="preserve">
@ -4833,6 +4905,7 @@ To connect, please ask your contact to create another connection link and check
</trans-unit>
<trans-unit id="Use new incognito profile" xml:space="preserve">
<source>Use new incognito profile</source>
<target>新しいシークレットプロファイルを使用する</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use server" xml:space="preserve">
@ -5047,10 +5120,12 @@ To connect, please ask your contact to create another connection link and check
</trans-unit>
<trans-unit id="You can enable later via Settings" xml:space="preserve">
<source>You can enable later via Settings</source>
<target>あとで設定から有効にできます</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can enable them later via app Privacy &amp; Security settings." xml:space="preserve">
<source>You can enable them later via app Privacy &amp; Security settings.</source>
<target>あとでアプリのプライバシーとセキュリティの設定から有効にすることができます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve">
@ -5309,6 +5384,7 @@ You can change it in Settings.</source>
</trans-unit>
<trans-unit id="Your profile **%@** will be shared." xml:space="preserve">
<source>Your profile **%@** will be shared.</source>
<target>あなたのプロファイル **%@** が共有されます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile is stored on your device and shared only with your contacts.&#10;SimpleX servers cannot see your profile." xml:space="preserve">
@ -5385,10 +5461,12 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
</trans-unit>
<trans-unit id="agreeing encryption for %@…" xml:space="preserve">
<source>agreeing encryption for %@…</source>
<target>%@の暗号化に同意しています…</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="agreeing encryption…" xml:space="preserve">
<source>agreeing encryption…</source>
<target>暗号化に同意しています…</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="always" xml:space="preserve">
@ -5453,10 +5531,12 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
</trans-unit>
<trans-unit id="changing address for %@…" xml:space="preserve">
<source>changing address for %@…</source>
<target>%@ のアドレスを変更しています…</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="changing address…" xml:space="preserve">
<source>changing address…</source>
<target>アドレスを変更しています…</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="colored" xml:space="preserve">
@ -5561,10 +5641,12 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
</trans-unit>
<trans-unit id="default (no)" xml:space="preserve">
<source>default (no)</source>
<target>デフォルト(いいえ)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="default (yes)" xml:space="preserve">
<source>default (yes)</source>
<target>デフォルト(はい)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="deleted" xml:space="preserve">
@ -5589,6 +5671,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
</trans-unit>
<trans-unit id="disabled" xml:space="preserve">
<source>disabled</source>
<target>無効</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="duplicate message" xml:space="preserve">
@ -5618,34 +5701,42 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
</trans-unit>
<trans-unit id="encryption agreed" xml:space="preserve">
<source>encryption agreed</source>
<target>暗号化に同意しました</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption agreed for %@" xml:space="preserve">
<source>encryption agreed for %@</source>
<target>%@ の暗号化に同意しました</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption ok" xml:space="preserve">
<source>encryption ok</source>
<target>暗号化OK</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption ok for %@" xml:space="preserve">
<source>encryption ok for %@</source>
<target>%@ の暗号化OK</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption re-negotiation allowed" xml:space="preserve">
<source>encryption re-negotiation allowed</source>
<target>暗号化の再ネゴシエーションを許可</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption re-negotiation allowed for %@" xml:space="preserve">
<source>encryption re-negotiation allowed for %@</source>
<target>%@ の暗号化の再ネゴシエーションを許可</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption re-negotiation required" xml:space="preserve">
<source>encryption re-negotiation required</source>
<target>暗号化の再ネゴシエーションが必要</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="encryption re-negotiation required for %@" xml:space="preserve">
<source>encryption re-negotiation required for %@</source>
<target>%@ の暗号化の再ネゴシエーションが必要</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="ended" xml:space="preserve">
@ -5665,6 +5756,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
</trans-unit>
<trans-unit id="event happened" xml:space="preserve">
<source>event happened</source>
<target>イベント発生</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="group deleted" xml:space="preserve">
@ -5834,6 +5926,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
</trans-unit>
<trans-unit id="no text" xml:space="preserve">
<source>no text</source>
<target>テキストなし</target>
<note>copied message info in history</note>
</trans-unit>
<trans-unit id="observer" xml:space="preserve">
@ -5924,6 +6017,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
</trans-unit>
<trans-unit id="security code changed" xml:space="preserve">
<source>security code changed</source>
<target>セキュリティコードが変更されました</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="starting…" xml:space="preserve">

View File

@ -244,7 +244,7 @@
</trans-unit>
<trans-unit id="%u messages failed to decrypt." xml:space="preserve">
<source>%u messages failed to decrypt.</source>
<target>%u-berichten kunnen niet worden gedecodeerd.</target>
<target>%u berichten kunnen niet worden ontsleuteld.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%u messages skipped." xml:space="preserve">
@ -2713,7 +2713,7 @@
</trans-unit>
<trans-unit id="It can happen when you or your connection used the old database backup." xml:space="preserve">
<source>It can happen when you or your connection used the old database backup.</source>
<target>Het kan gebeuren wanneer u of uw verbinding de oude databaseback-up gebruikte.</target>
<target>Het kan gebeuren wanneer u of de ander een oude databaseback-up gebruikt.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="It can happen when:&#10;1. The messages expired in the sending client after 2 days or on the server after 30 days.&#10;2. Message decryption failed, because you or your contact used old database backup.&#10;3. The connection was compromised." xml:space="preserve">
@ -4919,7 +4919,7 @@ Om verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link
</trans-unit>
<trans-unit id="Use new incognito profile" xml:space="preserve">
<source>Use new incognito profile</source>
<target>Gebruik een nieuw incognito -profiel</target>
<target>Gebruik een nieuw incognitoprofiel</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use server" xml:space="preserve">

View File

@ -89,6 +89,7 @@
</trans-unit>
<trans-unit id="%@ and %@ connected" xml:space="preserve">
<source>%@ and %@ connected</source>
<target>%@ i %@ połączeni</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ at %@:" xml:space="preserve">
@ -123,6 +124,7 @@
</trans-unit>
<trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve">
<source>%@, %@ and %lld other members connected</source>
<target>%@, %@ i %lld innych członków połączeni</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@:" xml:space="preserve">
@ -4256,6 +4258,7 @@
</trans-unit>
<trans-unit id="Show last messages" xml:space="preserve">
<source>Show last messages</source>
<target>Pokaż ostatnie wiadomości</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show preview" xml:space="preserve">
@ -5767,6 +5770,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit>
<trans-unit id="event happened" xml:space="preserve">
<source>event happened</source>
<target>nowe wydarzenie</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="group deleted" xml:space="preserve">

View File

@ -44,14 +44,17 @@
</trans-unit>
<trans-unit id="# %@" xml:space="preserve">
<source># %@</source>
<target># %@</target>
<note>copied message info title, # &lt;title&gt;</note>
</trans-unit>
<trans-unit id="## History" xml:space="preserve">
<source>## History</source>
<target>## 历史</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="## In reply to" xml:space="preserve">
<source>## In reply to</source>
<target>## 回复</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="#secret#" xml:space="preserve">
@ -86,10 +89,12 @@
</trans-unit>
<trans-unit id="%@ and %@ connected" xml:space="preserve">
<source>%@ and %@ connected</source>
<target>%@ 和%@ 以建立连接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ at %@:" xml:space="preserve">
<source>%1$@ at %2$@:</source>
<target>%2$@:</target>
<note>copied message info, &lt;sender&gt; at &lt;time&gt;</note>
</trans-unit>
<trans-unit id="%@ is connected!" xml:space="preserve">
@ -119,6 +124,7 @@
</trans-unit>
<trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve">
<source>%@, %@ and %lld other members connected</source>
<target>%@, %@ 和 %lld 个成员</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@:" xml:space="preserve">
@ -325,6 +331,9 @@
<source>- more stable message delivery.
- a bit better groups.
- and more!</source>
<target>- 更稳定的传输!
- 更好的社群!
- 以及更多!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve">
@ -405,6 +414,7 @@
</trans-unit>
<trans-unit id="A few more things" xml:space="preserve">
<source>A few more things</source>
<target/>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="A new contact" xml:space="preserve">
@ -414,6 +424,7 @@
</trans-unit>
<trans-unit id="A new random profile will be shared." xml:space="preserve">
<source>A new random profile will be shared.</source>
<target>创建一个随机的共享文件</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve">

View File

@ -88,6 +88,15 @@
/* No comment provided by engineer. */
"*bold*" = "\\*tučně*";
/* copied message info title, # <title> */
"# %@" = "# %@";
/* copied message info */
"## History" = "## Historie";
/* copied message info */
"## In reply to" = "## Odpovídáno";
/* No comment provided by engineer. */
"#secret#" = "#tajný#";
@ -106,6 +115,9 @@
/* No comment provided by engineer. */
"%@ %@" = "%@ %@";
/* No comment provided by engineer. */
"%@ and %@ connected" = "%@ a %@ připojen";
/* copied message info, <sender> at <time> */
"%@ at %@:" = "%1$@ na %2$@:";
@ -124,6 +136,9 @@
/* notification title */
"%@ wants to connect!" = "%@ se chce připojit!";
/* No comment provided by engineer. */
"%@, %@ and %lld other members connected" = "%@, %@ a %lld ostatní členové připojeni";
/* copied message info */
"%@:" = "%@:";
@ -282,7 +297,7 @@
"Accept" = "Přijmout";
/* No comment provided by engineer. */
"Accept connection request?" = "Přijmout kontakt";
"Accept connection request?" = "Přijmout kontakt?";
/* notification body */
"Accept contact request from %@?" = "Přijmout žádost o kontakt od %@?";
@ -693,11 +708,17 @@
/* server test step */
"Connect" = "Připojit";
/* No comment provided by engineer. */
"Connect directly" = "Připojit přímo";
/* No comment provided by engineer. */
"Connect incognito" = "Spojit se inkognito";
/* No comment provided by engineer. */
"connect to SimpleX Chat developers." = "připojit se k vývojářům SimpleX Chat.";
/* No comment provided by engineer. */
"Connect via contact link" = "Připojit se přes kontaktní odkaz?";
"Connect via contact link" = "Připojit se přes odkaz";
/* No comment provided by engineer. */
"Connect via group link?" = "Připojit se přes odkaz skupiny?";
@ -709,7 +730,7 @@
"Connect via link / QR code" = "Připojit se prostřednictvím odkazu / QR kódu";
/* No comment provided by engineer. */
"Connect via one-time link" = "Připojit se jednorázovým odkazem?";
"Connect via one-time link" = "Připojit se jednorázovým odkazem";
/* No comment provided by engineer. */
"connected" = "připojeno";
@ -1053,6 +1074,9 @@
/* rcv group event chat item */
"deleted group" = "odstraněna skupina";
/* No comment provided by engineer. */
"Delivery" = "Doručenka";
/* No comment provided by engineer. */
"Delivery receipts are disabled!" = "Potvrzení o doručení jsou vypnuté!";
@ -1722,6 +1746,9 @@
/* No comment provided by engineer. */
"Incognito mode" = "Režim inkognito";
/* No comment provided by engineer. */
"Incognito mode protects your privacy by using a new random profile for each contact." = "Režim inkognito chrání vaše soukromí používáním nového náhodného profilu pro každý kontakt.";
/* chat list item description */
"incognito via contact address link" = "inkognito přes odkaz na kontaktní adresu";
@ -1785,6 +1812,9 @@
/* No comment provided by engineer. */
"Invalid server address!" = "Neplatná adresa serveru!";
/* item status text */
"Invalid status" = "Neplatný status";
/* No comment provided by engineer. */
"Invitation expired!" = "Platnost pozvánky vypršela!";
@ -1858,10 +1888,10 @@
"Join group" = "Připojit ke skupině";
/* No comment provided by engineer. */
"Join incognito" = "Připojte se inkognito";
"Join incognito" = "Připojit se inkognito";
/* No comment provided by engineer. */
"Joining group" = "Připojení ke skupině";
"Joining group" = "Připojování ke skupině";
/* No comment provided by engineer. */
"Keep your connections" = "Zachovat vaše připojení";
@ -2046,6 +2076,9 @@
/* No comment provided by engineer. */
"More improvements are coming soon!" = "Další vylepšení se chystají již brzy!";
/* item status description */
"Most likely this connection is deleted." = "Pravděpodobně je toto spojení smazáno.";
/* No comment provided by engineer. */
"Most likely this contact has deleted the connection with you." = "Tento kontakt s největší pravděpodobností smazal spojení s vámi.";
@ -3071,7 +3104,7 @@
"These settings are for your current profile **%@**." = "Toto nastavení je pro váš aktuální profil **%@**.";
/* No comment provided by engineer. */
"They can be overridden in contact and group settings." = "Mohou být přepsány v nastavení kontaktů";
"They can be overridden in contact and group settings." = "Mohou být přepsány v nastavení kontaktů.";
/* No comment provided by engineer. */
"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Tuto akci nelze vrátit zpět - všechny přijaté a odeslané soubory a média budou smazány. Obrázky s nízkým rozlišením zůstanou zachovány.";

View File

@ -281,7 +281,7 @@
"About SimpleX" = "Acerca de SimpleX";
/* No comment provided by engineer. */
"About SimpleX address" = "Acerca de dirección SimpleX";
"About SimpleX address" = "Acerca de la dirección SimpleX";
/* No comment provided by engineer. */
"About SimpleX Chat" = "Sobre SimpleX Chat";
@ -832,7 +832,7 @@
"Create" = "Crear";
/* No comment provided by engineer. */
"Create an address to let people connect with you." = "Crear una dirección para que otras personas se puedan conectar contigo.";
"Create an address to let people connect with you." = "Crea una dirección para que otras personas puedan conectar contigo.";
/* server test step */
"Create file" = "Crear archivo";
@ -853,10 +853,10 @@
"Create secret group" = "Crea grupo secreto";
/* No comment provided by engineer. */
"Create SimpleX address" = "Crear dirección SimpleX";
"Create SimpleX address" = "Crear tu dirección SimpleX";
/* No comment provided by engineer. */
"Create your profile" = "Crear tu perfil";
"Create your profile" = "Crea tu perfil";
/* No comment provided by engineer. */
"Created on %@" = "Creado en %@";
@ -943,7 +943,7 @@
"days" = "días";
/* No comment provided by engineer. */
"Decentralized" = "Descentralizado";
"Decentralized" = "Descentralizada";
/* message decrypt error item */
"Decryption error" = "Error descifrado";
@ -1162,7 +1162,7 @@
"Do NOT use SimpleX for emergency calls." = "NO uses SimpleX para llamadas de emergencia.";
/* No comment provided by engineer. */
"Don't create address" = "No crear dirección";
"Don't create address" = "No crear dirección SimpleX";
/* No comment provided by engineer. */
"Don't enable" = "No activar";
@ -1981,7 +1981,7 @@
"Mark verified" = "Marcar como verificado";
/* No comment provided by engineer. */
"Markdown in messages" = "Sintaxis markdown en los mensajes";
"Markdown in messages" = "Sintaxis Markdown";
/* marked deleted chat item preview text */
"marked deleted" = "marcado eliminado";
@ -2510,7 +2510,7 @@
"Received message" = "Mensaje entrante";
/* No comment provided by engineer. */
"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "La dirección de recepción se cambiará. El cambio se completará cuando el remitente esté en línea.";
"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "La dirección de recepción pasará a otro servidor. El cambio se completará cuando el remitente esté en línea.";
/* No comment provided by engineer. */
"Receiving file will be stopped." = "Se detendrá la recepción del archivo.";
@ -2966,7 +2966,7 @@
"Stop" = "Detener";
/* No comment provided by engineer. */
"Stop chat to enable database actions" = "Para habilitar las acciones sobre la base de datos, previamente debes detener Chat";
"Stop chat to enable database actions" = "Detén SimpleX para habilitar las acciones sobre la base de datos";
/* No comment provided by engineer. */
"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Para poder exportar, importar o eliminar la base de datos primero debes detener Chat. Durante el tiempo que esté detenido no podrás recibir ni enviar mensajes.";
@ -3095,7 +3095,7 @@
"The message will be marked as moderated for all members." = "El mensaje será marcado como moderado para todos los miembros.";
/* No comment provided by engineer. */
"The next generation of private messaging" = "La próxima generación de mensajería privada";
"The next generation of private messaging" = "La nueva generación de mensajería privada";
/* No comment provided by engineer. */
"The old database was not removed during the migration, it can be deleted." = "La base de datos antigua no se eliminó durante la migración, puede eliminarse.";
@ -3464,7 +3464,7 @@
"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "También puedes conectarte haciendo clic en el enlace. Si se abre en el navegador, haz clic en el botón **Abrir en aplicación móvil**.";
/* No comment provided by engineer. */
"You can create it later" = "Puedes crearlo más tarde";
"You can create it later" = "Puedes crearla más tarde";
/* No comment provided by engineer. */
"You can enable later via Settings" = "Puedes activar más tarde en Configuración";
@ -3599,7 +3599,7 @@
"Your calls" = "Llamadas";
/* No comment provided by engineer. */
"Your chat database" = "Base de datos Chat";
"Your chat database" = "Base de datos";
/* No comment provided by engineer. */
"Your chat database is not encrypted - set passphrase to encrypt it." = "La base de datos no está cifrada - establece una contraseña para cifrarla.";
@ -3647,7 +3647,7 @@
"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Tu perfil se almacena en tu dispositivo y sólo se comparte con tus contactos.\nLos servidores de SimpleX no pueden ver tu perfil.";
/* No comment provided by engineer. */
"Your profile, contacts and delivered messages are stored on your device." = "Tu perfil, contactos y mensajes entregados se almacenan en tu dispositivo.";
"Your profile, contacts and delivered messages are stored on your device." = "Tu perfil, contactos y mensajes se almacenan en tu dispositivo.";
/* No comment provided by engineer. */
"Your random profile" = "Tu perfil aleatorio";

View File

@ -1273,7 +1273,7 @@
"encryption agreed for %@" = "chiffrement accepté pour %@";
/* chat item text */
"encryption ok" = "chiffrement ok";
"encryption ok" = "chiffrement OK";
/* chat item text */
"encryption ok for %@" = "chiffrement ok pour %@";

View File

@ -19,6 +19,9 @@
/* No comment provided by engineer. */
"_italic_" = "\\_斜体_";
/* No comment provided by engineer. */
"- more stable message delivery.\n- a bit better groups.\n- and more!" = "- より安定したメッセージ配信。\n- 改良されたグループ。\n- などなど!";
/* No comment provided by engineer. */
"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- 最長 5 分間の音声メッセージ。\n- 消えるまでのカスタム時間。\n- 編集履歴。";
@ -85,6 +88,15 @@
/* No comment provided by engineer. */
"*bold*" = "\\*太文字*";
/* copied message info title, # <title> */
"# %@" = "# %@";
/* copied message info */
"## History" = "## 履歴";
/* copied message info */
"## In reply to" = "## 返信先";
/* No comment provided by engineer. */
"#secret#" = "シークレット";
@ -103,6 +115,12 @@
/* No comment provided by engineer. */
"%@ %@" = "%@ %@";
/* No comment provided by engineer. */
"%@ and %@ connected" = "%@ と %@ は接続中";
/* copied message info, <sender> at <time> */
"%@ at %@:" = "%1$@ at %2$@:";
/* notification title */
"%@ is connected!" = "%@ 接続中!";
@ -118,6 +136,9 @@
/* notification title */
"%@ wants to connect!" = "%@ が接続を希望しています!";
/* No comment provided by engineer. */
"%@, %@ and %lld other members connected" = "%@, %@ および %lld 人のメンバーが接続中";
/* copied message info */
"%@:" = "%@:";
@ -232,15 +253,30 @@
/* No comment provided by engineer. */
"30 seconds" = "30秒";
/* No comment provided by engineer. */
"A few more things" = "その他";
/* notification title */
"A new contact" = "新しい連絡先";
/* No comment provided by engineer. */
"A new random profile will be shared." = "新しいランダムなプロファイルが共有されます。";
/* No comment provided by engineer. */
"A separate TCP connection will be used **for each chat profile you have in the app**." = "**アプリ内のチャット プロフィールごとに**、個別の TCP 接続が使用されます。";
/* No comment provided by engineer. */
"A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "**アプリ内のチャット プロファイルごとに**、個別の TCP 接続が使用されます。\n**注意**:多くの接続がある場合、バッテリーと通信量の消費が大幅に増加し、一部の接続に失敗することがあります。";
/* No comment provided by engineer. */
"Abort" = "中止";
/* No comment provided by engineer. */
"Abort changing address" = "アドレス変更の中止";
/* No comment provided by engineer. */
"Abort changing address?" = "アドレス変更を中止しますか?";
/* No comment provided by engineer. */
"About SimpleX" = "SimpleXについて";
@ -296,6 +332,9 @@
/* No comment provided by engineer. */
"Address" = "アドレス";
/* No comment provided by engineer. */
"Address change will be aborted. Old receiving address will be used." = "アドレス変更は中止されます。古い受信アドレスが使用されます。";
/* member role */
"admin" = "管理者";
@ -305,6 +344,12 @@
/* No comment provided by engineer. */
"Advanced network settings" = "ネットワーク詳細設定";
/* chat item text */
"agreeing encryption for %@…" = "%@の暗号化に同意しています…";
/* chat item text */
"agreeing encryption…" = "暗号化に同意しています…";
/* No comment provided by engineer. */
"All app data is deleted." = "すべてのアプリデータが削除されます。";
@ -353,6 +398,9 @@
/* No comment provided by engineer. */
"Allow to irreversibly delete sent messages." = "送信済みメッセージの永久削除を許可する。";
/* No comment provided by engineer. */
"Allow to send files and media." = "ファイルやメディアの送信を許可する。";
/* No comment provided by engineer. */
"Allow to send voice messages." = "音声メッセージの送信を許可する。";
@ -467,6 +515,9 @@
/* No comment provided by engineer. */
"Bad message ID" = "メッセージ ID が正しくありません";
/* No comment provided by engineer. */
"Better messages" = "より良いメッセージ";
/* No comment provided by engineer. */
"bold" = "太文字";
@ -564,6 +615,12 @@
/* rcv group event chat item */
"changed your role to %@" = "あなたの役割を %@ に変更しました";
/* chat item text */
"changing address for %@…" = "%@ のアドレスを変更しています…";
/* chat item text */
"changing address…" = "アドレスを変更しています…";
/* No comment provided by engineer. */
"Chat archive" = "チャットのアーカイブ";
@ -651,6 +708,12 @@
/* server test step */
"Connect" = "接続";
/* No comment provided by engineer. */
"Connect directly" = "直接接続する";
/* No comment provided by engineer. */
"Connect incognito" = "シークレットモードで接続";
/* No comment provided by engineer. */
"connect to SimpleX Chat developers." = "SimpleX Chat 開発者に接続します。";
@ -750,6 +813,9 @@
/* No comment provided by engineer. */
"Contact preferences" = "連絡先の設定";
/* No comment provided by engineer. */
"Contacts" = "連絡先";
/* No comment provided by engineer. */
"Contacts can mark messages for deletion; you will be able to view them." = "連絡先はメッセージを削除対象とすることができます。あなたには閲覧可能です。";
@ -885,6 +951,12 @@
/* pref value */
"default (%@)" = "デフォルト (%@)";
/* No comment provided by engineer. */
"default (no)" = "デフォルト(いいえ)";
/* No comment provided by engineer. */
"default (yes)" = "デフォルト(はい)";
/* chat item action */
"Delete" = "削除";
@ -1002,6 +1074,12 @@
/* rcv group event chat item */
"deleted group" = "削除されたグループ";
/* No comment provided by engineer. */
"Delivery" = "Delivery";
/* No comment provided by engineer. */
"Delivery receipts are disabled!" = "Delivery receipts are disabled!";
/* No comment provided by engineer. */
"Description" = "説明";
@ -1035,9 +1113,18 @@
/* No comment provided by engineer. */
"Direct messages between members are prohibited in this group." = "このグループではメンバー間のダイレクトメッセージが使用禁止です。";
/* No comment provided by engineer. */
"Disable (keep overrides)" = "無効にする(設定の優先を維持)";
/* No comment provided by engineer. */
"Disable for all" = "すべて無効";
/* authentication reason */
"Disable SimpleX Lock" = "SimpleXロックを無効にする";
/* No comment provided by engineer. */
"disabled" = "無効";
/* No comment provided by engineer. */
"Disappearing message" = "消えるメッセージ";
@ -1074,6 +1161,9 @@
/* No comment provided by engineer. */
"Don't create address" = "アドレスを作成しないでください";
/* No comment provided by engineer. */
"Don't enable" = "有効にしない";
/* No comment provided by engineer. */
"Don't show again" = "次から表示しない";
@ -1104,9 +1194,15 @@
/* No comment provided by engineer. */
"Enable" = "有効";
/* No comment provided by engineer. */
"Enable (keep overrides)" = "有効にする(設定の優先を維持)";
/* No comment provided by engineer. */
"Enable automatic message deletion?" = "自動メッセージ削除を有効にしますか?";
/* No comment provided by engineer. */
"Enable for all" = "すべて有効";
/* No comment provided by engineer. */
"Enable instant notifications?" = "即時通知を有効にしますか?";
@ -1167,6 +1263,30 @@
/* notification */
"Encrypted message: unexpected error" = "暗号化されたメッセージ : 予期しないエラー";
/* chat item text */
"encryption agreed" = "暗号化に同意しました";
/* chat item text */
"encryption agreed for %@" = "%@ の暗号化に同意しました";
/* chat item text */
"encryption ok" = "暗号化OK";
/* chat item text */
"encryption ok for %@" = "%@ の暗号化OK";
/* chat item text */
"encryption re-negotiation allowed" = "暗号化の再ネゴシエーションを許可";
/* chat item text */
"encryption re-negotiation allowed for %@" = "%@ の暗号化の再ネゴシエーションを許可";
/* chat item text */
"encryption re-negotiation required" = "暗号化の再ネゴシエーションが必要";
/* chat item text */
"encryption re-negotiation required for %@" = "%@ の暗号化の再ネゴシエーションが必要";
/* No comment provided by engineer. */
"ended" = "終了";
@ -1200,6 +1320,9 @@
/* No comment provided by engineer. */
"Error" = "エラー";
/* No comment provided by engineer. */
"Error aborting address change" = "アドレス変更中止エラー";
/* No comment provided by engineer. */
"Error accepting contact request" = "連絡先リクエストの承諾にエラー発生";
@ -1311,6 +1434,9 @@
/* No comment provided by engineer. */
"Error switching profile!" = "プロフィール切り替えにエラー発生!";
/* No comment provided by engineer. */
"Error synchronizing connection" = "接続の同期エラー";
/* No comment provided by engineer. */
"Error updating group link" = "グループのリンクのアップデートにエラー発生";
@ -1335,6 +1461,12 @@
/* No comment provided by engineer. */
"Error: URL is invalid" = "エラー: 無効なURL";
/* No comment provided by engineer. */
"Even when disabled in the conversation." = "会話中に無効になっている場合でも。";
/* No comment provided by engineer. */
"event happened" = "イベント発生";
/* No comment provided by engineer. */
"Exit without saving" = "保存せずに閉じる";
@ -1356,6 +1488,9 @@
/* No comment provided by engineer. */
"Fast and no wait until the sender is online!" = "送信者がオンラインになるまでの待ち時間がなく、速い!";
/* No comment provided by engineer. */
"Favorite" = "お気に入り";
/* No comment provided by engineer. */
"File will be deleted from servers." = "ファイルはサーバーから削除されます。";
@ -1371,6 +1506,42 @@
/* No comment provided by engineer. */
"Files & media" = "ファイルとメディア";
/* chat feature */
"Files and media" = "ファイルとメディア";
/* No comment provided by engineer. */
"Files and media are prohibited in this group." = "このグループでは、ファイルとメディアは禁止されています。";
/* No comment provided by engineer. */
"Files and media prohibited!" = "ファイルとメディアは禁止されています!";
/* No comment provided by engineer. */
"Filter unread and favorite chats." = "未読とお気に入りをフィルターします。";
/* No comment provided by engineer. */
"Finally, we have them! 🚀" = "ついに、私たちはそれらを手に入れました! 🚀";
/* No comment provided by engineer. */
"Find chats faster" = "チャットを素早く検索";
/* No comment provided by engineer. */
"Fix" = "修正";
/* No comment provided by engineer. */
"Fix connection" = "接続を修正";
/* No comment provided by engineer. */
"Fix connection?" = "接続を修正しますか?";
/* No comment provided by engineer. */
"Fix encryption after restoring backups." = "バックアップの復元後に暗号化を修正します。";
/* No comment provided by engineer. */
"Fix not supported by contact" = "連絡先による修正はサポートされていません";
/* No comment provided by engineer. */
"Fix not supported by group member" = "グループメンバーによる修正はサポートされていません";
/* No comment provided by engineer. */
"For console" = "コンソール";
@ -1437,6 +1608,9 @@
/* No comment provided by engineer. */
"Group members can send disappearing messages." = "グループのメンバーが消えるメッセージを送信できます。";
/* No comment provided by engineer. */
"Group members can send files and media." = "グループメンバーはファイルやメディアを送信できます。";
/* No comment provided by engineer. */
"Group members can send voice messages." = "グループのメンバーが音声メッセージを送信できます。";
@ -1560,12 +1734,18 @@
/* No comment provided by engineer. */
"Improved server configuration" = "サーバ設定の向上";
/* No comment provided by engineer. */
"In reply to" = "返信先";
/* No comment provided by engineer. */
"Incognito" = "シークレットモード";
/* No comment provided by engineer. */
"Incognito mode" = "シークレットモード";
/* No comment provided by engineer. */
"Incognito mode protects your privacy by using a new random profile for each contact." = "シークレットモードとは、メインのプロフィールとプロフィール画像を守るために、新しい連絡先を追加する時に、その連絡先に対してランダムなプロフィールが作成されるという対策です。";
/* chat list item description */
"incognito via contact address link" = "連絡先リンク経由でシークレットモード";
@ -1629,6 +1809,9 @@
/* No comment provided by engineer. */
"Invalid server address!" = "無効なサーバアドレス!";
/* item status text */
"Invalid status" = "無効なステータス";
/* No comment provided by engineer. */
"Invitation expired!" = "招待が期限切れました!";
@ -1707,6 +1890,9 @@
/* No comment provided by engineer. */
"Joining group" = "グループに参加";
/* No comment provided by engineer. */
"Keep your connections" = "接続を維持";
/* No comment provided by engineer. */
"Keychain error" = "キーチェーンのエラー";
@ -1764,6 +1950,9 @@
/* No comment provided by engineer. */
"Make a private connection" = "プライベートな接続をする";
/* No comment provided by engineer. */
"Make one message disappear" = "メッセージを1つ消す";
/* No comment provided by engineer. */
"Make profile private!" = "プロフィールを非表示にできます!";
@ -1881,6 +2070,9 @@
/* No comment provided by engineer. */
"More improvements are coming soon!" = "まだまだ改善してまいります!";
/* item status description */
"Most likely this connection is deleted." = "おそらく、この接続は削除されています。";
/* No comment provided by engineer. */
"Most likely this contact has deleted the connection with you." = "恐らくこの連絡先があなたとの接続を削除しました。";
@ -1953,21 +2145,33 @@
/* No comment provided by engineer. */
"No contacts to add" = "追加できる連絡先がありません";
/* No comment provided by engineer. */
"No delivery information" = "送信情報なし";
/* No comment provided by engineer. */
"No device token!" = "デバイストークンがありません!";
/* No comment provided by engineer. */
"no e2e encryption" = "エンドツーエンド暗号化がありません";
/* No comment provided by engineer. */
"No filtered chats" = "フィルタされたチャットはありません";
/* No comment provided by engineer. */
"No group!" = "グループが見つかりません!";
/* No comment provided by engineer. */
"No history" = "履歴はありません";
/* No comment provided by engineer. */
"No permission to record voice message" = "音声メッセージを録音する権限がありません";
/* No comment provided by engineer. */
"No received or sent files" = "送受信済みのファイルがありません";
/* copied message info in history */
"no text" = "テキストなし";
/* No comment provided by engineer. */
"Notifications" = "通知";
@ -2026,6 +2230,9 @@
/* No comment provided by engineer. */
"Only group owners can change group preferences." = "グループ設定を変えられるのはグループのオーナーだけです。";
/* No comment provided by engineer. */
"Only group owners can enable files and media." = "ファイルやメディアを有効にできるのは、グループオーナーだけです。";
/* No comment provided by engineer. */
"Only group owners can enable voice messages." = "音声メッセージを利用可能に設定できるのはグループのオーナーだけです。";
@ -2227,6 +2434,9 @@
/* No comment provided by engineer. */
"Prohibit sending disappearing messages." = "消えるメッセージを使用禁止にする。";
/* No comment provided by engineer. */
"Prohibit sending files and media." = "ファイルやメディアの送信を禁止します。";
/* No comment provided by engineer. */
"Prohibit sending voice messages." = "音声メッセージを使用禁止にする。";
@ -2239,12 +2449,18 @@
/* No comment provided by engineer. */
"Protocol timeout" = "プロトコル・タイムアウト";
/* No comment provided by engineer. */
"Protocol timeout per KB" = "KB あたりのプロトコル タイムアウト";
/* No comment provided by engineer. */
"Push notifications" = "プッシュ通知";
/* No comment provided by engineer. */
"Rate the app" = "アプリを評価";
/* chat item menu */
"React…" = "反応する…";
/* No comment provided by engineer. */
"Read" = "読む";
@ -2281,6 +2497,9 @@
/* message info title */
"Received message" = "受信したメッセージ";
/* No comment provided by engineer. */
"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "開発中の機能です相手のクライアントが4.2でなければ機能しません。アドレス変更が完了すると、会話にメッセージが出ます。連絡相手 (またはグループのメンバー) からメッセージを受信できないかをご確認ください。";
/* No comment provided by engineer. */
"Receiving file will be stopped." = "ファイルの受信を停止します。";
@ -2290,6 +2509,12 @@
/* No comment provided by engineer. */
"Recipients see updates as you type them." = "受信者には、入力時に更新内容が表示されます。";
/* No comment provided by engineer. */
"Reconnect all connected servers to force message delivery. It uses additional traffic." = "接続されているすべてのサーバーを再接続して、メッセージを強制的に配信します。 追加のトラフィックを使用します。";
/* No comment provided by engineer. */
"Reconnect servers?" = "サーバーに再接続しますか?";
/* No comment provided by engineer. */
"Record updated at" = "レコード更新日時";
@ -2338,6 +2563,15 @@
/* rcv group event chat item */
"removed you" = "あなたを除名しました";
/* No comment provided by engineer. */
"Renegotiate" = "再ネゴシエート";
/* No comment provided by engineer. */
"Renegotiate encryption" = "暗号化の再ネゴシエート";
/* No comment provided by engineer. */
"Renegotiate encryption?" = "暗号化を再ネゴシエートしますか?";
/* chat item action */
"Reply" = "返信";
@ -2476,6 +2710,9 @@
/* No comment provided by engineer. */
"Security code" = "セキュリティコード";
/* chat item text */
"security code changed" = "セキュリティコードが変更されました";
/* No comment provided by engineer. */
"Select" = "選択";
@ -2614,6 +2851,9 @@
/* No comment provided by engineer. */
"Show developer options" = "開発者向けオプションを表示";
/* No comment provided by engineer. */
"Show last messages" = "最新のメッセージを表示";
/* No comment provided by engineer. */
"Show preview" = "プレビューを表示";
@ -2662,9 +2902,15 @@
/* No comment provided by engineer. */
"Skipped messages" = "飛ばしたメッセージ";
/* No comment provided by engineer. */
"Small groups (max 20)" = "小グループ最大20名";
/* No comment provided by engineer. */
"SMP servers" = "SMPサーバ";
/* No comment provided by engineer. */
"Some non-fatal errors occurred during import - you may see Chat console for more details." = "インポート中に致命的でないエラーが発生しました - 詳細はチャットコンソールを参照してください。";
/* notification title */
"Somebody" = "誰か";
@ -2794,6 +3040,9 @@
/* No comment provided by engineer. */
"The created archive is available via app Settings / Database / Old database archive." = "作成されたアーカイブは、アプリの設定/データベース/過去のデータベースアーカイブから利用できます。";
/* No comment provided by engineer. */
"The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "暗号化は機能しており、新しい暗号化への同意は必要ありません。接続エラーが発生する可能性があります!";
/* No comment provided by engineer. */
"The group is fully decentralized it is visible only to the members." = "グループは完全分散型で、メンバーしか内容を見れません。";
@ -2818,6 +3067,9 @@
/* No comment provided by engineer. */
"The profile is only shared with your contacts." = "プロフィールは連絡先にしか共有されません。";
/* No comment provided by engineer. */
"The second tick we missed! ✅" = "長らくお待たせしました! ✅";
/* No comment provided by engineer. */
"The sender will NOT be notified" = "送信者には通知されません";
@ -2833,6 +3085,12 @@
/* No comment provided by engineer. */
"There should be at least one visible user profile." = "少なくとも1つのユーザープロフィールが表示されている必要があります。";
/* No comment provided by engineer. */
"These settings are for your current profile **%@**." = "これらの設定は現在のプロファイル **%@** 用です。";
/* No comment provided by engineer. */
"They can be overridden in contact and group settings." = "これらは連絡先の設定が優先します。";
/* No comment provided by engineer. */
"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "ファイルとメディアが全て削除されます (※元に戻せません※)。低解像度の画像が残ります。";
@ -2908,6 +3166,9 @@
/* No comment provided by engineer. */
"Unexpected migration state" = "予期しない移行状態";
/* No comment provided by engineer. */
"Unfav." = "お気に入りを取り消す。";
/* No comment provided by engineer. */
"Unhide" = "表示にする";
@ -2986,12 +3247,18 @@
/* No comment provided by engineer. */
"Use chat" = "チャット";
/* No comment provided by engineer. */
"Use current profile" = "現在のプロファイルを使用する";
/* No comment provided by engineer. */
"Use for new connections" = "新しい接続に使う";
/* No comment provided by engineer. */
"Use iOS call interface" = "iOS通話インターフェースを使用する";
/* No comment provided by engineer. */
"Use new incognito profile" = "新しいシークレットプロファイルを使用する";
/* No comment provided by engineer. */
"Use server" = "サーバを使う";
@ -3160,6 +3427,12 @@
/* No comment provided by engineer. */
"You can create it later" = "後からでも作成できます";
/* No comment provided by engineer. */
"You can enable later via Settings" = "あとで設定から有効にできます";
/* No comment provided by engineer. */
"You can enable them later via app Privacy & Security settings." = "あとでアプリのプライバシーとセキュリティの設定から有効にすることができます。";
/* No comment provided by engineer. */
"You can hide or mute a user profile - swipe it to the right." = "ユーザープロファイルを右にスワイプすると、非表示またはミュートにすることができます。";
@ -3328,6 +3601,9 @@
/* No comment provided by engineer. */
"Your privacy" = "あなたのプライバシー";
/* No comment provided by engineer. */
"Your profile **%@** will be shared." = "あなたのプロファイル **%@** が共有されます。";
/* No comment provided by engineer. */
"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "プロフィールはデバイスに保存され、連絡先とのみ共有されます。\nSimpleX サーバーはあなたのプロファイルを参照できません。";

View File

@ -209,7 +209,7 @@
"%lldw" = "%lldw";
/* No comment provided by engineer. */
"%u messages failed to decrypt." = "%u-berichten kunnen niet worden gedecodeerd.";
"%u messages failed to decrypt." = "%u berichten kunnen niet worden ontsleuteld.";
/* No comment provided by engineer. */
"%u messages skipped." = "%u berichten zijn overgeslagen.";
@ -1867,7 +1867,7 @@
"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chat profiel.";
/* No comment provided by engineer. */
"It can happen when you or your connection used the old database backup." = "Het kan gebeuren wanneer u of uw verbinding de oude databaseback-up gebruikte.";
"It can happen when you or your connection used the old database backup." = "Het kan gebeuren wanneer u of de ander een oude databaseback-up gebruikt.";
/* No comment provided by engineer. */
"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Het kan gebeuren wanneer:\n1. De berichten zijn na 2 dagen verlopen bij de verzendende client of na 30 dagen op de server.\n2. Decodering van het bericht is mislukt, omdat u of uw contactpersoon een oude databaseback-up heeft gebruikt.\n3. De verbinding is verbroken.";
@ -3299,7 +3299,7 @@
"Use iOS call interface" = "De iOS-oproepinterface gebruiken";
/* No comment provided by engineer. */
"Use new incognito profile" = "Gebruik een nieuw incognito -profiel";
"Use new incognito profile" = "Gebruik een nieuw incognitoprofiel";
/* No comment provided by engineer. */
"Use server" = "Gebruik server";

View File

@ -115,6 +115,9 @@
/* No comment provided by engineer. */
"%@ %@" = "%@ %@";
/* No comment provided by engineer. */
"%@ and %@ connected" = "%@ i %@ połączeni";
/* copied message info, <sender> at <time> */
"%@ at %@:" = "%1$@ o %2$@:";
@ -133,6 +136,9 @@
/* notification title */
"%@ wants to connect!" = "%@ chce się połączyć!";
/* No comment provided by engineer. */
"%@, %@ and %lld other members connected" = "%@, %@ i %lld innych członków połączeni";
/* copied message info */
"%@:" = "%@:";
@ -1467,6 +1473,9 @@
/* No comment provided by engineer. */
"Even when disabled in the conversation." = "Nawet po wyłączeniu w rozmowie.";
/* No comment provided by engineer. */
"event happened" = "nowe wydarzenie";
/* No comment provided by engineer. */
"Exit without saving" = "Wyjdź bez zapisywania";
@ -2881,6 +2890,9 @@
/* No comment provided by engineer. */
"Show developer options" = "Pokaż opcje dewelopera";
/* No comment provided by engineer. */
"Show last messages" = "Pokaż ostatnie wiadomości";
/* No comment provided by engineer. */
"Show preview" = "Pokaż podgląd";

View File

@ -19,6 +19,9 @@
/* No comment provided by engineer. */
"_italic_" = "\\_斜体_";
/* No comment provided by engineer. */
"- more stable message delivery.\n- a bit better groups.\n- and more!" = "- 更稳定的传输!\n- 更好的社群!\n- 以及更多!";
/* No comment provided by engineer. */
"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- 语音消息最长5分钟。\n- 自定义限时消息。\n- 编辑消息历史。";
@ -85,6 +88,15 @@
/* No comment provided by engineer. */
"*bold*" = "\\*加粗*";
/* copied message info title, # <title> */
"# %@" = "# %@";
/* copied message info */
"## History" = "## 历史";
/* copied message info */
"## In reply to" = "## 回复";
/* No comment provided by engineer. */
"#secret#" = "#秘密#";
@ -103,6 +115,12 @@
/* No comment provided by engineer. */
"%@ %@" = "%@ %@";
/* No comment provided by engineer. */
"%@ and %@ connected" = "%@ 和%@ 以建立连接";
/* copied message info, <sender> at <time> */
"%@ at %@:" = "%2$@:";
/* notification title */
"%@ is connected!" = "%@ 已连接!";
@ -118,6 +136,9 @@
/* notification title */
"%@ wants to connect!" = "%@ 要连接!";
/* No comment provided by engineer. */
"%@, %@ and %lld other members connected" = "%@, %@ 和 %lld 个成员";
/* copied message info */
"%@:" = "%@:";
@ -232,9 +253,15 @@
/* No comment provided by engineer. */
"30 seconds" = "30秒";
/* No comment provided by engineer. */
"A few more things" = "";
/* notification title */
"A new contact" = "新联系人";
/* No comment provided by engineer. */
"A new random profile will be shared." = "创建一个随机的共享文件";
/* No comment provided by engineer. */
"A separate TCP connection will be used **for each chat profile you have in the app**." = "一个单独的 TCP 连接将被用于**您在应用程序中的每个聊天资料**。";

View File

@ -83,12 +83,15 @@ android {
// Comma separated list of languages that will be included in the apk
android.defaultConfig.resConfigs(
"en",
"ar",
"bg",
"cs",
"de",
"es",
"fi",
"fr",
"it",
"iw",
"ja",
"nl",
"pl",

View File

@ -141,7 +141,12 @@ fun processExternalIntent(intent: Intent?) {
when {
intent.type == "text/plain" -> {
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
if (text != null) {
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
if (uri != null) {
// Shared file that contains plain text, like `*.log` file
chatModel.sharedContent.value = SharedContent.File(text ?: "", uri.toURI())
} else if (text != null) {
// Shared just a text
chatModel.sharedContent.value = SharedContent.Text(text)
}
}

View File

@ -71,7 +71,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
}
Lifecycle.Event.ON_RESUME -> {
isAppOnForeground = true
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
SimplexService.showBackgroundServiceNoticeIfNeeded()
}
/**
@ -80,7 +80,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
* It can happen when app was started and a user enables battery optimization while app in background
* */
if (chatModel.chatRunning.value != false &&
chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete &&
chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete &&
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE
) {
SimplexService.start()
@ -191,7 +191,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
override fun androidChatInitializedAndStarted() {
// Prevents from showing "Enable notifications" alert when onboarding wasn't complete yet
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
SimplexService.showBackgroundServiceNoticeIfNeeded()
if (appPrefs.notificationsMode.get() == NotificationsMode.SERVICE)
withBGApi {

View File

@ -46,7 +46,7 @@ buildscript {
classpath("com.android.tools.build:gradle:${rootProject.extra["gradle.plugin.version"]}")
classpath(kotlin("gradle-plugin", version = rootProject.extra["kotlin.version"] as String))
classpath("org.jetbrains.kotlin:kotlin-serialization:1.3.2")
classpath("dev.icerock.moko:resources-generator:0.22.3")
classpath("dev.icerock.moko:resources-generator:0.23.0")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View File

@ -39,7 +39,7 @@ kotlin {
api("org.jetbrains.kotlinx:kotlinx-datetime:0.3.2")
api("com.russhwolf:multiplatform-settings:1.0.0")
api("com.charleskorn.kaml:kaml:0.43.0")
api("dev.icerock.moko:resources-compose:0.22.3")
api("dev.icerock.moko:resources-compose:0.23.0")
api("org.jetbrains.compose.ui:ui-text:${rootProject.extra["compose.version"] as String}")
implementation("org.jetbrains.compose.components:components-animatedimage:${rootProject.extra["compose.version"] as String}")
//Barcode
@ -48,7 +48,7 @@ kotlin {
// Link Previews
implementation("org.jsoup:jsoup:1.13.1")
// Resources
implementation("dev.icerock.moko:resources:0.22.3")
implementation("dev.icerock.moko:resources:0.23.0")
}
}
val commonTest by getting {
@ -62,7 +62,7 @@ kotlin {
val work_version = "2.7.1"
implementation("androidx.work:work-runtime-ktx:$work_version")
implementation("com.google.accompanist:accompanist-insets:0.23.0")
implementation("dev.icerock.moko:resources:0.22.3")
implementation("dev.icerock.moko:resources:0.23.0")
// Video support
implementation("com.google.android.exoplayer:exoplayer:2.17.1")

View File

@ -23,6 +23,8 @@ actual val agentDatabaseFileName: String = "files_agent.db"
actual val databaseExportDir: File = androidAppContext.cacheDir
actual fun desktopOpenDatabaseDir() {}
@Composable
actual fun rememberFileChooserLauncher(getContent: Boolean, rememberedValue: Any?, onResult: (URI?) -> Unit): FileChooserLauncher {
val launcher = rememberLauncherForActivityResult(

View File

@ -0,0 +1,106 @@
package chat.simplex.common.views.database
import SectionItemView
import SectionTextFooter
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.common.ui.theme.SimplexGreen
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@Composable
actual fun SavePassphraseSetting(
useKeychain: Boolean,
initialRandomDBPassphrase: Boolean,
storedKey: Boolean,
progressIndicator: Boolean,
minHeight: Dp,
onCheckedChange: (Boolean) -> Unit,
) {
SectionItemView(minHeight = minHeight) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
if (storedKey) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_vpn_key_off_filled),
stringResource(MR.strings.save_passphrase_in_keychain),
tint = if (storedKey) SimplexGreen else MaterialTheme.colors.secondary
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(MR.strings.save_passphrase_in_keychain),
Modifier.padding(end = 24.dp),
color = Color.Unspecified
)
Spacer(Modifier.fillMaxWidth().weight(1f))
DefaultSwitch(
checked = useKeychain,
onCheckedChange = onCheckedChange,
enabled = !initialRandomDBPassphrase && !progressIndicator
)
}
}
}
@Composable
actual fun DatabaseEncryptionFooter(
useKeychain: MutableState<Boolean>,
chatDbEncrypted: Boolean?,
storedKey: MutableState<Boolean>,
initialRandomDBPassphrase: MutableState<Boolean>,
) {
if (chatDbEncrypted == false) {
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
} else if (useKeychain.value) {
if (storedKey.value) {
SectionTextFooter(generalGetString(MR.strings.keychain_is_storing_securely))
if (initialRandomDBPassphrase.value) {
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
} else {
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
}
} else {
SectionTextFooter(generalGetString(MR.strings.keychain_allows_to_receive_ntfs))
}
} else {
SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time))
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
}
}
actual fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.encrypt_database_question),
text = generalGetString(MR.strings.database_will_be_encrypted_and_passphrase_stored) + "\n" + storeSecurelySaved(),
confirmText = generalGetString(MR.strings.encrypt_database),
onConfirm = onConfirm,
destructive = true,
)
}
actual fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.change_database_passphrase_question),
text = generalGetString(MR.strings.database_encryption_will_be_updated) + "\n" + storeSecurelySaved(),
confirmText = generalGetString(MR.strings.update_database),
onConfirm = onConfirm,
destructive = false,
)
}
actual fun removePassphraseAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.remove_passphrase_from_keychain),
text = generalGetString(MR.strings.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(),
confirmText = generalGetString(MR.strings.remove_passphrase),
onConfirm = onConfirm,
destructive = true,
)
}

View File

@ -32,8 +32,7 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.*
data class SettingsViewState(
val userPickerState: MutableStateFlow<AnimatedViewState>,
@ -64,7 +63,7 @@ fun MainScreen() {
if (
!chatModel.controller.appPrefs.laNoticeShown.get()
&& showAdvertiseLAAlert
&& chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete
&& chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete
&& chatModel.chats.isNotEmpty()
&& chatModel.activeCallInvitation.value == null
) {
@ -102,7 +101,10 @@ fun MainScreen() {
}
Box {
val onboarding = chatModel.onboardingStage.value
var onboarding by remember { mutableStateOf(chatModel.controller.appPrefs.onboardingStage.get()) }
LaunchedEffect(Unit) {
snapshotFlow { chatModel.controller.appPrefs.onboardingStage.state.value }.distinctUntilChanged().collect { onboarding = it }
}
val userCreated = chatModel.userCreated.value
var showInitializationView by remember { mutableStateOf(false) }
when {
@ -112,7 +114,7 @@ fun MainScreen() {
DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs)
}
}
onboarding == null || userCreated == null -> SplashView()
remember { chatModel.chatDbEncrypted }.value == null || userCreated == null -> SplashView()
onboarding == OnboardingStage.OnboardingComplete && userCreated -> {
Box {
showAdvertiseLAAlert = true
@ -134,6 +136,7 @@ fun MainScreen() {
}
}
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {}
onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel)
onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel)
onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)
}

View File

@ -38,7 +38,6 @@ import kotlin.time.*
@Stable
object ChatModel {
val controller: ChatController = ChatController
val onboardingStage = mutableStateOf<OnboardingStage?>(null)
val setDeliveryReceipts = mutableStateOf(false)
val currentUser = mutableStateOf<User?>(null)
val users = mutableStateListOf<UserInfo>()

View File

@ -50,17 +50,16 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
if (user == null) {
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
chatModel.currentUser.value = null
chatModel.users.clear()
} else {
val savedOnboardingStage = appPreferences.onboardingStage.get()
chatModel.onboardingStage.value = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {
appPreferences.onboardingStage.set(if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {
OnboardingStage.Step3_CreateSimpleXAddress
} else {
savedOnboardingStage
}
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) {
})
if (appPreferences.onboardingStage.get() == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) {
chatModel.setDeliveryReceipts.value = true
}
chatController.startChat(user)

View File

@ -23,6 +23,8 @@ expect val agentDatabaseFileName: String
* */
expect val databaseExportDir: File
expect fun desktopOpenDatabaseDir()
fun copyFileToFile(from: File, to: URI, finally: () -> Unit) {
try {
to.outputStream().use { stream ->

View File

@ -100,7 +100,7 @@ abstract class NtfManager {
if (chatModel.chatRunning.value == null) {
val step = 50L
for (i in 0..(timeout / step)) {
if (chatModel.chatRunning.value == true || chatModel.onboardingStage.value == OnboardingStage.Step1_SimpleXInfo) {
if (chatModel.chatRunning.value == true || chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.Step1_SimpleXInfo) {
break
}
delay(step)

View File

@ -24,6 +24,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.Profile
import chat.simplex.common.platform.appPlatform
import chat.simplex.common.platform.navigationBarsWithImePadding
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
@ -88,14 +89,20 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
icon = painterResource(MR.images.ic_arrow_back_ios_new),
textDecoration = TextDecoration.None,
fontWeight = FontWeight.Medium
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo }
) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) }
}
Spacer(Modifier.fillMaxWidth().weight(1f))
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
val createModifier: Modifier
val createColor: Color
if (enabled) {
createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value, close) }.padding(8.dp)
createModifier = Modifier.clickable {
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
createProfileInProfiles(chatModel, displayName.value, fullName.value, close)
} else {
createProfileOnboarding(chatModel, displayName.value, fullName.value, close)
}
}.padding(8.dp)
createColor = MaterialTheme.colors.primary
} else {
createModifier = Modifier.padding(8.dp)
@ -116,7 +123,7 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
}
}
fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) {
fun createProfileInProfiles(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) {
withApi {
val user = chatModel.controller.apiCreateActiveUser(
Profile(displayName, fullName, null)
@ -125,16 +132,32 @@ fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, c
if (chatModel.users.isEmpty()) {
chatModel.controller.startChat(user)
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress)
chatModel.onboardingStage.value = OnboardingStage.Step3_CreateSimpleXAddress
} else {
val users = chatModel.controller.listUsers()
chatModel.users.clear()
chatModel.users.addAll(users)
chatModel.controller.getUserChatData()
close()
}
}
}
fun createProfileOnboarding(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) {
withApi {
chatModel.controller.apiCreateActiveUser(
Profile(displayName, fullName, null)
) ?: return@withApi
val onboardingStage = chatModel.controller.appPrefs.onboardingStage
if (chatModel.users.isEmpty()) {
onboardingStage.set(if (appPlatform.isDesktop && chatModel.controller.appPrefs.initialRandomDBPassphrase.get()) {
OnboardingStage.Step2_5_SetupDatabasePassphrase
} else {
OnboardingStage.Step3_CreateSimpleXAddress
})
} else {
// the next two lines are only needed for failure case when because of the database error the app gets stuck on on-boarding screen,
// this will get it unstuck.
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)
chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete
onboardingStage.set(OnboardingStage.OnboardingComplete)
close()
}
}

View File

@ -30,6 +30,7 @@ import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.platform.appPlatform
import chat.simplex.res.MR
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.datetime.Clock
@ -61,46 +62,8 @@ fun DatabaseEncryptionView(m: ChatModel) {
initialRandomDBPassphrase,
progressIndicator,
onConfirmEncrypt = {
progressIndicator.value = true
withApi {
try {
prefs.encryptionStartedAt.set(Clock.System.now())
val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value)
prefs.encryptionStartedAt.set(null)
val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError
when {
sqliteError is SQLiteError.ErrorNotADatabase -> {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.wrong_passphrase_title),
generalGetString(MR.strings.enter_correct_current_passphrase)
)
}
}
error != null -> {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database),
"failed to set storage encryption: ${error.responseType} ${error.details}"
)
}
}
else -> {
prefs.initialRandomDBPassphrase.set(false)
initialRandomDBPassphrase.value = false
if (useKeychain.value) {
DatabaseUtils.ksDatabasePassword.set(newKey.value)
}
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
}
}
}
} catch (e: Exception) {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database), e.stackTraceToString())
}
}
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator)
}
}
)
@ -143,17 +106,11 @@ fun DatabaseEncryptionLayout(
if (checked) {
setUseKeychain(true, useKeychain, prefs)
} else if (storedKey.value) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.remove_passphrase_from_keychain),
text = generalGetString(MR.strings.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(),
confirmText = generalGetString(MR.strings.remove_passphrase),
onConfirm = {
DatabaseUtils.ksDatabasePassword.remove()
setUseKeychain(false, useKeychain, prefs)
storedKey.value = false
},
destructive = true,
)
removePassphraseAlert {
DatabaseUtils.ksDatabasePassword.remove()
setUseKeychain(false, useKeychain, prefs)
storedKey.value = false
}
} else {
setUseKeychain(false, useKeychain, prefs)
}
@ -217,37 +174,13 @@ fun DatabaseEncryptionLayout(
}
Column {
if (chatDbEncrypted == false) {
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
} else if (useKeychain.value) {
if (storedKey.value) {
SectionTextFooter(generalGetString(MR.strings.keychain_is_storing_securely))
if (initialRandomDBPassphrase.value) {
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
} else {
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
}
} else {
SectionTextFooter(generalGetString(MR.strings.keychain_allows_to_receive_ntfs))
}
} else {
SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time))
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
}
DatabaseEncryptionFooter(useKeychain, chatDbEncrypted, storedKey, initialRandomDBPassphrase)
}
SectionBottomSpacer()
}
}
fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.encrypt_database_question),
text = generalGetString(MR.strings.database_will_be_encrypted_and_passphrase_stored) + "\n" + storeSecurelySaved(),
confirmText = generalGetString(MR.strings.encrypt_database),
onConfirm = onConfirm,
destructive = true,
)
}
expect fun encryptDatabaseSavedAlert(onConfirm: () -> Unit)
fun encryptDatabaseAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
@ -259,15 +192,7 @@ fun encryptDatabaseAlert(onConfirm: () -> Unit) {
)
}
fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.change_database_passphrase_question),
text = generalGetString(MR.strings.database_encryption_will_be_updated) + "\n" + storeSecurelySaved(),
confirmText = generalGetString(MR.strings.update_database),
onConfirm = onConfirm,
destructive = false,
)
}
expect fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit)
fun changeDatabaseKeyAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
@ -279,37 +204,25 @@ fun changeDatabaseKeyAlert(onConfirm: () -> Unit) {
)
}
expect fun removePassphraseAlert(onConfirm: () -> Unit)
@Composable
fun SavePassphraseSetting(
expect fun SavePassphraseSetting(
useKeychain: Boolean,
initialRandomDBPassphrase: Boolean,
storedKey: Boolean,
progressIndicator: Boolean,
minHeight: Dp = TextFieldDefaults.MinHeight,
onCheckedChange: (Boolean) -> Unit,
) {
SectionItemView(minHeight = minHeight) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
if (storedKey) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_vpn_key_off_filled),
stringResource(MR.strings.save_passphrase_in_keychain),
tint = if (storedKey) SimplexGreen else MaterialTheme.colors.secondary
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(MR.strings.save_passphrase_in_keychain),
Modifier.padding(end = 24.dp),
color = Color.Unspecified
)
Spacer(Modifier.fillMaxWidth().weight(1f))
DefaultSwitch(
checked = useKeychain,
onCheckedChange = onCheckedChange,
enabled = !initialRandomDBPassphrase && !progressIndicator
)
}
}
}
)
@Composable
expect fun DatabaseEncryptionFooter(
useKeychain: MutableState<Boolean>,
chatDbEncrypted: Boolean?,
storedKey: MutableState<Boolean>,
initialRandomDBPassphrase: MutableState<Boolean>,
)
fun resetFormAfterEncryption(
m: ChatModel,
@ -443,6 +356,62 @@ fun PassphraseField(
}
}
suspend fun encryptDatabase(
currentKey: MutableState<String>,
newKey: MutableState<String>,
confirmNewKey: MutableState<String>,
initialRandomDBPassphrase: MutableState<Boolean>,
useKeychain: MutableState<Boolean>,
storedKey: MutableState<Boolean>,
progressIndicator: MutableState<Boolean>
): Boolean {
val m = ChatModel
val prefs = ChatController.appPrefs
progressIndicator.value = true
return try {
prefs.encryptionStartedAt.set(Clock.System.now())
val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value)
prefs.encryptionStartedAt.set(null)
val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError
when {
sqliteError is SQLiteError.ErrorNotADatabase -> {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.wrong_passphrase_title),
generalGetString(MR.strings.enter_correct_current_passphrase)
)
}
false
}
error != null -> {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database),
"failed to set storage encryption: ${error.responseType} ${error.details}"
)
}
false
}
else -> {
prefs.initialRandomDBPassphrase.set(false)
initialRandomDBPassphrase.value = false
if (useKeychain.value) {
DatabaseUtils.ksDatabasePassword.set(newKey.value)
}
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
}
true
}
}
} catch (e: Exception) {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database), e.stackTraceToString())
}
false
}
}
// based on https://generatepasswords.org/how-to-calculate-entropy/
private fun passphraseEntropy(s: String): Double {
var hasDigits = false

View File

@ -12,6 +12,9 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.AppPreferences
@ -252,6 +255,11 @@ private fun mtrErrorDescription(err: MTRError): String =
@Composable
private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onClick: (() -> Unit)? = null) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
delay(100L)
focusRequester.requestFocus()
}
PassphraseField(
text,
generalGetString(MR.strings.enter_passphrase),
@ -259,7 +267,15 @@ private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onCli
keyboardActions = KeyboardActions(onDone = if (enabled) {
{ onClick?.invoke() }
} else null
)
),
modifier = Modifier.focusRequester(focusRequester).onPreviewKeyEvent {
if (onClick != null && it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
onClick()
true
} else {
false
}
}
)
}

View File

@ -39,7 +39,6 @@ fun DatabaseView(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)
) {
val progressIndicator = remember { mutableStateOf(false) }
val runChat = remember { m.chatRunning }
val prefs = m.controller.appPrefs
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
val chatArchiveName = remember { mutableStateOf(prefs.chatArchiveName.get()) }
@ -60,20 +59,19 @@ fun DatabaseView(
importArchiveAlert(m, to, appFilesCountAndSize, progressIndicator)
}
}
LaunchedEffect(m.chatRunning) {
runChat.value = m.chatRunning.value ?: true
}
val chatItemTTL = remember { mutableStateOf(m.chatItemTTL.value) }
Box(
Modifier.fillMaxSize(),
) {
DatabaseLayout(
progressIndicator.value,
runChat.value != false,
remember { m.chatRunning }.value != false,
m.chatDbChanged.value,
useKeychain.value,
m.chatDbEncrypted.value,
m.controller.appPrefs.storeDBPassphrase.state.value,
m.controller.appPrefs.initialRandomDBPassphrase,
m.controller.appPrefs.developerTools.state.value,
importArchiveLauncher,
chatArchiveName,
chatArchiveTime,
@ -82,8 +80,8 @@ fun DatabaseView(
chatItemTTL,
m.currentUser.value,
m.users,
startChat = { startChat(m, runChat, chatLastStart, m.chatDbChanged) },
stopChatAlert = { stopChatAlert(m, runChat) },
startChat = { startChat(m, chatLastStart, m.chatDbChanged) },
stopChatAlert = { stopChatAlert(m) },
exportArchive = { exportArchive(m, progressIndicator, chatArchiveName, chatArchiveTime, chatArchiveFile, saveArchiveLauncher) },
deleteChatAlert = { deleteChatAlert(m, progressIndicator) },
deleteAppFilesAndMedia = { deleteFilesAndMediaAlert(appFilesCountAndSize) },
@ -122,7 +120,9 @@ fun DatabaseLayout(
chatDbChanged: Boolean,
useKeyChain: Boolean,
chatDbEncrypted: Boolean?,
passphraseSaved: Boolean,
initialRandomDBPassphrase: SharedPreference<Boolean>,
developerTools: Boolean,
importArchiveLauncher: FileChooserLauncher,
chatArchiveName: MutableState<String?>,
chatArchiveTime: MutableState<Instant?>,
@ -182,9 +182,17 @@ fun DatabaseLayout(
else painterResource(MR.images.ic_lock),
stringResource(MR.strings.database_passphrase),
click = showSettingsModal() { DatabaseEncryptionView(it) },
iconColor = if (unencrypted) WarningOrange else MaterialTheme.colors.secondary,
iconColor = if (unencrypted || (appPlatform.isDesktop && passphraseSaved)) WarningOrange else MaterialTheme.colors.secondary,
disabled = operationsDisabled
)
if (appPlatform.isDesktop && developerTools) {
SettingsActionItem(
painterResource(MR.images.ic_folder_open),
stringResource(MR.strings.open_database_folder),
::desktopOpenDatabaseDir,
disabled = operationsDisabled
)
}
SettingsActionItem(
painterResource(MR.images.ic_ios_share),
stringResource(MR.strings.export_database),
@ -327,7 +335,7 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String {
return stringResource(if (chatArchiveTime < chatLastStart) MR.strings.old_database_archive else MR.strings.new_database_archive)
}
private fun startChat(m: ChatModel, runChat: MutableState<Boolean?>, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
private fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
withApi {
try {
if (chatDbChanged.value) {
@ -344,7 +352,6 @@ private fun startChat(m: ChatModel, runChat: MutableState<Boolean?>, chatLastSta
return@withApi
} else {
m.controller.apiStartChat()
runChat.value = true
m.chatRunning.value = true
}
val ts = Clock.System.now()
@ -352,19 +359,19 @@ private fun startChat(m: ChatModel, runChat: MutableState<Boolean?>, chatLastSta
chatLastStart.value = ts
platform.androidChatStartedAfterBeingOff()
} catch (e: Error) {
runChat.value = false
m.chatRunning.value = false
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_starting_chat), e.toString())
}
}
}
private fun stopChatAlert(m: ChatModel, runChat: MutableState<Boolean?>) {
private fun stopChatAlert(m: ChatModel) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.stop_chat_question),
text = generalGetString(MR.strings.stop_chat_to_export_import_or_delete_chat_database),
confirmText = generalGetString(MR.strings.stop_chat_confirmation),
onConfirm = { authStopChat(m, runChat) },
onDismiss = { runChat.value = true }
onConfirm = { authStopChat(m) },
onDismiss = { m.chatRunning.value = true }
)
}
@ -375,7 +382,7 @@ private fun exportProhibitedAlert() {
)
}
private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean?>) {
private fun authStopChat(m: ChatModel) {
if (m.controller.appPrefs.performLA.get()) {
authenticate(
generalGetString(MR.strings.auth_stop_chat),
@ -383,30 +390,29 @@ private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean?>) {
completed = { laResult ->
when (laResult) {
LAResult.Success, is LAResult.Unavailable -> {
stopChat(m, runChat)
stopChat(m)
}
is LAResult.Error -> {
runChat.value = true
m.chatRunning.value = true
}
is LAResult.Failed -> {
runChat.value = true
m.chatRunning.value = true
}
}
}
)
} else {
stopChat(m, runChat)
stopChat(m)
}
}
private fun stopChat(m: ChatModel, runChat: MutableState<Boolean?>) {
private fun stopChat(m: ChatModel) {
withApi {
try {
runChat.value = false
stopChatAsync(m)
platform.androidChatStopped()
} catch (e: Error) {
runChat.value = true
m.chatRunning.value = true
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_stopping_chat), e.toString())
}
}
@ -657,7 +663,9 @@ fun PreviewDatabaseLayout() {
chatDbChanged = false,
useKeyChain = false,
chatDbEncrypted = false,
passphraseSaved = false,
initialRandomDBPassphrase = SharedPreference({ true }, {}),
developerTools = true,
importArchiveLauncher = rememberFileChooserLauncher(true) {},
chatArchiveName = remember { mutableStateOf("dummy_archive") },
chatArchiveTime = remember { mutableStateOf(Clock.System.now()) },

View File

@ -101,6 +101,10 @@ class AlertManager {
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF),
horizontalArrangement = Arrangement.SpaceBetween
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
TextButton(onClick = {
onDismiss?.invoke()
hideAlert()
@ -108,7 +112,7 @@ class AlertManager {
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
}, Modifier.focusRequester(focusRequester)) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))

View File

@ -54,6 +54,12 @@ object DatabaseUtils {
} else {
dbKey = ksDatabasePassword.get() ?: ""
}
} else if (appPlatform.isDesktop && !hasDatabase(dataDir.absolutePath)) {
// In case of database was deleted by hand
dbKey = randomDatabasePassword()
ksDatabasePassword.set(dbKey)
appPreferences.initialRandomDBPassphrase.set(true)
appPreferences.storeDBPassphrase.set(true)
}
return dbKey
}

View File

@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
@ -66,11 +67,13 @@ fun SimpleButton(
fun SimpleButtonIconEnded(
text: String,
icon: Painter,
style: TextStyle = MaterialTheme.typography.caption,
color: Color = MaterialTheme.colors.primary,
disabled: Boolean = false,
click: () -> Unit
) {
SimpleButtonFrame(click) {
Text(text, style = MaterialTheme.typography.caption, color = color)
SimpleButtonFrame(click, disabled = disabled) {
Text(text, style = style, color = color)
Icon(
icon, text, tint = color,
modifier = Modifier.padding(start = 8.dp)

View File

@ -66,7 +66,6 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (
val createdUser = m.controller.apiCreateActiveUser(profile, pastTimestamp = true)
m.currentUser.value = createdUser
m.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)
m.onboardingStage.value = OnboardingStage.OnboardingComplete
if (createdUser != null) {
m.controller.startChat(createdUser)
}

View File

@ -14,8 +14,7 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.UserContactLinkRec
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
@ -29,6 +28,10 @@ fun CreateSimpleXAddress(m: ChatModel) {
val clipboard = LocalClipboardManager.current
val uriHandler = LocalUriHandler.current
LaunchedEffect(Unit) {
prepareChatBeforeAddressCreation()
}
CreateSimpleXAddressLayout(
userAddress.value,
share = { address: String -> clipboard.shareText(address) },
@ -63,7 +66,6 @@ fun CreateSimpleXAddress(m: ChatModel) {
OnboardingStage.OnboardingComplete
}
m.controller.appPrefs.onboardingStage.set(next)
m.onboardingStage.value = next
},
)
@ -172,3 +174,19 @@ private fun ProgressIndicator() {
)
}
}
private fun prepareChatBeforeAddressCreation() {
if (chatModel.users.isNotEmpty()) return
withApi {
val user = chatModel.controller.apiGetActiveUser() ?: return@withApi
chatModel.currentUser.value = user
if (chatModel.users.isEmpty()) {
chatModel.controller.startChat(user)
} else {
val users = chatModel.controller.listUsers()
chatModel.users.clear()
chatModel.users.addAll(users)
chatModel.controller.getUserChatData()
}
}
}

View File

@ -13,8 +13,7 @@ import androidx.compose.ui.text.*
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.ChatController
import chat.simplex.common.model.User
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.MarkdownText
import chat.simplex.common.views.helpers.*
@ -22,7 +21,7 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
@Composable
fun HowItWorks(user: User?, onboardingStage: MutableState<OnboardingStage?>? = null) {
fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>? = null) {
Column(Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),

View File

@ -14,6 +14,7 @@ import kotlinx.coroutines.launch
enum class OnboardingStage {
Step1_SimpleXInfo,
Step2_CreateProfile,
Step2_5_SetupDatabasePassphrase,
Step3_CreateSimpleXAddress,
Step4_SetNotificationsMode,
OnboardingComplete

View File

@ -41,7 +41,7 @@ fun SetNotificationsMode(m: ChatModel) {
}
Spacer(Modifier.fillMaxHeight().weight(1f))
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), contentAlignment = Alignment.Center) {
OnboardingActionButton(MR.strings.use_chat, OnboardingStage.OnboardingComplete, m.onboardingStage, false) {
OnboardingActionButton(MR.strings.use_chat, OnboardingStage.OnboardingComplete, false) {
changeNotificationsMode(currentMode.value, m)
}
}

View File

@ -0,0 +1,233 @@
package chat.simplex.common.views.onboarding
import SectionBottomSpacer
import SectionItemView
import SectionItemViewSpaceBetween
import SectionTextFooter
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.*
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.database.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.delay
@Composable
fun SetupDatabasePassphrase(m: ChatModel) {
val progressIndicator = remember { mutableStateOf(false) }
val prefs = m.controller.appPrefs
val saveInPreferences = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) }
// Do not do rememberSaveable on current key to prevent saving it on disk in clear text
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") }
val newKey = rememberSaveable { mutableStateOf("") }
val confirmNewKey = rememberSaveable { mutableStateOf("") }
fun nextStep() {
m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress)
}
SetupDatabasePassphraseLayout(
currentKey,
newKey,
confirmNewKey,
progressIndicator,
onConfirmEncrypt = {
withApi {
if (m.chatRunning.value == true) {
// Stop chat if it's started before doing anything
stopChatAsync(m)
}
prefs.storeDBPassphrase.set(false)
val newKeyValue = newKey.value
val success = encryptDatabase(currentKey, newKey, confirmNewKey, mutableStateOf(true), saveInPreferences, mutableStateOf(true), progressIndicator)
if (success) {
startChat(newKeyValue)
nextStep()
} else {
// Rollback in case of it is finished with error in order to allow to repeat the process again
prefs.storeDBPassphrase.set(true)
}
}
},
nextStep = ::nextStep,
)
if (progressIndicator.value) {
ProgressIndicator()
}
DisposableEffect(Unit) {
onDispose {
if (m.chatRunning.value != true) {
withBGApi {
val user = chatController.apiGetActiveUser()
if (user != null) {
m.controller.startChat(user)
}
}
}
}
}
}
@Composable
private fun SetupDatabasePassphraseLayout(
currentKey: MutableState<String>,
newKey: MutableState<String>,
confirmNewKey: MutableState<String>,
progressIndicator: MutableState<Boolean>,
onConfirmEncrypt: () -> Unit,
nextStep: () -> Unit,
) {
Column(
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(top = DEFAULT_PADDING),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AppBarTitle(stringResource(MR.strings.setup_database_passphrase))
Spacer(Modifier.weight(1f))
Column(Modifier.width(600.dp)) {
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
LaunchedEffect(Unit) {
delay(100L)
focusRequester.requestFocus()
}
PassphraseField(
newKey,
generalGetString(MR.strings.new_passphrase),
modifier = Modifier
.padding(horizontal = DEFAULT_PADDING)
.focusRequester(focusRequester)
.onPreviewKeyEvent {
if (it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
focusManager.moveFocus(FocusDirection.Down)
true
} else {
false
}
},
showStrength = true,
isValid = ::validKey,
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
)
val onClickUpdate = {
// Don't do things concurrently. Shouldn't be here concurrently, just in case
if (!progressIndicator.value) {
encryptDatabaseAlert(onConfirmEncrypt)
}
}
val disabled = currentKey.value == newKey.value ||
newKey.value != confirmNewKey.value ||
newKey.value.isEmpty() ||
!validKey(currentKey.value) ||
!validKey(newKey.value) ||
progressIndicator.value
PassphraseField(
confirmNewKey,
generalGetString(MR.strings.confirm_new_passphrase),
modifier = Modifier
.padding(horizontal = DEFAULT_PADDING)
.onPreviewKeyEvent {
if (!disabled && it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
onClickUpdate()
true
} else {
false
}
},
isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value },
keyboardActions = KeyboardActions(onDone = {
if (!disabled) onClickUpdate()
defaultKeyboardAction(ImeAction.Done)
}),
)
Box(Modifier.align(Alignment.CenterHorizontally).padding(vertical = DEFAULT_PADDING)) {
SetPassphraseButton(disabled, onClickUpdate)
}
Column {
SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time))
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
}
}
Spacer(Modifier.weight(1f))
SkipButton(progressIndicator.value, nextStep)
SectionBottomSpacer()
}
}
@Composable
private fun SetPassphraseButton(disabled: Boolean, onClick: () -> Unit) {
SimpleButtonIconEnded(
stringResource(MR.strings.set_database_passphrase),
painterResource(MR.images.ic_check),
style = MaterialTheme.typography.h2,
color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
disabled = disabled,
click = onClick
)
}
@Composable
private fun SkipButton(disabled: Boolean, onClick: () -> Unit) {
SimpleButtonIconEnded(stringResource(MR.strings.use_random_passphrase), painterResource(MR.images.ic_chevron_right), color =
if (disabled) MaterialTheme.colors.secondary else WarningOrange, disabled = disabled, click = onClick)
Text(
stringResource(MR.strings.you_can_change_it_later),
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING * 3),
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.secondary,
textAlign = TextAlign.Center,
)
}
@Composable
private fun ProgressIndicator() {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(30.dp),
color = MaterialTheme.colors.secondary,
strokeWidth = 3.dp
)
}
}
private suspend fun startChat(key: String?) {
val m = ChatModel
initChatController(key)
m.chatDbChanged.value = false
m.chatRunning.value = true
}

View File

@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
@ -25,7 +24,7 @@ import dev.icerock.moko.resources.StringResource
fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) {
SimpleXInfoLayout(
user = chatModel.currentUser.value,
onboardingStage = if (onboarding) chatModel.onboardingStage else null,
onboardingStage = if (onboarding) chatModel.controller.appPrefs.onboardingStage else null,
showModal = { modalView -> { if (onboarding) ModalManager.fullscreen.showModal { modalView(chatModel) } else ModalManager.start.showModal { modalView(chatModel) } } },
)
}
@ -33,7 +32,7 @@ fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) {
@Composable
fun SimpleXInfoLayout(
user: User?,
onboardingStage: MutableState<OnboardingStage?>?,
onboardingStage: SharedPreference<OnboardingStage>?,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
) {
Column(
@ -100,11 +99,11 @@ private fun InfoRow(icon: Painter, titleId: StringResource, textId: StringResour
}
@Composable
fun OnboardingActionButton(user: User?, onboardingStage: MutableState<OnboardingStage?>, onclick: (() -> Unit)? = null) {
fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference<OnboardingStage>, onclick: (() -> Unit)? = null) {
if (user == null) {
OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, true, onclick)
OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, onclick)
} else {
OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, true, onclick)
OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, true, onclick)
}
}
@ -112,7 +111,6 @@ fun OnboardingActionButton(user: User?, onboardingStage: MutableState<Onboarding
fun OnboardingActionButton(
labelId: StringResource,
onboarding: OnboardingStage?,
onboardingStage: MutableState<OnboardingStage?>,
border: Boolean,
onclick: (() -> Unit)?
) {
@ -129,7 +127,6 @@ fun OnboardingActionButton(
SimpleButtonFrame(click = {
onclick?.invoke()
onboardingStage.value = onboarding
if (onboarding != null) {
ChatController.appPrefs.onboardingStage.set(onboarding)
}

View File

@ -201,12 +201,15 @@ object AppearanceScope {
val supportedLanguages = mapOf(
"system" to generalGetString(MR.strings.language_system),
"en" to "English",
"ar" to "العربية",
"bg" to "Български",
"cs" to "Čeština",
"de" to "Deutsch",
"es" to "Español",
"fi" to "Suomi",
"fr" to "Français",
"it" to "Italiano",
"iw" to "עִברִית",
"ja" to "日本語",
"nl" to "Nederlands",
"pl" to "Polski",

View File

@ -43,6 +43,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerSt
profile = user.profile,
stopped,
chatModel.chatDbEncrypted.value == true,
remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value,
remember { chatModel.controller.appPrefs.notificationsMode.state },
user.displayName,
setPerformLA = setPerformLA,
@ -115,6 +116,7 @@ fun SettingsLayout(
profile: LocalProfile,
stopped: Boolean,
encrypted: Boolean,
passphraseSaved: Boolean,
notificationsMode: State<NotificationsMode>,
userDisplayName: String,
setPerformLA: (Boolean) -> Unit,
@ -162,7 +164,7 @@ fun SettingsLayout(
SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true)
SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true)
SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it, showSettingsModal) }, extraPadding = true)
DatabaseItem(encrypted, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
}
SectionDividerSpaced()
@ -207,7 +209,7 @@ expect fun SettingsSectionApp(
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
)
@Composable private fun DatabaseItem(encrypted: Boolean, openDatabaseView: () -> Unit, stopped: Boolean) {
@Composable private fun DatabaseItem(encrypted: Boolean, saved: Boolean, openDatabaseView: () -> Unit, stopped: Boolean) {
SectionItemViewWithIcon(openDatabaseView) {
Row(
Modifier.fillMaxWidth(),
@ -217,7 +219,7 @@ expect fun SettingsSectionApp(
Icon(
painterResource(MR.images.ic_database),
contentDescription = stringResource(MR.strings.database_passphrase_and_export),
tint = if (encrypted) MaterialTheme.colors.secondary else WarningOrange,
tint = if (encrypted && (appPlatform.isAndroid || !saved)) MaterialTheme.colors.secondary else WarningOrange,
)
TextIconSpaced(true)
Text(stringResource(MR.strings.database_passphrase_and_export))
@ -393,9 +395,13 @@ fun SettingsActionItemWithContent(icon: Painter?, text: String? = null, click: (
val padding = with(LocalDensity.current) { 6.sp.toDp() }
Text(text, Modifier.weight(1f).padding(vertical = padding), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING))
}
Row(Modifier.widthIn(max = (windowWidth() - DEFAULT_PADDING * 2) / 2)) {
content()
Row(Modifier.widthIn(max = (windowWidth() - DEFAULT_PADDING * 2) / 2)) {
content()
}
} else {
Row {
content()
}
}
}
}
@ -469,6 +475,7 @@ fun PreviewSettingsLayout() {
profile = LocalProfile.sampleData,
stopped = false,
encrypted = false,
passphraseSaved = false,
notificationsMode = remember { mutableStateOf(NotificationsMode.OFF) },
userDisplayName = "Alice",
setPerformLA = { _ -> },

View File

@ -24,7 +24,7 @@
<string name="member_role_will_be_changed_with_notification">سيتم تغيير الدور إلى \"%s\". سيتم إبلاغ كل فرد في المجموعة.</string>
<string name="member_role_will_be_changed_with_invitation">سيتم تغيير الدور إلى \"%s\". سيتلقى العضو دعوة جديدة.</string>
<string name="smp_servers_per_user">خوادم الاتصالات الجديدة لملف تعريف الدردشة الحالي الخاص بك</string>
<string name="switch_receiving_address_desc">هذه الميزة تجريبية! ستعمل فقط إذا كان لدى العميل الآخر الإصدار 4.2 مثبتًا. يجب أن ترى الرسالة في المحادثة بمجرد اكتمال تغيير العنوان - يرجى التحقق من أنه لا يزال بإمكانك تلقي الرسائل من جهة الاتصال هذه (أو عضو المجموعة).</string>
<string name="switch_receiving_address_desc">سيتم تغيير عنوان الاستلام إلى خادم مختلف. سيتم إكمال تغيير العنوان بعد اتصال المرسل بالإنترنت.</string>
<string name="this_link_is_not_a_valid_connection_link">هذا الارتباط ليس ارتباط اتصال صالح!</string>
<string name="allow_verb">يسمح</string>
<string name="smp_servers_preset_add">أضف خوادم محددة مسبقًا</string>

View File

@ -769,6 +769,11 @@
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Good for battery</b>. Background service checks messages every 10 minutes. You may miss calls or urgent messages.]]></string>
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Uses more battery</b>! Background service always runs notifications are shown as soon as messages are available.]]></string>
<!-- SetupDatabasePassphrase.kt -->
<string name="setup_database_passphrase">Setup database passphrase</string>
<string name="you_can_change_it_later">Random passphrase is stored in settings as plaintext.\nYou can change it later.</string>
<string name="use_random_passphrase">Use random passphrase</string>
<!-- MakeConnection -->
<string name="paste_the_link_you_received">Paste received link</string>
@ -939,6 +944,7 @@
<string name="import_database">Import database</string>
<string name="new_database_archive">New database archive</string>
<string name="old_database_archive">Old database archive</string>
<string name="open_database_folder">Open database folder</string>
<string name="delete_database">Delete database</string>
<string name="error_starting_chat">Error starting chat</string>
<string name="stop_chat_question">Stop chat?</string>
@ -984,9 +990,11 @@
<!-- DatabaseEncryptionView.kt -->
<string name="save_passphrase_in_keychain">Save passphrase in Keystore</string>
<string name="save_passphrase_in_settings">Save passphrase in settings</string>
<string name="database_encrypted">Database encrypted!</string>
<string name="error_encrypting_database">Error encrypting database</string>
<string name="remove_passphrase_from_keychain">Remove passphrase from Keystore?</string>
<string name="remove_passphrase_from_settings">Remove passphrase from settings?</string>
<string name="notifications_will_be_hidden">Notifications will be delivered only until the app stops!</string>
<string name="remove_passphrase">Remove</string>
<string name="encrypt_database">Encrypt</string>
@ -995,18 +1003,23 @@
<string name="new_passphrase">New passphrase…</string>
<string name="confirm_new_passphrase">Confirm new passphrase…</string>
<string name="update_database_passphrase">Update database passphrase</string>
<string name="set_database_passphrase">Set database passphrase</string>
<string name="enter_correct_current_passphrase">Please enter correct current passphrase.</string>
<string name="database_is_not_encrypted">Your chat database is not encrypted - set passphrase to protect it.</string>
<string name="keychain_is_storing_securely">Android Keystore is used to securely store passphrase - it allows notification service to work.</string>
<string name="settings_is_storing_in_clear_text">The passphrase is stored in settings as plaintext.</string>
<string name="encrypted_with_random_passphrase">Database is encrypted using a random passphrase, you can change it.</string>
<string name="impossible_to_recover_passphrase"><![CDATA[<b>Please note</b>: you will NOT be able to recover or change passphrase if you lose it.]]></string>
<string name="keychain_allows_to_receive_ntfs">Android Keystore will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving notifications.</string>
<string name="passphrase_will_be_saved_in_settings">The passphrase will be stored in settings as plaintext after you change it or restart the app.</string>
<string name="you_have_to_enter_passphrase_every_time">You have to enter passphrase every time the app starts - it is not stored on the device.</string>
<string name="encrypt_database_question">Encrypt database?</string>
<string name="change_database_passphrase_question">Change database passphrase?</string>
<string name="database_will_be_encrypted">Database will be encrypted.</string>
<string name="database_will_be_encrypted_and_passphrase_stored">Database will be encrypted and the passphrase stored in the Keystore.</string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Database will be encrypted and the passphrase stored in settings.</string>
<string name="database_encryption_will_be_updated">Database encryption passphrase will be updated and stored in the Keystore.</string>
<string name="database_encryption_will_be_updated_in_settings">Database encryption passphrase will be updated and stored in settings.</string>
<string name="database_passphrase_will_be_updated">Database encryption passphrase will be updated.</string>
<string name="store_passphrase_securely">Please store passphrase securely, you will NOT be able to change it if you lose it.</string>
<string name="store_passphrase_securely_without_recover">Please store passphrase securely, you will NOT be able to access chat if you lose it.</string>

View File

@ -1337,7 +1337,7 @@
<string name="v5_2_message_delivery_receipts_descr">Druhé zaškrtnutí jsme přehlédli! ✅</string>
<string name="switch_receiving_address_desc">Přijímací adresa bude změněna na jiný server. Změna adresy bude dokončena po připojení odesílatele.</string>
<string name="choose_file_title">Vybrat soubor</string>
<string name="connect_via_link_incognito">Připojit se inkognito</string>
<string name="connect_via_link_incognito">Spojit se inkognito</string>
<string name="turn_off_battery_optimization_button">Povolit</string>
<string name="disable_notifications_button">Vypnout upozornění</string>
<string name="turn_off_system_restriction_button">Otevřít nastavení aplikace</string>
@ -1355,4 +1355,25 @@
<string name="send_receipts_disabled">vypnut</string>
<string name="send_receipts_disabled_alert_title">Receipts jsou zakázány</string>
<string name="in_developing_title">Již brzy!</string>
<string name="connect_use_current_profile">Použít aktuální profil</string>
<string name="connect_use_new_incognito_profile">Použít nový incognito profil</string>
<string name="system_restricted_background_desc">SimpleX nemůže běžet na pozadí. Pouze při spuštěné aplikaci obdržíte upozornění.</string>
<string name="system_restricted_background_warn"><![CDATA[Chcete-li povolit oznámení, vyberte prosím <b>Baterii</b> / <b>bez omezení</b> v nastavení aplikace.]]></string>
<string name="system_restricted_background_in_call_desc">Aplikace může být uzavřena po 1 minutě na pozadí.</string>
<string name="system_restricted_background_in_call_warn"><![CDATA[Chcete-li volat na pozadí, vyberte prosím <b>Baterii</b> / <b>bez omezení</b> v nastavení aplikace.]]></string>
<string name="connect__your_profile_will_be_shared">Váš profil %1$s bude sdílen.</string>
<string name="connect_via_member_address_alert_desc">Požadavek na připojení bude zaslán tomuto členu skupiny.</string>
<string name="delivery">Doručenka</string>
<string name="receipts_groups_title_disable">Zakázat doručenky pro skupiny\?</string>
<string name="receipts_groups_title_enable">Povolit doručenky pro skupiny\?</string>
<string name="receipts_groups_override_enabled">Odeslání doručenek je povoleno pro %d skupiny</string>
<string name="receipts_section_groups">Malé skupiny (max. 20)</string>
<string name="recipient_colon_delivery_status">%s: %s</string>
<string name="rcv_group_event_2_members_connected">%s a %s připojen</string>
<string name="rcv_group_event_3_members_connected">%s, %s a %s připojeni</string>
<string name="rcv_group_event_n_members_connected">%s, %s a %d dalších členů připojeno</string>
<string name="privacy_message_draft">Rozepsáno</string>
<string name="privacy_show_last_messages">Zobrazit poslední zprávy</string>
<string name="send_receipts_disabled_alert_msg">Tato skupina má více než %1$d členů, doručenky nejsou odeslány.</string>
<string name="in_developing_desc">Tato funkce zatím není podporována. Vyzkoušejte další vydání.</string>
</resources>

View File

@ -146,7 +146,7 @@
<string name="smp_server_test_disconnect">Desconectar</string>
<string name="notification_preview_mode_contact">Contacto</string>
<string name="copy_verb">Copiar</string>
<string name="create_your_profile">Crear tu perfil</string>
<string name="create_your_profile">Crea tu perfil</string>
<string name="always_use_relay">Usar siempre retransmisor</string>
<string name="set_password_to_export_desc">La base de datos está cifrada con una contraseña aleatoria. Cámbiala antes de exportar.</string>
<string name="total_files_count_and_size">%d archivo(s) con tamaño total de %s</string>
@ -192,7 +192,7 @@
<string name="delete_address__question">¿Eliminar la dirección\?</string>
<string name="display_name__field">Nombre mostrado:</string>
<string name="callstate_connecting">conectando…</string>
<string name="decentralized">Descentralizado</string>
<string name="decentralized">Descentralizada</string>
<string name="database_will_be_encrypted">La base de datos será cifrada.</string>
<string name="delete_chat_archive_question">¿Eliminar archivo del chat\?</string>
<string name="create_group_link">Crear enlace de grupo</string>
@ -485,7 +485,7 @@
<string name="mark_unread">Marcar como no leído</string>
<string name="invalid_QR_code">Código QR inválido</string>
<string name="incorrect_code">¡Código de seguridad incorrecto!</string>
<string name="markdown_in_messages">Sintaxis markdown en los mensajes</string>
<string name="markdown_in_messages">Sintaxis Markdown</string>
<string name="network_use_onion_hosts_no">No</string>
<string name="callstatus_missed">llamada perdida</string>
<string name="import_database_confirmation">Importar</string>
@ -717,7 +717,7 @@
<string name="la_notice_title_simplex_lock">Bloqueo SimpleX</string>
<string name="auth_unlock">Desbloquear</string>
<string name="this_text_is_available_in_settings">Este texto está disponible en Configuración</string>
<string name="switch_receiving_address_desc">La dirección de recepción se cambiará. El cambio se completará cuando el remitente esté en línea.</string>
<string name="switch_receiving_address_desc">La dirección de recepción pasará a otro servidor. El cambio se completará cuando el remitente esté en línea.</string>
<string name="chat_lock">Bloqueo SimpleX</string>
<string name="using_simplex_chat_servers">Usando servidores SimpleX Chat.</string>
<string name="network_session_mode_transport_isolation">Aislamiento de transporte</string>
@ -749,11 +749,11 @@
<string name="share_invitation_link">Compartir enlace de un uso</string>
<string name="update_network_session_mode_question">¿Actualizar el modo de aislamiento de transporte\?</string>
<string name="icon_descr_speaker_on">Altavoz activado</string>
<string name="stop_chat_to_enable_database_actions">Para habilitar las acciones sobre la base de datos, previamente debes detener Chat</string>
<string name="stop_chat_to_enable_database_actions">Detén SimpleX para habilitar las acciones sobre la base de datos.</string>
<string name="connection_you_accepted_will_be_cancelled">¡La conexión que has aceptado se cancelará!</string>
<string name="database_initialization_error_desc">La base de datos no funciona correctamente. Pulsa para saber más</string>
<string name="moderate_message_will_be_marked_warning">El mensaje será marcado como moderado para todos los miembros.</string>
<string name="next_generation_of_private_messaging">La próxima generación de mensajería privada</string>
<string name="next_generation_of_private_messaging">La nueva generación de mensajería privada</string>
<string name="delete_files_and_media_desc">Esta acción no se puede deshacer. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán.</string>
<string name="enable_automatic_deletion_message">Esta acción no se puede deshacer. Se eliminarán los mensajes enviados y recibidos anteriores a la selección. Puede tardar varios minutos.</string>
<string name="messages_section_description">Esta configuración se aplica a los mensajes del perfil actual</string>
@ -914,7 +914,7 @@
<string name="your_settings">Configuración</string>
<string name="your_SMP_servers">Servidores SMP</string>
<string name="you_control_your_chat">¡Tú controlas tu chat!</string>
<string name="your_profile_is_stored_on_your_device">Tu perfil, contactos y mensajes entregados se almacenan en tu dispositivo.</string>
<string name="your_profile_is_stored_on_your_device">Tu perfil, contactos y mensajes se almacenan en tu dispositivo.</string>
<string name="callstate_waiting_for_answer">esperando respuesta…</string>
<string name="callstate_waiting_for_confirmation">esperando confirmación…</string>
<string name="onboarding_notifications_mode_off">Cuando la aplicación se está ejecutando</string>
@ -923,7 +923,7 @@
<string name="your_ice_servers">Servidores ICE</string>
<string name="your_privacy">Privacidad</string>
<string name="settings_section_title_you">MIS DATOS</string>
<string name="your_chat_database">Base de datos Chat</string>
<string name="your_chat_database">Base de datos</string>
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Puedes iniciar el chat en Configuración / Base de datos o reiniciando la aplicación.</string>
<string name="you_sent_group_invitation">Has enviado una invitación de grupo</string>
<string name="num_contacts_selected">%d contacto(s) seleccionado(s)</string>
@ -1126,7 +1126,7 @@
<string name="learn_more">Más información</string>
<string name="if_you_cant_meet_in_person">Si no puedes reunirte en persona, **muestra el código QR por videollamada**, o comparte el enlace.</string>
<string name="scan_qr_to_connect_to_contact">Para conectarse, tu contacto puede escanear el código QR o usar el enlace en la aplicación.</string>
<string name="create_simplex_address">Crear dirección SimpleX</string>
<string name="create_simplex_address">Crear tu dirección SimpleX</string>
<string name="auto_accept_contact">Auto aceptar</string>
<string name="group_welcome_preview">Vista previa</string>
<string name="opening_database">Abriendo base de datos…</string>
@ -1144,15 +1144,15 @@
<string name="export_theme">Exportar tema</string>
<string name="color_surface">Menús y alertas</string>
<string name="add_address_to_your_profile">Añade la dirección a tu perfil para que tus contactos puedan compartirla con otros. La actualización del perfil se enviará a tus contactos.</string>
<string name="learn_more_about_address">Acerca de dirección SimpleX</string>
<string name="learn_more_about_address">Acerca de la dirección SimpleX</string>
<string name="address_section_title">Dirección</string>
<string name="all_your_contacts_will_remain_connected_update_sent">Todos tus contactos permanecerán conectados. La actualización del perfil se enviará a tus contactos.</string>
<string name="continue_to_next_step">Continuar</string>
<string name="dark_theme">Tema oscuro</string>
<string name="customize_theme_title">Personalizar tema</string>
<string name="enter_welcome_message_optional">Introduce mensaje de bienvenida… (opcional)</string>
<string name="create_address_and_let_people_connect">Crear una dirección para que otras personas se puedan conectar contigo.</string>
<string name="dont_create_address">No crear dirección</string>
<string name="create_address_and_let_people_connect">Crea una dirección para que otras personas puedan conectar contigo.</string>
<string name="dont_create_address">No crear dirección SimpleX</string>
<string name="email_invite_body">¡Hola!
\nConecta conmigo a través de SimpleX Chat: %s</string>
<string name="import_theme">Importar tema</string>
@ -1170,7 +1170,7 @@
<string name="stop_sharing">Dejar de compartir</string>
<string name="stop_sharing_address">¿Dejar de compartir la dirección\?</string>
<string name="theme_colors_section_title">COLORES DEL TEMA</string>
<string name="you_can_create_it_later">Puedes crearlo más tarde</string>
<string name="you_can_create_it_later">Puedes crearla más tarde</string>
<string name="share_address_with_contacts_question">¿Compartir la dirección con los contactos\?</string>
<string name="share_with_contacts">Compartir con contactos</string>
<string name="color_title">Título</string>

View File

@ -82,7 +82,7 @@
<string name="info_row_connection">Yhteys</string>
<string name="ttl_d">%dd</string>
<string name="ttl_days">%d päivää</string>
<string name="disappearing_prohibited_in_this_chat">Tuhoutuvat viestit ovat kiellettyjä tässä keskustelussa.</string>
<string name="disappearing_prohibited_in_this_chat">Katoavat viestit ovat kiellettyjä tässä keskustelussa.</string>
<string name="network_session_mode_user_description"><![CDATA[Erillistä TCP-yhteyttä (ja SOCKS-tunnistetietoja) käytetään <b>jokaisessa käyttämässäsi sovelluksen chat-profiilissa</b>.]]></string>
<string name="app_version_code">Sovellusversio: %s</string>
<string name="delete_address">Poista osoite</string>
@ -140,7 +140,7 @@
<string name="delete_verb">Poista</string>
<string name="delete_message__question">Poista viesti\?</string>
<string name="group_connection_pending">yhdistää…</string>
<string name="disappearing_message">Tuhoutuva viesti</string>
<string name="disappearing_message">Katoava viesti</string>
<string name="send_disappearing_message_custom_time">Mukautettu aika</string>
<string name="delete_contact_menu_action">Poista</string>
<string name="delete_group_menu_action">Poista</string>
@ -158,7 +158,7 @@
<string name="settings_developer_tools">Kehittäjän työkalut</string>
<string name="cannot_access_keychain">Ei pääsyä Keystoreen tietokannan salasanan tallentamiseksi</string>
<string name="share_text_database_id">Tietokannan tunnus: %d</string>
<string name="info_row_disappears_at">Tuhoutuu klo</string>
<string name="info_row_disappears_at">Katoaa klo</string>
<string name="chat_database_deleted">Keskustelujen tietokanta poistettu</string>
<string name="delete_chat_profile_question">Poista keskusteluprofiili\?</string>
<string name="delete_messages_after">Poista viestit tämän jälkeen</string>
@ -187,7 +187,7 @@
<string name="audio_call_no_encryption">äänipuhelu (ei e2e-salattu)</string>
<string name="always_use_relay">Käytä aina relettä</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Salli kontaktiesi lähettää katoavia viestejä.</string>
<string name="timed_messages">Tuhoutuvat viestit</string>
<string name="timed_messages">Katoavat viestit</string>
<string name="icon_descr_context">Kontekstikuvake</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Skannaa QR-koodi</b>: muodostaaksesi yhteyden kontaktiisi, joka näyttää QR-koodin sinulle.]]></string>
<string name="icon_descr_cancel_live_message">Peruuta live-viesti</string>
@ -199,7 +199,7 @@
<string name="settings_audio_video_calls">Ääni- ja videopuhelut</string>
<string name="call_on_lock_screen">Puhelut lukitusnäytöllä:</string>
<string name="conn_level_desc_direct">suora</string>
<string name="disappearing_messages_are_prohibited">Tuhoutuvat viestit ovat kiellettyjä tässä ryhmässä.</string>
<string name="disappearing_messages_are_prohibited">Katoavat viestit ovat kiellettyjä tässä ryhmässä.</string>
<string name="server_connected">yhdistetty</string>
<string name="display_name_connecting">yhdistää…</string>
<string name="display_name_connection_established">yhteys luotu</string>
@ -240,7 +240,7 @@
<string name="both_you_and_your_contact_can_send_disappearing">Sekä sinä että kontaktisi voitte lähettää katoavia viestejä.</string>
<string name="chat_preferences_contact_allows">Kontakti sallii</string>
<string name="chat_preferences_default">oletus (%s)</string>
<string name="v4_4_disappearing_messages">Tuhoutuvat viestit</string>
<string name="v4_4_disappearing_messages">Katoavat viestit</string>
<string name="copied">Kopioitu leikepöydälle</string>
<string name="share_one_time_link">Luo kertaluonteinen kutsulinkki</string>
<string name="mtr_error_no_down_migration">tietokantaversio on uudempi kuin sovellus, mutta ei alaspäin siirtymistä: %s</string>
@ -336,7 +336,7 @@
<string name="change_verb">Muuta</string>
<string name="item_info_current">(nykyinen)</string>
<string name="share_text_deleted_at">Poistettu: %s</string>
<string name="share_text_disappears_at">Tuhoutuu klo: %s</string>
<string name="share_text_disappears_at">Katoaa klo: %s</string>
<string name="create_secret_group_title">Luo salainen ryhmä</string>
<string name="chat_preferences_always">aina</string>
<string name="cant_delete_user_profile">Käyttäjäprofiilia ei voi poistaa!</string>
@ -406,7 +406,7 @@
<string name="join_group_question">Liity ryhmään\?</string>
<string name="network_option_enable_tcp_keep_alive">Ota TCP-säilytys käyttöön</string>
<string name="incognito">Incognito</string>
<string name="incognito_info_protects">Incognito-tila suojaa pääprofiilisi nimen ja kuvan yksityisyyttä jokaiselle uudelle yhteyshenkilölle luodaan uusi satunnainen profiili.</string>
<string name="incognito_info_protects">Incognito-tila suojaa yksityisyyttäsi käyttämällä uutta satunnaista profiilia jokaiselle kontaktille.</string>
<string name="ttl_mth">%dmth</string>
<string name="ttl_m">%dm</string>
<string name="v4_6_group_welcome_message">Ryhmän tervetuloviesti</string>
@ -451,7 +451,7 @@
<string name="import_database_question">Tuo keskustelujen-tietokanta\?</string>
<string name="file_with_path">Tiedosto: %s</string>
<string name="error_removing_member">Virhe poistettaessa jäsentä</string>
<string name="message_deletion_prohibited">Viestien peruuttamaton poistaminen on kielletty tässä keskustelussa.</string>
<string name="message_deletion_prohibited">Viestien peruuttamaton poisto on kielletty tässä keskustelussa.</string>
<string name="join_group_button">Liity</string>
<string name="join_group_incognito_button">Liity incognito-tilassa</string>
<string name="joining_group">Liittyy ryhmään</string>
@ -474,10 +474,10 @@
<string name="image_will_be_received_when_contact_is_online">Kuva vastaanotetaan, kun kontaktisi on verkossa, odota tai tarkista myöhemmin!</string>
<string name="if_you_cant_meet_in_person">Jos et voi tavata henkilökohtaisesti, näytä QR-koodi videopuhelussa tai jaa linkki.</string>
<string name="onboarding_notifications_mode_subtitle">Voit muuttaa sitä myöhemmin asetuksista.</string>
<string name="encrypt_database_question">Salataanko tietokanta\?</string>
<string name="encrypt_database_question">Salaa tietokanta\?</string>
<string name="button_edit_group_profile">Muokkaa ryhmäprofiilia</string>
<string name="delete_group_for_all_members_cannot_undo_warning">Ryhmä poistetaan kaikilta jäseniltä - tätä ei voi kumota!</string>
<string name="delete_group_for_self_cannot_undo_warning">Ryhmä poistetaan sinulta - tätä ei voi peruuttaa!</string>
<string name="delete_group_for_self_cannot_undo_warning">Ryhmä poistetaan sinulta - tätä ei voi perua!</string>
<string name="error_creating_link_for_group">Virhe ryhmälinkin luomisessa</string>
<string name="info_row_group">Ryhmä</string>
<string name="incognito_info_allows">Se mahdollistaa useiden nimettömien yhteyksien muodostamisen yhdessä keskusteluprofiilissa ilman, että niiden välillä on jaettuja tietoja.</string>
@ -514,7 +514,7 @@
<string name="onboarding_notifications_mode_service">Välitön</string>
<string name="encrypted_audio_call">e2e-salattu äänipuhelu</string>
<string name="allow_accepting_calls_from_lock_screen">Ota puhelut käyttöön lukitusnäytöltä asetuksista.</string>
<string name="status_e2e_encrypted">e2e salattu</string>
<string name="status_e2e_encrypted">e2e-salattu</string>
<string name="settings_section_title_incognito">Incognito-tila</string>
<string name="error_with_info">Virhe: %s</string>
<string name="alert_message_group_invitation_expired">Ryhmäkutsu ei ole enää voimassa, lähettäjä poisti sen.</string>
@ -543,15 +543,15 @@
<string name="downgrade_and_open_chat">Alenna ja avaa chat</string>
<string name="icon_descr_group_inactive">Ei-aktiivinen ryhmä</string>
<string name="v4_3_improved_privacy_and_security_desc">Piilota sovellusnäyttö viimeisimmissä sovelluksissa.</string>
<string name="v4_5_italian_interface">Italian käyttöliittymä</string>
<string name="v5_0_large_files_support_descr">Nopea ja ei odota, kunnes lähettäjä on online-tilassa!</string>
<string name="v4_5_italian_interface">Italialainen käyttöliittymä</string>
<string name="v5_0_large_files_support_descr">Nopea ja ei odotusta, kunnes lähettäjä on online-tilassa!</string>
<string name="v4_6_reduced_battery_usage">Entisestä vähentynyt akun käyttö</string>
<string name="v5_1_japanese_portuguese_interface">Japanin ja portugalin käyttöliittymä</string>
<string name="error_saving_file">Virhe tiedoston tallentamisessa</string>
<string name="icon_descr_help">apua</string>
<string name="incorrect_code">Väärä turvakoodi!</string>
<string name="error_sending_message">Virhe viestin lähettämisessä</string>
<string name="turn_off_battery_optimization"><![CDATA[Jotta voit käyttää sitä, <b>poista akun optimointi käytöstä</b> kohteelle SimpleX seuraavassa valintaikkunassa. Muussa tapauksessa ilmoitukset poistetaan käytöstä.]]></string>
<string name="turn_off_battery_optimization"><![CDATA[Käyttääksesi sitä, <b> salli SimpleX:n toimia taustalla </b> seuraavassa ikkunassa. Muutoin ilmoitukset poistetaan käytöstä.]]></string>
<string name="notification_preview_mode_hidden">Piilotettu</string>
<string name="la_immediately">Heti</string>
<string name="la_enter_app_passcode">Syötä pääsykoodi</string>
@ -570,7 +570,7 @@
<string name="enable_lock">Ota lukitus käyttöön</string>
<string name="group_invitation_item_description">kutsu ryhmään %1$s</string>
<string name="icon_descr_add_members">Kutsu jäseniä</string>
<string name="group_invitation_expired">Ryhmäkutsu on vanhentunut</string>
<string name="group_invitation_expired">Vanhentunut ryhmäkutsu</string>
<string name="rcv_group_event_member_added">kutsuttu %1$s</string>
<string name="group_full_name_field">Ryhmän koko nimi:</string>
<string name="full_name_optional__prompt">Koko nimi (valinnainen)</string>
@ -592,7 +592,7 @@
<string name="section_title_for_console">KONSOLIIN</string>
<string name="group_member_status_group_deleted">poistettu ryhmä</string>
<string name="snd_group_event_group_profile_updated">ryhmäprofiili päivitetty</string>
<string name="alert_title_group_invitation_expired">Kutsu on vanhentunut!</string>
<string name="alert_title_group_invitation_expired">Vanhentunut kutsu!</string>
<string name="group_member_status_invited">kutsuttu</string>
<string name="conn_level_desc_indirect">epäsuora (%1$s)</string>
<string name="group_display_name_field">Ryhmän näyttönimi:</string>
@ -602,10 +602,10 @@
<string name="group_members_can_delete">Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti.</string>
<string name="group_members_can_send_dms">Ryhmän jäsenet voivat lähettää suoraviestejä.</string>
<string name="group_members_can_send_voice">Ryhmän jäsenet voivat lähettää ääniviestejä.</string>
<string name="message_deletion_prohibited_in_chat">Viestien peruuttamaton poistaminen on kielletty tässä ryhmässä.</string>
<string name="message_deletion_prohibited_in_chat">Viestien peruuttamaton poisto on kielletty tässä ryhmässä.</string>
<string name="ttl_months">%d kuukautta</string>
<string name="ttl_sec">%d sek</string>
<string name="v5_1_message_reactions_descr">Vihdoinkin meillä on ne! 🚀</string>
<string name="v5_1_message_reactions_descr">Vihdoinkin meillä! 🚀</string>
<string name="custom_time_unit_hours">tuntia</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Tarkista, että käytit oikeaa linkkiä tai pyydä kontaktiasi lähettämään sinulle uusi linkki.</string>
<string name="sender_may_have_deleted_the_connection_request">Lähettäjä on saattanut poistaa yhteyspyynnön.</string>
@ -619,7 +619,7 @@
<string name="notification_preview_mode_message">Viestin teksti</string>
<string name="notification_preview_mode_contact_desc">Näytä vain kontakti</string>
<string name="notification_preview_new_message">uusi viesti</string>
<string name="la_notice_title_simplex_lock">Simplex Lock</string>
<string name="la_notice_title_simplex_lock">SimpleX Lock</string>
<string name="auth_simplex_lock_turned_on">SimpleX Lock päällä</string>
<string name="auth_open_chat_console">Avaa keskustelukonsoli</string>
<string name="message_delivery_error_title">Viestin toimitusvirhe</string>
@ -848,7 +848,7 @@
<string name="read_more_in_github_with_link"><![CDATA[Lue lisää <font color="#0088ff">GitHub-arkistostamme</font>.]]></string>
<string name="onboarding_notifications_mode_periodic">Säännölliset</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Vain asiakaslaitteet tallentavat käyttäjäprofiileja, yhteystietoja, ryhmiä ja viestejä, jotka on lähetetty <b>2-kerroksisella päästä päähän -salauksella</b>.]]></string>
<string name="read_more_in_github">Lue lisää GitHub-arkistostamme.</string>
<string name="read_more_in_github">Lue lisää GitHub-tietovarastostamme.</string>
<string name="paste_the_link_you_received">Liitä vastaanotettu linkki</string>
<string name="relay_server_protects_ip">Välityspalvelin suojaa IP-osoitteesi, mutta se voi tarkkailla puhelun kestoa.</string>
<string name="open_simplex_chat_to_accept_call">Avaa SimpleX Chat hyväksyäksesi puhelun</string>
@ -1007,7 +1007,7 @@
<string name="prohibit_message_reactions">Estä viestireaktiot.</string>
<string name="prohibit_sending_disappearing_messages">Estä katoavien viestien lähettäminen.</string>
<string name="accept_feature_set_1_day">Aseta 1 päivä</string>
<string name="only_your_contact_can_make_calls">Vain yhteyshenkilösi voi soittaa puheluita.</string>
<string name="only_your_contact_can_make_calls">Vain kontaktisi voi soittaa puheluita.</string>
<string name="prohibit_calls">Estä ääni- ja videopuhelut.</string>
<string name="prohibit_direct_messages">Estä suorien viestien lähettäminen jäsenille.</string>
<string name="message_reactions_prohibited_in_this_chat">Viestireaktiot ovat kiellettyjä tässä keskustelussa.</string>
@ -1043,7 +1043,7 @@
<string name="xftp_servers">XFTP-palvelimet</string>
<string name="smp_servers_your_server">Palvelimesi</string>
<string name="smp_servers_your_server_address">Palvelimesi osoite</string>
<string name="enable_automatic_deletion_message">Tätä toimintoa ei voi kumota - valittua aikaisemmin lähetetyt ja vastaanotetut viestit poistetaan. Se voi kestää useita minuutteja.</string>
<string name="enable_automatic_deletion_message">Tätä toimintoa ei voi kumota - valittua aikaisemmin lähetetyt ja vastaanotetut viestit poistetaan. Tämä voi kestää useita minuutteja.</string>
<string name="whats_new">Uusimmat</string>
<string name="you_will_still_receive_calls_and_ntfs">Saat edelleen puheluita ja ilmoituksia mykistetyiltä profiileilta, kun ne ovat aktiivisia.</string>
<string name="network_disable_socks">Käytä suoraa Internet-yhteyttä\?</string>
@ -1199,7 +1199,7 @@
<string name="videos_limit_title">Liikaa videoita!</string>
<string name="voice_message">Ääniviesti</string>
<string name="waiting_for_video">Odottaa videota</string>
<string name="switch_receiving_address_desc">Tämä ominaisuus on kokeellinen! Se toimii vain, jos toisella on asennettuna versio 4.2. Sinun pitäisi nähdä viesti keskustelussa, kun osoitteenmuutos on valmis - tarkista, että voit edelleen vastaanottaa viestejä kyseiseltä kontaktilta (tai ryhmän jäseneltä).</string>
<string name="switch_receiving_address_desc">Vastaanotto-osoite vaihdetaan toiseen palvelimeen. Osoitteenmuutos tehdään sen jälkeen, kun lähettäjä tulee verkkoon.</string>
<string name="you_need_to_allow_to_send_voice">Sinun on sallittava kontaktiesi lähettää ääniviestejä, jotta voit lähettää niitä.</string>
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Voit <font color="#0088ff">olla yhteydessä SimpleX Chatin -kehittäjiin kysyäksesi kysymyksiä ja saadaksesi päivityksiä</font>.]]></string>
<string name="this_QR_code_is_not_a_link">Tämä QR-koodi ei ole linkki!</string>
@ -1260,4 +1260,118 @@
<string name="settings_restart_app">Käynnistä uudelleen</string>
<string name="shutdown_alert_question">Sulje\?</string>
<string name="la_mode_off">Pois</string>
<string name="v5_2_message_delivery_receipts">Viestien toimituskuittaukset!</string>
<string name="v5_2_more_things_descr">- vakaampi viestien toimitus.
\n- hieman paremmat ryhmät.
\n- ja paljon muuta!</string>
<string name="delivery_receipts_are_disabled">Toimituskuittaukset poissa käytöstä!</string>
<string name="you_can_enable_delivery_receipts_later">Voit ottaa käyttöön myöhemmin asetusten kautta</string>
<string name="you_can_enable_delivery_receipts_later_alert">Voit ottaa ne käyttöön myöhemmin sovelluksen Yksityisyys &amp; Turvallisuus -asetuksista.</string>
<string name="error_aborting_address_change">Virhe osoitteenmuutoksen keskeytyksessä</string>
<string name="abort_switch_receiving_address">Keskeytä osoitteenvaihto</string>
<string name="network_option_protocol_timeout_per_kb">Protokollan aikakatkaisu per KB</string>
<string name="files_are_prohibited_in_group">Tiedostot ja media ovat tässä ryhmässä kiellettyjä.</string>
<string name="group_members_can_send_files">Ryhmän jäsenet voivat lähettää tiedostoja ja mediaa.</string>
<string name="connect_via_link_incognito">Yhdistä Incognito</string>
<string name="connect_use_current_profile">Käytä nykyistä profiilia</string>
<string name="connect_use_new_incognito_profile">Käytä uutta incognito-profiilia</string>
<string name="turn_off_battery_optimization_button">Salli</string>
<string name="disable_notifications_button">Poista ilmoitukset käytöstä</string>
<string name="turn_off_system_restriction_button">Avaa asetukset</string>
<string name="system_restricted_background_desc">SimpleX ei toimi tausta-ajossa. Saat ilmoitukset ainostaan, kun sovellus on käynnissä.</string>
<string name="system_restricted_background_warn"><![CDATA[Ilmoitusten sallimiseksi valitse <b> Sovelluksen akun käyttö </b> / <b> rajoittamaton </b> sovellusasetuksista.]]></string>
<string name="system_restricted_background_in_call_title">Ei taustapuheluita</string>
<string name="system_restricted_background_in_call_desc">Sovellus voi sulkeutua 1 minuutin jälkeen tausta-ajossa.</string>
<string name="system_restricted_background_in_call_warn"><![CDATA[Puheluiden soittamiseksi taustalla, valitse <b>Sovelluksen akun käyttö </b> / <b> rajoittamaton </b> sovellusasetuksista.]]></string>
<string name="in_reply_to">Vastauksena</string>
<string name="abort_switch_receiving_address_question">Keskeytä osoitteenvaihto\?</string>
<string name="favorite_chat">Suosikki</string>
<string name="unfavorite_chat">Epäsuosikki</string>
<string name="connect__a_new_random_profile_will_be_shared">Uusi satunnainen profiili jaetaan.</string>
<string name="paste_the_link_you_received_to_connect_with_your_contact">Liitä linkki, jonka sait yhteydenottoon kontaktisi kanssa…</string>
<string name="connect__your_profile_will_be_shared">Profiilisi %1$s jaetaan.</string>
<string name="receipts_groups_title_disable">Kuittaukset pois käytöstä ryhmiltä\?</string>
<string name="in_developing_title">Tulossa pian!</string>
<string name="receipts_groups_disable_for_all">Poista käytöstä kaikilta ryhmiltä</string>
<string name="files_and_media">Tiedostot ja media</string>
<string name="receipts_groups_enable_keep_overrides">Salli (pidä ryhmäohitukset)</string>
<string name="sending_delivery_receipts_will_be_enabled_all_profiles">"Toimituskuittauksien lähettäminen otetaan käyttöön kaikille kontakteille näkyvissä keskusteluprofiileissa."</string>
<string name="receipts_groups_override_enabled">Kuittauksien lähettäminen on käytössä %d ryhmille</string>
<string name="abort_switch_receiving_address_confirm">Keskeytä</string>
<string name="sync_connection_force_confirm">Uudelleenneuvottele</string>
<string name="sync_connection_force_question">Uudelleenneuvottele salaus\?</string>
<string name="sync_connection_force_desc">Salaus toimii ja uutta salaussopimusta ei tarvita. Tämä voi johtaa yhteysvirheisiin!</string>
<string name="receipts_section_description">Nämä asetukset koskevat nykyistä profiiliasi</string>
<string name="receipts_section_description_1">Ne voidaan ohittaa kontakti- ja ryhmäasetuksissa.</string>
<string name="conn_event_ratchet_sync_ok">salaus ok</string>
<string name="conn_event_ratchet_sync_allowed">salauksen uudelleenneuvottelu sallittu</string>
<string name="fix_connection_confirm">Korjaa</string>
<string name="fix_connection_not_supported_by_contact">Kontakti ei tue korjausta</string>
<string name="fix_connection_not_supported_by_group_member">Ryhmän jäsen ei tue korjausta</string>
<string name="renegotiate_encryption">Uudelleenneuvottele salaus</string>
<string name="in_developing_desc">Tätä ominaisuutta ei vielä tueta. Kokeile seuraavaa versiota.</string>
<string name="delivery">Toimitus</string>
<string name="no_info_on_delivery">Ei toimitustietoja</string>
<string name="no_filtered_chats">Ei suodatettuja keskusteluja</string>
<string name="no_selected_chat">Ei valittua keskustelua</string>
<string name="only_owners_can_enable_files_and_media">Vain ryhmän omistajat voivat sallia tiedostoja ja mediaa.</string>
<string name="files_and_media_prohibited">Tiedostot ja media kielletty!</string>
<string name="abort_switch_receiving_address_desc">Osoitteenmuutos keskeytetään. Käytetään vanhaa vastaanotto-osoitetta.</string>
<string name="choose_file_title">Valitse tiedosto</string>
<string name="receipts_section_groups">Pienryhmät (max 20)</string>
<string name="send_receipts_disabled_alert_title">Kuittaukset pois käytöstä</string>
<string name="send_receipts_disabled_alert_msg">Ryhmällä on yli %1$d jäsentä, toimituskuittauksia ei lähetetä.</string>
<string name="recipient_colon_delivery_status">%s: %s</string>
<string name="v5_2_more_things">Muutama asia lisää</string>
<string name="v5_2_fix_encryption">Pidä kontaktisi</string>
<string name="receipts_section_contacts">Kontaktit</string>
<string name="receipts_contacts_disable_for_all">Poista käytöstä kaikilta</string>
<string name="receipts_contacts_enable_for_all">Salli kaikille</string>
<string name="receipts_contacts_enable_keep_overrides">Salli (pidä ohitukset)</string>
<string name="receipts_contacts_disable_keep_overrides">Poista käytöstä (pidä ohitukset)</string>
<string name="receipts_contacts_title_disable">Kuittaukset pois käytöstä\?</string>
<string name="receipts_contacts_title_enable">Salli kuittaukset\?</string>
<string name="receipts_contacts_override_disabled">Kuittauksien lähettäminen on pois käytöstä %d kontakteilta</string>
<string name="receipts_contacts_override_enabled">Kuittauksien lähettäminen on käytössä %d kontakteille</string>
<string name="settings_section_title_delivery_receipts">LÄHETÄ TOIMITUSKUITTAUKSET VASTAANOTTAJALLE</string>
<string name="rcv_conn_event_verification_code_reset">turvakoodi on muuttunut</string>
<string name="conn_event_ratchet_sync_started">hyväksyy salausta…</string>
<string name="snd_conn_event_ratchet_sync_allowed">salauksen uudelleenneuvottelu sallittu %s:lle</string>
<string name="conn_event_ratchet_sync_required">tarvitaan salauksen uudelleenneuvottelua</string>
<string name="snd_conn_event_ratchet_sync_started">hyväksyy salausta %s:lle…</string>
<string name="conn_event_ratchet_sync_agreed">salaus sovittu</string>
<string name="snd_conn_event_ratchet_sync_agreed">salaus sovittu %s:lle</string>
<string name="snd_conn_event_ratchet_sync_ok">salaus ok %s:lle</string>
<string name="snd_conn_event_ratchet_sync_required">tarvitaan salauksen uudelleenneuvottelua %s:lle</string>
<string name="sender_at_ts">"%s klo %s"</string>
<string name="send_receipts">Lähetä kuittaukset</string>
<string name="fix_connection">Korjaa yhteys</string>
<string name="fix_connection_question">Korjaa yhteys\?</string>
<string name="allow_to_send_files">Salli tiedostojen ja median lähettäminen.</string>
<string name="prohibit_sending_files">Estä tiedostojen ja median lähettäminen.</string>
<string name="v5_2_disappear_one_message">Hävitä yksi viesti</string>
<string name="v5_2_fix_encryption_descr">Korjaa salaus varmuuskopioiden palauttamisen jälkeen.</string>
<string name="v5_2_disappear_one_message_descr">Jopa kun ei käytössä keskustelussa.</string>
<string name="receipts_groups_disable_keep_overrides">Poista käytöstä (pidä ryhmäohitukset)</string>
<string name="receipts_groups_enable_for_all">Salli kaikille ryhmille</string>
<string name="receipts_groups_title_enable">Salli kuittaukset ryhmille\?</string>
<string name="privacy_message_draft">Viestiluonnos</string>
<string name="receipts_groups_override_disabled">Kuittauksien lähettäminen on pois käytöstä %d ryhmiltä</string>
<string name="privacy_show_last_messages">Näytä viimeiset viestit</string>
<string name="send_receipts_disabled">ei käytössä</string>
<string name="rcv_group_event_2_members_connected">%s ja %s yhdistetty</string>
<string name="rcv_group_event_n_members_connected">%s, %s ja %d muut jäsenet yhdistetty</string>
<string name="rcv_group_event_3_members_connected">%s, %s ja %s yhdistetty</string>
<string name="dont_enable_receipts">Älä salli</string>
<string name="error_enabling_delivery_receipts">Virhe toimituskuittauksien sallimisessa!</string>
<string name="connect_via_member_address_alert_title">Yhdistä suoraan\?</string>
<string name="connect_via_member_address_alert_desc">Yhteyspyyntö lähetetään tälle ryhmän jäsenelle.</string>
<string name="delivery_receipts_title">Toimituskuittaukset!</string>
<string name="enable_receipts_all">Salli</string>
<string name="v5_2_favourites_filter_descr">Suodata lukemattomia- ja suosikkikeskusteluja.</string>
<string name="v5_2_favourites_filter">Löydä keskustelut nopeammin</string>
<string name="sending_delivery_receipts_will_be_enabled">Toimituskuittauksien lähettäminen otetaan käyttöön kaikille kontakteille.</string>
<string name="v5_2_message_delivery_receipts_descr">Toinen kuittaus, joka uupui! ✅</string>
<string name="error_synchronizing_connection">Virhe yhteyden synkronoinnissa</string>
<string name="no_history">Ei historiaa</string>
</resources>

View File

@ -1316,7 +1316,7 @@
<string name="receipts_contacts_override_enabled">L\'envoi d\'accusés de réception est activé pour les contacts de %d</string>
<string name="conn_event_ratchet_sync_started">accord sur le chiffrement…</string>
<string name="conn_event_ratchet_sync_agreed">chiffrement accepté</string>
<string name="conn_event_ratchet_sync_ok">chiffrement ok</string>
<string name="conn_event_ratchet_sync_ok">chiffrement OK</string>
<string name="conn_event_ratchet_sync_allowed">renégociation de chiffrement autorisée</string>
<string name="conn_event_ratchet_sync_required">renégociation de chiffrement requise</string>
<string name="snd_conn_event_ratchet_sync_ok">chiffrement ok pour %s</string>

View File

@ -689,7 +689,7 @@
<string name="old_database_archive">ארכיון מסד נתונים ישן</string>
<string name="chat_item_ttl_none">לעולם לא</string>
<string name="no_received_app_files">לא התקבלו או נשלחו קבצים</string>
<string name="new_member_role">תפקיד חבר קבוצה</string>
<string name="new_member_role">תפקיד חבר קבוצה חדש</string>
<string name="no_contacts_selected">לא נבחרו אנשי קשר</string>
<string name="member_info_section_title_member">חבר קבוצה</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">חבר הקבוצה יוסר מהקבוצה לא ניתן לבטל זאת!</string>
@ -1257,7 +1257,7 @@
<string name="your_preferences">ההעדפות שלך</string>
<string name="you_will_still_receive_calls_and_ntfs">עדיין תקבלו שיחות והתראות מפרופילים מושתקים כאשר הם פעילים.</string>
<string name="abort_switch_receiving_address_confirm">בטל</string>
<string name="abort_switch_receiving_address_question">בטל שינוי כתובת\?</string>
<string name="abort_switch_receiving_address_question">האם לבטל שינוי כתובת\?</string>
<string name="abort_switch_receiving_address_desc">שינוי הכתובת יבוטל. ייעשה שימוש בכתובת הקבלה הישנה.</string>
<string name="shutdown_alert_desc">ההתראות יפסיקו לפעול עד שתפעיל את האפליקציה מחדש</string>
<string name="abort_switch_receiving_address">בטל שינוי כתובת</string>

View File

@ -167,7 +167,6 @@
<string name="icon_descr_call_ended">通話が終了しました。</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[直接会えない時は、 <b>ビデオ通話中にQRコードを見せてもらうか</b>、招待リンクを送ってもらえば相手に繋がります。]]></string>
<string name="member_will_be_removed_from_group_cannot_be_undone">メンバーをグループから除名する (※元に戻せません※)</string>
<string name="message_delivery_error_title">メッセージ送信エラー</string>
<string name="call_on_lock_screen">通話をロック画面に表示</string>
<string name="icon_descr_cancel_link_preview">リンクのプレビューを中止</string>
<string name="icon_descr_cancel_live_message">ライブメッセージを中止</string>
@ -485,7 +484,7 @@
<string name="smp_server_test_create_queue">サーバの待ち行列を作成する</string>
<string name="smp_server_test_delete_queue">待ち行列を削除</string>
<string name="smp_server_test_disconnect">切断</string>
<string name="turn_off_battery_optimization"><![CDATA[利用するには次の画面にてSimpleXに対する <b>電気省電力の設定をオフ</b> for SimpleX してください。そうしないと通知が無効になります。]]></string>
<string name="turn_off_battery_optimization"><![CDATA[利用するには次の画面にてSimpleXに対する <b>SimpleX のバックグラウンドでの実行を許可</b> してください。そうしないと通知が無効になります。]]></string>
<string name="database_initialization_error_title">データベースを起動できません。</string>
<string name="settings_notification_preview_title">通知のプレビュー</string>
<string name="simplex_service_notification_text">メッセージ受信中…</string>
@ -588,7 +587,7 @@
<string name="error_removing_member">メンバー除名にエラー発生</string>
<string name="conn_level_desc_indirect">関節 (%1$s)</string>
<string name="incognito">シークレットモード</string>
<string name="incognito_info_protects">シークレットモードとは、メインのプロフィールとプロフィール画像を守るために、新しい連絡先を追加する時に、その連絡先に対してランダムなプロフィールが作成されるという対策です。</string>
<string name="incognito_info_protects">シークレット モードでは、連絡先ごとに新しいランダムなプロファイルを使用してプライバシーを保護します。</string>
<string name="chat_preferences_no">いいえ</string>
<string name="chat_preferences_on">オン</string>
<string name="direct_messages">ダイレクトメッセージ</string>
@ -904,7 +903,7 @@
<string name="simplex_link_mode">SimpleXリンク</string>
<string name="settings_section_title_support">SIMPLEX CHATを支援</string>
<string name="smp_servers_test_servers">テストサーバ</string>
<string name="switch_receiving_address_desc">開発中の機能です相手のクライアントが4.2でなければ機能しません。アドレス変更が完了すると、会話にメッセージが出ます。連絡相手 (またはグループのメンバー) からメッセージを受信できないかをご確認ください</string>
<string name="switch_receiving_address_desc">受信アドレスは別のサーバーに変更されます。アドレス変更は送信者がオンラインになった後に完了します</string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[あなたのプライバシーを守るために、このアプリはプッシュ通知の変わりに <b>SimpleX バックグラウンド・サービス</b> を使ってます。一日の電池使用量は約3%です。]]></string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">あなたのプライバシーを守るために、他のアプリと違って、ユーザーIDの変わりに SimpleX メッセージ束毎にIDを配布し、各連絡先が別々と扱います。</string>
<string name="group_main_profile_sent">あなたのチャットプロフィールが他のグループメンバーに送られます。</string>
@ -1258,9 +1257,9 @@
<string name="choose_file_title">ファイルを選択</string>
<string name="unfavorite_chat">お気に入りを解除</string>
<string name="favorite_chat">お気に入り</string>
<string name="receipts_contacts_override_enabled">連絡先 %d に対して既読の通知が有効です</string>
<string name="receipts_contacts_override_enabled">Sending receipts is enabled for %d contacts</string>
<string name="receipts_contacts_enable_for_all">すべて有効</string>
<string name="receipts_contacts_override_disabled">連絡先 %d に対して既読の通知が無効です</string>
<string name="receipts_contacts_override_disabled">Sending receipts is disabled for %d contacts</string>
<string name="receipts_contacts_disable_for_all">すべて無効</string>
<string name="settings_shutdown">終了</string>
<string name="conn_event_ratchet_sync_started">暗号化に同意しています…</string>
@ -1289,7 +1288,7 @@
<string name="error_aborting_address_change">アドレス変更中止エラー</string>
<string name="no_filtered_chats">フィルタされたチャットはありません</string>
<string name="files_and_media_prohibited">ファイルとメディアは禁止されています!</string>
<string name="abort_switch_receiving_address_desc">住所変更は中止されます。古い受信アドレスが使用されます。</string>
<string name="abort_switch_receiving_address_desc">アドレス変更は中止されます。古い受信アドレスが使用されます。</string>
<string name="abort_switch_receiving_address_question">アドレス変更を中止しますか?</string>
<string name="settings_section_title_app">アプリ</string>
<string name="non_fatal_errors_occured_during_import">インポート中に致命的でないエラーが発生しました - 詳細はチャットコンソールを参照してください。</string>
@ -1304,18 +1303,17 @@
<string name="v5_2_fix_encryption_descr">バックアップの復元後に暗号化を修正します。</string>
<string name="snd_conn_event_ratchet_sync_started">暗号化に同意しています: %s</string>
<string name="conn_event_ratchet_sync_agreed">暗号化に同意しました</string>
<string name="receipts_contacts_title_disable">既読通知を無効にしますか?</string>
<string name="receipts_contacts_title_disable">Disable receipts\?</string>
<string name="fix_connection_not_supported_by_contact">連絡先による修正はサポートされていません</string>
<string name="fix_connection_not_supported_by_group_member">グループメンバーによる修正はサポートされていません</string>
<string name="receipts_section_contacts">連絡先</string>
<string name="receipts_section_description_1">これらは連絡先の設定が優先します</string>
<string name="receipts_section_description_1">これらは連絡先とグループの設定が優先されます。</string>
<string name="receipts_section_description">これらの設定は現在のプロファイル用です</string>
<string name="receipts_contacts_title_enable">既読通知を有効にしますか?</string>
<string name="receipts_contacts_title_enable">Enable receipts\?</string>
<string name="sender_at_ts">%s : %s</string>
<string name="fix_connection">接続を修正</string>
<string name="files_and_media">ファイルとメディア</string>
<string name="you_can_enable_delivery_receipts_later_alert">あとでアプリのプライバシーとセキュリティの設定から有効にすることができます。</string>
<string name="error_enabling_delivery_receipts">既読通知の有効化でエラーが発生しました!</string>
<string name="item_info_no_text">テキストなし</string>
<string name="error_synchronizing_connection">接続の同期エラー</string>
<string name="no_history">履歴はありません</string>
@ -1326,4 +1324,56 @@
<string name="only_owners_can_enable_files_and_media">ファイルやメディアを有効にできるのは、グループオーナーだけです。</string>
<string name="sync_connection_force_confirm">再ネゴシエート</string>
<string name="settings_restart_app">再起動</string>
<string name="connect_via_link_incognito">シークレットモードで接続</string>
<string name="turn_off_battery_optimization_button">許可</string>
<string name="connect_via_member_address_alert_title">直接接続しますか\?</string>
<string name="connect__a_new_random_profile_will_be_shared">新しいランダムなプロファイルが共有されます。</string>
<string name="delivery">送信</string>
<string name="in_developing_title">近日公開!</string>
<string name="v5_2_disappear_one_message">メッセージを1つ消す</string>
<string name="connect_use_current_profile">現在のプロファイルを使用する</string>
<string name="connect_use_new_incognito_profile">新しいシークレットプロファイルを使用する</string>
<string name="turn_off_system_restriction_button">アプリの設定を開く</string>
<string name="disable_notifications_button">通知を無効にする</string>
<string name="system_restricted_background_warn"><![CDATA[通知を有効にするには、アプリの設定で<b>アプリのバッテリー使用量</b> / <b>制限なし</b> を選択してください。]]></string>
<string name="system_restricted_background_in_call_desc">アプリはバックグラウンドで1分経過すると終了します。</string>
<string name="system_restricted_background_in_call_warn"><![CDATA[バックグラウンドで通話を行うには、アプリの設定で<b>アプリのバッテリー使用量</b> / <b>制限なし</b> を選択してください。]]></string>
<string name="connect__your_profile_will_be_shared">あなたのプロフィール %1$s が共有されます。</string>
<string name="receipts_groups_title_enable">Enable receipts for groups\?</string>
<string name="receipts_groups_title_disable">Disable receipts for groups\?</string>
<string name="receipts_groups_override_enabled">Sending receipts is enabled for %d groups</string>
<string name="receipts_groups_override_disabled">Sending receipts is disabled for %d groups</string>
<string name="send_receipts_disabled">無効</string>
<string name="system_restricted_background_in_call_title">バックグラウンド通話なし</string>
<string name="paste_the_link_you_received_to_connect_with_your_contact">受信したリンクを貼り付け、連絡先に接続する。</string>
<string name="rcv_group_event_2_members_connected">%s と %s は接続中</string>
<string name="system_restricted_background_desc">SimpleXはバックグラウンドでは動作できません。アプリが起動している時のみ通知を受け取ることができます。</string>
<string name="no_info_on_delivery">送信情報なし</string>
<string name="no_selected_chat">チャットが選択されていません</string>
<string name="receipts_section_groups">小グループ最大20名</string>
<string name="receipts_groups_enable_for_all">すべてのグループで有効にする</string>
<string name="recipient_colon_delivery_status">%s: %s</string>
<string name="connect_via_member_address_alert_desc">接続リクエストはこのグループ メンバーに送信されます。</string>
<string name="receipts_groups_disable_for_all">すべてのグループで無効にする</string>
<string name="privacy_message_draft">メッセージの下書き</string>
<string name="privacy_show_last_messages">最新のメッセージを表示</string>
<string name="send_receipts">Send receipts</string>
<string name="rcv_group_event_n_members_connected">%s, %s および %d 人の他のメンバーが接続しています。</string>
<string name="rcv_group_event_3_members_connected">%s, %s と %s は接続中</string>
<string name="in_developing_desc">この機能はまだサポートされていません。次のリリースをお試しください。</string>
<string name="receipts_contacts_enable_keep_overrides">有効にする(設定の優先を維持)</string>
<string name="receipts_groups_disable_keep_overrides">無効にする(グループの設定の優先を維持)</string>
<string name="receipts_groups_enable_keep_overrides">有効にする(グループの設定の優先を維持)</string>
<string name="receipts_contacts_disable_keep_overrides">無効にする(設定の優先を維持)</string>
<string name="v5_2_disappear_one_message_descr">会話中に無効になっている場合でも。</string>
<string name="v5_2_message_delivery_receipts">Message delivery receipts!</string>
<string name="delivery_receipts_title">Delivery receipts!</string>
<string name="sending_delivery_receipts_will_be_enabled_all_profiles">Sending delivery receipts will be enabled for all contacts in all visible chat profiles.</string>
<string name="message_delivery_error_title">Message delivery error</string>
<string name="send_receipts_disabled_alert_title">Receipts are disabled</string>
<string name="settings_section_title_delivery_receipts">SEND DELIVERY RECEIPTS TO</string>
<string name="sending_delivery_receipts_will_be_enabled">Sending delivery receipts will be enabled for all contacts.</string>
<string name="send_receipts_disabled_alert_msg">This group has over %1$d members, delivery receipts are not sent.</string>
<string name="error_enabling_delivery_receipts">Error enabling delivery receipts!</string>
<string name="delivery_receipts_are_disabled">Delivery receipts are disabled!</string>
</resources>

View File

@ -1086,13 +1086,13 @@
<string name="decryption_error">Decodering fout</string>
<string name="alert_text_msg_bad_hash">De hash van het vorige bericht is anders.</string>
<string name="alert_text_decryption_error_too_many_skipped">%1$d berichten overgeslagen.</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Het kan gebeuren wanneer u of uw verbinding de oude databaseback-up gebruikte.</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Het kan gebeuren wanneer u of de ander een oude databaseback-up gebruikt.</string>
<string name="alert_text_fragment_please_report_to_developers">Meld het alsjeblieft aan de ontwikkelaars.</string>
<string name="alert_title_msg_bad_hash">Onjuiste bericht hash</string>
<string name="alert_title_msg_bad_id">Onjuiste bericht-ID</string>
<string name="alert_text_msg_bad_id">De ID van het volgende bericht is onjuist (minder of gelijk aan het vorige).
\nHet kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</string>
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d-berichten konden niet worden ontsleuteld.</string>
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d berichten konden niet worden ontsleuteld.</string>
<string name="no_spaces">Geen spaties!</string>
<string name="stop_rcv_file__message">Het ontvangen van het bestand wordt gestopt.</string>
<string name="revoke_file__confirm">Intrekken</string>
@ -1369,7 +1369,7 @@
<string name="connect_via_member_address_alert_desc">Verzoek voor het verbinden wordt naar dit groepslid verzonden.</string>
<string name="paste_the_link_you_received_to_connect_with_your_contact">Plak de link die je hebt ontvangen om verbinding te maken met je contact…</string>
<string name="system_restricted_background_warn"><![CDATA[Als u meldingen wilt inschakelen, kiest u <b> App-batterijgebruik</b> /<b> Onbeperkt</b> in de app-instellingen.]]></string>
<string name="connect_use_new_incognito_profile">Gebruik een nieuw incognito -profiel</string>
<string name="connect_use_new_incognito_profile">Gebruik een nieuw incognito profiel</string>
<string name="privacy_message_draft">Concept bericht</string>
<string name="rcv_group_event_n_members_connected">%s, %s en %d andere leden verbonden</string>
<string name="privacy_show_last_messages">Laat laatste berichten zien</string>

View File

@ -414,7 +414,7 @@
<string name="initial_member_role">Função inicial</string>
<string name="snd_group_event_group_profile_updated">perfil do grupo atualizado</string>
<string name="group_member_status_group_deleted">Grupo excluído</string>
<string name="incognito_info_protects">O modo de navegação anônima protege a privacidade do nome e da imagem do seu perfil principal — para cada novo contato, um novo perfil aleatório é criado.</string>
<string name="incognito_info_protects">O modo Incognito protege sua privacidade usando um novo perfil aleatório para cada contato.</string>
<string name="group_members_can_delete">Os membros do grupo podem excluir mensagens enviadas de forma irreversível.</string>
<string name="ttl_w">%dsemana</string>
<string name="v4_3_improved_server_configuration">Configuração de servidor aprimorada</string>
@ -631,7 +631,7 @@
<string name="images_limit_desc">Apenas 10 imagens podem ser enviadas ao mesmo tempo</string>
<string name="notifications">Notificações</string>
<string name="text_field_set_contact_placeholder">Definir nome do contato…</string>
<string name="switch_receiving_address_desc">Esse recurso é experimental! Ele só funcionará se o outro cliente tiver a versão 4.2 instalada. Você deve ver a mensagem na conversa assim que a alteração de endereço for concluída verifique se você ainda pode receber mensagens desse contato (ou membro do grupo).</string>
<string name="switch_receiving_address_desc">O endereço de recebimento será alterado para um servidor diferente. A mudança de endereço terminará após o remetente entrar on-line.</string>
<string name="send_verb">Enviar</string>
<string name="reset_verb">Redefinir</string>
<string name="live_message">Mensagem ao vivo!</string>
@ -1327,4 +1327,28 @@
<string name="enable_receipts_all">Ativar</string>
<string name="sending_delivery_receipts_will_be_enabled">Enviar confirmações de entrega serão ativadas para todos os contatos.</string>
<string name="error_enabling_delivery_receipts">Ocorreu um erro ao ativar as confirmações de entrega!</string>
<string name="choose_file_title">Escolher arquivo</string>
<string name="connect_via_link_incognito">Conectar incógnito</string>
<string name="turn_off_battery_optimization_button">Permitir</string>
<string name="disable_notifications_button">Desativar notificações</string>
<string name="system_restricted_background_in_call_title">Sem chamadas de fundo</string>
<string name="turn_off_system_restriction_button">Abrir configurações do aplicativo</string>
<string name="connect__a_new_random_profile_will_be_shared">Um novo perfil aleatório será compartilhado.</string>
<string name="paste_the_link_you_received_to_connect_with_your_contact">Cole o link que você recebeu para se conectar com seu contato..</string>
<string name="receipts_groups_title_disable">Desativar recibos para grupos\?</string>
<string name="receipts_groups_title_enable">Ativar recibos para grupos\?</string>
<string name="receipts_groups_override_disabled">Recibos de entrega estão desativados para %d grupos</string>
<string name="receipts_groups_enable_for_all">Ativar para todos os grupos</string>
<string name="receipts_groups_enable_keep_overrides">Ativar (manter sobreposições do grupo)</string>
<string name="receipts_groups_disable_keep_overrides">Desativar (manter sobreposições do grupo)</string>
<string name="receipts_groups_disable_for_all">Desativar para todos os grupos</string>
<string name="in_developing_title">Em breve!</string>
<string name="delivery">Entrega</string>
<string name="no_info_on_delivery">Nenhuma informação de entrega</string>
<string name="sending_delivery_receipts_will_be_enabled_all_profiles">Enviar recibos de entrega será ativado para todos os contatos em todos os perfis de chat visíveis.</string>
<string name="rcv_group_event_2_members_connected">%s e %s conectados</string>
<string name="connect_via_member_address_alert_title">Conectar diretamente\?</string>
<string name="no_selected_chat">Sem chat selecionado</string>
<string name="privacy_message_draft">Rascunho de mensagem</string>
<string name="send_receipts_disabled">desativado</string>
</resources>

View File

@ -2,8 +2,10 @@ package chat.simplex.common.platform
import androidx.compose.runtime.*
import chat.simplex.common.*
import chat.simplex.common.views.helpers.AlertManager
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
import java.awt.Desktop
import java.io.*
import java.net.URI
@ -19,6 +21,20 @@ actual val agentDatabaseFileName: String = "simplex_v1_agent.db"
actual val databaseExportDir: File = tmpDir
actual fun desktopOpenDatabaseDir() {
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().open(dataDir);
} catch (e: IOException) {
Log.e(TAG, e.stackTraceToString())
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.unknown_error),
text = e.stackTraceToString()
)
}
}
}
@Composable
actual fun rememberFileChooserLauncher(getContent: Boolean, rememberedValue: Any?, onResult: (URI?) -> Unit): FileChooserLauncher =
remember(rememberedValue) { FileChooserLauncher(getContent, onResult) }

View File

@ -0,0 +1,106 @@
package chat.simplex.common.views.database
import SectionItemView
import SectionTextFooter
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.common.ui.theme.WarningOrange
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@Composable
actual fun SavePassphraseSetting(
useKeychain: Boolean,
initialRandomDBPassphrase: Boolean,
storedKey: Boolean,
progressIndicator: Boolean,
minHeight: Dp,
onCheckedChange: (Boolean) -> Unit,
) {
SectionItemView(minHeight = minHeight) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
if (storedKey) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_vpn_key_off_filled),
stringResource(MR.strings.save_passphrase_in_settings),
tint = if (storedKey) WarningOrange else MaterialTheme.colors.secondary
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(MR.strings.save_passphrase_in_settings),
Modifier.padding(end = 24.dp),
color = Color.Unspecified
)
Spacer(Modifier.fillMaxWidth().weight(1f))
DefaultSwitch(
checked = useKeychain,
onCheckedChange = onCheckedChange,
enabled = !initialRandomDBPassphrase && !progressIndicator
)
}
}
}
@Composable
actual fun DatabaseEncryptionFooter(
useKeychain: MutableState<Boolean>,
chatDbEncrypted: Boolean?,
storedKey: MutableState<Boolean>,
initialRandomDBPassphrase: MutableState<Boolean>,
) {
if (chatDbEncrypted == false) {
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
} else if (useKeychain.value) {
if (storedKey.value) {
SectionTextFooter(generalGetString(MR.strings.settings_is_storing_in_clear_text))
if (initialRandomDBPassphrase.value) {
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
} else {
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
}
} else {
SectionTextFooter(generalGetString(MR.strings.passphrase_will_be_saved_in_settings))
}
} else {
SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time))
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
}
}
actual fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.encrypt_database_question),
text = generalGetString(MR.strings.database_will_be_encrypted_and_passphrase_stored_in_settings) + "\n" + storeSecurelySaved(),
confirmText = generalGetString(MR.strings.encrypt_database),
onConfirm = onConfirm,
destructive = true,
)
}
actual fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.change_database_passphrase_question),
text = generalGetString(MR.strings.database_encryption_will_be_updated_in_settings) + "\n" + storeSecurelySaved(),
confirmText = generalGetString(MR.strings.update_database),
onConfirm = onConfirm,
destructive = false,
)
}
actual fun removePassphraseAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.remove_passphrase_from_settings),
text = storeSecurelyDanger(),
confirmText = generalGetString(MR.strings.remove_passphrase),
onConfirm = onConfirm,
destructive = true,
)
}

View File

@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 4c0b8a31d20870a23e120e243359901d8240f922
tag: 980e5c4d1ec15f44290542fd2a5d1c08456f00d1
source-repository-package
type: git

View File

@ -0,0 +1,64 @@
# Encrpting local app files
## Problem
Currently, the files are stored in the file storage unencrypted, unlike the database.
There are multiple operations in the app that access files:
1. Sending files via SMP - chat core reads the files chunk by chunk and sends them. The file can be encrypted once sent and the "encrypted" flag added.
2. Sending files via XFTP - simplexmq encrypts the file first and then sends it. Currently, we are deleting the file from chat, once its uploaded, there is no reason to keep unencrypted file (from XFTP point of view) once its encrypted.
3. Viewing images in the mobile apps.
4. Playing voice files in the mobile apps.
5. Playing videos and showing video previews in mobile apps.
6. Saving files from the app storage to the device.
## Possible solutions
### System encryption
A possible approach is to use platform-specific encryption mechanism. The problem with that approach is inconsistency between platforms, and that the files in chat archive will probably be unencrypted in this case.
### App encryption
Files will be encrypted once received, using storage key, and the core would expose C apis to mobile apps:
1. Read the file with decryption - this can be used for image previews, for example, as a replacement for OS file reading.
2. Copy the file with decryption to some permanent destination - this can be used for saving files to the device.
3. Copy the file into a temporary location with decryption - this can be used for playing voice/video files. The app would remove the files once no longer used, and this temporary location can be cleaned on each app start, to clean up the files that the app failed to remove. Alternative to that would be to have both encrypted and decrypted copies available for the file, with paths stored in the database, and clean up process removed decrypted copies once no longer used - there should be some flags to indicate when decrypted copy can be deleted.
For specific use cases:
1. Viewing images in the mobile apps.
- iOS: we use `UIImage(contentsOfFile path: String)`. We could use `init?(data: Data)` instead, and decrypt the file in memory before passing it to the image view. Images are small enough for this approach to be ok, and in any case the image is read to memory as a whole.
- Android: we use `BitmapFactory.decodeFileDescriptor` (?). We could use ...
2. Playing voice files in the mobile apps.
- iOS: we use `AVAudioPlayer.init(contentsOf: URL)` to play the file. We could either decrypt the file before playing it, or, given that voice files are small (even if we increase allowed duration, they are still likely to be under 1mb), we could use `init(data: Data)` to avoid creating decrypted file.
- Android: we use `MediaPlayer.setDataSource(filePath)`. We could use ...
3. Showing video previews.
- iOS: ...
- Android: ...
Possibly, we will need to store preview as a separate file, to avoid decrypting the whole video just to show preview.
4. Playing video files.
- iOS: we use `AVPlayer(url: URL)`, the file will have to be decrypted for playback.
- Android: ...
5. Saving files from the app storage to the device.
The file will have to be decrypted, passed to the system, and then decrypted copy deleted once no longer needed.
### Which key to use for encryption
1. Derive file encryption key from database storage key. The downside for this approach is managing key changes - they will be slow. Also, if file encryption is made optional, and in any case, for the existing users all files are not encrypted yet, we will need somehow to track which files are encrypted.
2. Random per-file encryption key stored in the database. Given that the database is already encrypted, it can be a better approach, and it makes it easier to manage file encryption/decryption. File keys will not be sent to the client application, but they will be accessible via the database queries of course.

View File

@ -0,0 +1,112 @@
# Groups improvements
See also:
- [Group contacts management](./2022-10-19-group-contacts-management.md).
- [Create groups without establishing direct connections](./2023-08-10-groups-wt-contacts.md).
## Problem
Establishing connections in groups is unstable and uses a lot of traffic. There are several areas for improvement that that could help optimize it:
- Joining group member prematurely creates direct and group connections for each member.
Some members may never come online, and that traffic would be completely wasted.
Instead of creating direct connections, we could allow to send direct messages inside group, and optionally have a separate protocol for automating establishing direct connection with member via them.
- Host sends N introduction messages (XGrpMemIntro) to joining member. Instead they could be batched.
## Possible solutions
### Improved group handshake protocol
Below are proposed changes to group handshake protocol to reduce traffic and improve stability.
Each joining member creates a new temporary per group address for introduced members to connect via. Joining member sends it to host when accepting group invitation.
``` haskell
XGrpAcptAddress :: MemberId -> ConnReqContact -> ChatMsgEvent 'Json
```
Host sends group introductions in batches, batching smaller messages first (introductions of members without profile picture).
For each received batch of N introductions joining member creates N transient per member identifiers (MemberCodes) and replies to host with batched XGrpMemInv messages including these identifiers. Joining member would then use them to verify contact requests from introduced members.
How is MemberCode different from MemberId? - MemberId is known to all group members and is constant per member per group. MemberCode would be known only to host and to introduced member (of existing members), so other members wouldn't be able to impersonate one another when requesting connection with joining member. An introduced member can still pass their identifier + joining member address to another member or outside of group, but it is no different to passing currently shared invitation links.
```haskell
newtype MemberCode = MemberCode {unMemberCode :: ByteString}
XGrpMemInvCode :: MemberId -> MemberCode -> ChatMsgEvent 'Json
-- instead of / in addition to batching message could be
type MemberCodes = Map MemberId MemberCode
XGrpMemInvCodes :: MemberCodes -> ChatMsgEvent 'Json
```
Host includes joining member address and code (unique for each introduced member) into XGrpMemFwd messages instead of invitation links:
```haskell
XGrpMemFwdCode :: MemberInfo -> ConnReqContact -> MemberCode -> ChatMsgEvent 'Json
```
Introduced members send contact requests with a new message XGroupMember / XIntroduced (similar to XInfo or XContact, see `processUserContactRequest`):
```haskell
XIntroduced :: MemberInfo -> MemberCode -> ChatMsgEvent 'Json
```
Joinee verifies profile and code and automatically accepts contact request. They both assign resulting connection to respective group member record, without creating contact.
After (if) all introduced members have connected, joining member deletes per group address. Possibly it can also be deleted after expiration interval.
#### Group links
We can reduce number of steps taken to join group via group link:
- Do not create direct connection and contact with group link host, instead use the connection resulting from contact request as a group connection, and assign it to a group member record.
- Host to not send XGrpInv message, joining member to not wait for it, instead joining member would initiate with XGrpAcptAddress after establishing connection via group link.
In addition to their profile, host includes MemberId of joining member into confirmation when accepting group link join request, using new message:
```haskell
XGroupLinkInfo :: Profile -> MemberId -> ChatMsgEvent 'Json
```
Joining member initially doesn't know group profile, they create a placeholder group with a new dummy profile (alternatively, we could include at least group display name into group link). After connection is established, host sends XGrpInfo containing group profile to joining member. This can happen in parallel with group handshake started by XGrpAcptAddress.
Group profile could also be included into XGroupLinkInfo if not for the limitation on size if both host's profile and group profile contain pictures.
![Adding member to the group](./diagrams/2023-08-28-groups-improvements.svg)
#### Clients compatibility
We have a [proposed mechanism](https://github.com/simplex-chat/simplex-chat/pull/2886) for communicating "chat protocol version" between clients.
Sending and processing new protocol messages would only be supported by updated clients.
Trying to support both protocols across different members in the same group would require complex logic:
Host would have to send introduced members versions, joining member would provide both address or invitation links depending on each members' versions, host would forward accordingly.
Instead we could assign "chat protocol version" per group and share it with members as part of group profile, and make a two-stage release when members would first be able to update and get new processing logic, but have it disabled until next release.
After group switching to new processing logic old clients wouldn't be able to connect in groups.
How should existing groups be switched?
- Owner user action?
- Owner client deciding automatically?
- In case group has multiple owners - which owner(s) can / should decide?
- Prohibited until all / part of existing members don't update? How to request members to update?
- Old clients will not be able to process and save group chat version from group profile update.
### Sending direct messages inside group
Group messages are sent by broadcasting them to all group member connections. As a replacement for creating additional direct connections in group we can allow to send message directly to members via group member connections. The UX would be to choose whether to send to group or to a specific member via compose view.
Possible approach is to extend ExtMsgContent with `direct :: Maybe Bool` field, which would only be considered for group messages.
Chat items should store information of receiving member database ID (for sending member) and of message being direct (for receiving member). Perhaps it could be a single field `direct_member_id`, which would be the same as `group_member_id` for received messages.
TODO - consider whether `connection_id` or `group_id` or both should be assigned in `messages` table.

View File

@ -0,0 +1,40 @@
sequenceDiagram
participant M as N existing<br>members
participant A as Alice
participant B as Bob
note over A, B: 1. send and accept group invitation /<br>join via group link
alt host invites contact
A ->> B: x.grp.inv<br>invite Bob to group<br>(via contact connection)
else user joins via group link
B ->> A: request to join group via link
A ->> B: auto-accept<br>x.group.link.info with host's profile<br>and joining member MemberId<br>establish group member connection
A ->> B: x.grp.info<br>group profile
end
note right of B: when joining via group link<br>Bob doesn't wait for x.grp.info<br>and initiates group handshake<br>with x.grp.acpt.address<br>after establishing connection
note over B: create per group address
B ->> A: x.grp.acpt.address<br>accept invitation<br>and send address to connect<br>(via member connection)
B ->> A: establish group member connection
note over M, B: 2. introduce new member Bob to all existing members
A ->> M: x.grp.mem.new<br>"announce" Bob<br>to existing members<br>(via member connections)
loop batched
A ->> B: x.grp.mem.intro * N<br>"introduce" members<br>(via member connection)
note over B: create N MemberCodes
B ->> A: x.grp.mem.inv.code<br>unique MemberCodes<br>for all members<br>(via member connection)
end
A ->> M: x.grp.mem.fwd.code<br>forward address<br>and unique MemberCodes<br>to all members<br>(via member connections)
note over M, B: 3. establish group member connection
M ->> B: request group member connection<br>x.introduced with MemberCode
B ->> M: verify MemberCode, auto-accept
note over M, B: no contact deduplication
opt all introduced members connected / expiration
note over B: delete per group address
end

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."4c0b8a31d20870a23e120e243359901d8240f922" = "0lrgfm8di0x4rmidqp7k2fw29yaal6467nmb85lwk95yz602906z";
"https://github.com/simplex-chat/simplexmq.git"."980e5c4d1ec15f44290542fd2a5d1c08456f00d1" = "1lqciyy215dvmbhykyp80bwipqmxybv39p6jff6vjgd5r34958nh";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb";
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";

View File

@ -1,6 +1,6 @@
cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.35.0.
-- This file has been generated from package.yaml by hpack version 0.35.2.
--
-- see: https://github.com/sol/hpack
@ -10,7 +10,7 @@ category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat
maintainer: chat@simplex.chat
copyright: 2020-23 simplex.chat
copyright: 2020-22 simplex.chat
license: AGPL-3
license-file: LICENSE
build-type: Simple
@ -108,8 +108,11 @@ library
Simplex.Chat.Migrations.M20230705_delivery_receipts
Simplex.Chat.Migrations.M20230721_group_snd_item_statuses
Simplex.Chat.Migrations.M20230814_indexes
Simplex.Chat.Migrations.M20230827_file_encryption
Simplex.Chat.Migrations.M20230829_connections_chat_vrange
Simplex.Chat.Mobile
Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared
Simplex.Chat.Mobile.WebRTC
Simplex.Chat.Options
Simplex.Chat.ProfileGenerator

View File

@ -1,4 +1,3 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
@ -26,7 +25,7 @@ import Crypto.Random (drgNew)
import qualified Data.Aeson as J
import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor (bimap, first, second)
import Data.Bifunctor (bimap, first)
import qualified Data.ByteString.Base64 as B64
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
@ -42,7 +41,6 @@ import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList)
import qualified Data.Set as S
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
@ -86,6 +84,8 @@ import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
import Simplex.Messaging.Client (defaultNetworkConfig)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import qualified Simplex.Messaging.Crypto.File as CF
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (base64P)
@ -564,8 +564,9 @@ processChatCommand = \case
SendFileSMP fileInline -> smpSndFileTransfer file fileSize fileInline
SendFileXFTP -> xftpSndFileTransfer user file fileSize 1 $ CGContact ct
where
smpSndFileTransfer :: FilePath -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
smpSndFileTransfer file fileSize fileInline = do
smpSndFileTransfer :: CryptoFile -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
smpSndFileTransfer (CryptoFile _ (Just _)) _ _ = throwChatError $ CEFileInternal "locally encrypted files can't be sent via SMP" -- can only happen if XFTP is disabled
smpSndFileTransfer (CryptoFile file Nothing) fileSize fileInline = do
(agentConnId_, fileConnReq) <-
if isJust fileInline
then pure (Nothing, Nothing)
@ -578,7 +579,8 @@ processChatCommand = \case
fileStatus <- case fileInline of
Just IFMSent -> createSndDirectInlineFT db ct ft $> CIFSSndTransfer 0 1
_ -> pure CIFSSndStored
let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus, fileProtocol = FPSMP}
let fileSource = Just $ CF.plain file
ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol = FPSMP}
pure (fileInvitation, ciFile, ft)
prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> m (MsgContainer, Maybe (CIQuote 'CTDirect))
prepareMsg fInv_ timed_ = case quotedItemId_ of
@ -627,15 +629,17 @@ processChatCommand = \case
SendFileSMP fileInline -> smpSndFileTransfer file fileSize fileInline
SendFileXFTP -> xftpSndFileTransfer user file fileSize n $ CGGroup g
where
smpSndFileTransfer :: FilePath -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
smpSndFileTransfer file fileSize fileInline = do
smpSndFileTransfer :: CryptoFile -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
smpSndFileTransfer (CryptoFile _ (Just _)) _ _ = throwChatError $ CEFileInternal "locally encrypted files can't be sent via SMP" -- can only happen if XFTP is disabled
smpSndFileTransfer (CryptoFile file Nothing) fileSize fileInline = do
let fileName = takeFileName file
fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq = Nothing, fileInline, fileDescr = Nothing}
fileStatus = if fileInline == Just IFMSent then CIFSSndTransfer 0 1 else CIFSSndStored
chSize <- asks $ fileChunkSize . config
withStore' $ \db -> do
ft@FileTransferMeta {fileId} <- createSndGroupFileTransfer db userId gInfo file fileInvitation chSize
let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus, fileProtocol = FPSMP}
let fileSource = Just $ CF.plain file
ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol = FPSMP}
pure (fileInvitation, ciFile, ft)
sendGroupFileInline :: [GroupMember] -> SharedMsgId -> FileTransferMeta -> m ()
sendGroupFileInline ms sharedMsgId ft@FileTransferMeta {fileInline} =
@ -690,17 +694,19 @@ processChatCommand = \case
qText = msgContentText qmc
qFileName = maybe qText (T.pack . (fileName :: CIFile d -> String)) ciFile_
qTextOrFile = if T.null qText then qFileName else qText
xftpSndFileTransfer :: User -> FilePath -> Integer -> Int -> ContactOrGroup -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
xftpSndFileTransfer user file fileSize n contactOrGroup = do
let fileName = takeFileName file
xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
xftpSndFileTransfer user file@(CryptoFile filePath cfArgs) fileSize n contactOrGroup = do
let fileName = takeFileName filePath
fileDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False}
fInv = xftpFileInvitation fileName fileSize fileDescr
fsFilePath <- toFSFilePath file
aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) fsFilePath (roundedFDCount n)
fsFilePath <- toFSFilePath filePath
let srcFile = CryptoFile fsFilePath cfArgs
aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) srcFile (roundedFDCount n)
-- TODO CRSndFileStart event for XFTP
chSize <- asks $ fileChunkSize . config
ft@FileTransferMeta {fileId} <- withStore' $ \db -> createSndFileTransferXFTP db user contactOrGroup file fInv (AgentSndFileId aFileId) chSize
let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored, fileProtocol = FPXFTP}
let fileSource = Just $ CryptoFile filePath cfArgs
ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus = CIFSSndStored, fileProtocol = FPXFTP}
case contactOrGroup of
CGContact Contact {activeConn} -> withStore' $ \db -> createSndFTDescrXFTP db user Nothing activeConn ft fileDescr
CGGroup (Group _ ms) -> forM_ ms $ \m -> saveMemberFD m `catchChatError` (toView . CRChatError (Just user))
@ -1621,26 +1627,40 @@ processChatCommand = \case
asks showLiveItems >>= atomically . (`writeTVar` on) >> ok_
SendFile chatName f -> withUser $ \user -> do
chatRef <- getChatRef user chatName
processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage (Just f) Nothing (MCFile "")
processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage (Just $ CF.plain f) Nothing (MCFile "")
SendImage chatName f -> withUser $ \user -> do
chatRef <- getChatRef user chatName
filePath <- toFSFilePath f
unless (any ((`isSuffixOf` map toLower f)) imageExtensions) $ throwChatError CEFileImageType {filePath}
unless (any (`isSuffixOf` map toLower f) imageExtensions) $ throwChatError CEFileImageType {filePath}
fileSize <- getFileSize filePath
unless (fileSize <= maxImageSize) $ throwChatError CEFileImageSize {filePath}
-- TODO include file description for preview
processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage (Just f) Nothing (MCImage "" fixedImagePreview)
processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage (Just $ CF.plain f) Nothing (MCImage "" fixedImagePreview)
ForwardFile chatName fileId -> forwardFile chatName fileId SendFile
ForwardImage chatName fileId -> forwardFile chatName fileId SendImage
SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO"
ReceiveFile fileId rcvInline_ filePath_ -> withUser $ \_ ->
ReceiveFile fileId encrypted rcvInline_ filePath_ -> withUser $ \_ ->
withChatLock "receiveFile" . procCmd $ do
(user, ft) <- withStore $ \db -> getRcvFileTransferById db fileId
receiveFile' user ft rcvInline_ filePath_
SetFileToReceive fileId -> withUser $ \_ -> do
(user, ft) <- withStore (`getRcvFileTransferById` fileId)
ft' <- if encrypted then encryptLocalFile ft else pure ft
receiveFile' user ft' rcvInline_ filePath_
where
encryptLocalFile ft@RcvFileTransfer {xftpRcvFile} = case xftpRcvFile of
Nothing -> throwChatError $ CEFileInternal "locally encrypted files can't be received via SMP"
Just f -> do
cfArgs <- liftIO $ CF.randomArgs
withStore' $ \db -> setFileCryptoArgs db fileId cfArgs
pure ft {xftpRcvFile = Just ((f :: XFTPRcvFile) {cryptoArgs = Just cfArgs})}
SetFileToReceive fileId encrypted -> withUser $ \_ -> do
withChatLock "setFileToReceive" . procCmd $ do
withStore' (`setRcvFileToReceive` fileId)
cfArgs <- if encrypted then fileCryptoArgs else pure Nothing
withStore' $ \db -> setRcvFileToReceive db fileId cfArgs
ok_
where
fileCryptoArgs = do
(_, RcvFileTransfer {xftpRcvFile = f}) <- withStore (`getRcvFileTransferById` fileId)
unless (isJust f) $ throwChatError $ CEFileInternal "locally encrypted files can't be received via SMP"
liftIO $ Just <$> CF.randomArgs
CancelFile fileId -> withUser $ \user@User {userId} ->
withChatLock "cancelFile" . procCmd $
withStore (\db -> getFileTransfer db user fileId) >>= \case
@ -1744,17 +1764,17 @@ processChatCommand = \case
ResetAgentStats -> withAgent resetAgentStats >> ok_
GetAgentSubs -> summary <$> withAgent getAgentSubscriptions
where
summary SubscriptionsInfo {activeSubscriptions, pendingSubscriptions} =
CRAgentSubs {activeSubs, distinctActiveSubs, pendingSubs, distinctPendingSubs}
summary SubscriptionsInfo {activeSubscriptions, pendingSubscriptions, removedSubscriptions} =
CRAgentSubs
{ activeSubs = foldl' countSubs M.empty activeSubscriptions,
pendingSubs = foldl' countSubs M.empty pendingSubscriptions,
removedSubs = foldl' accSubErrors M.empty removedSubscriptions
}
where
(activeSubs, distinctActiveSubs) = foldSubs activeSubscriptions
(pendingSubs, distinctPendingSubs) = foldSubs pendingSubscriptions
foldSubs :: [SubInfo] -> (Map Text Int, Map Text Int)
foldSubs = second (M.map S.size) . foldl' acc (M.empty, M.empty)
acc (m, m') SubInfo {server, rcvId} =
( M.alter (Just . maybe 1 (+ 1)) server m,
M.alter (Just . maybe (S.singleton rcvId) (S.insert rcvId)) server m'
)
countSubs m SubInfo {server} = M.alter (Just . maybe 1 (+ 1)) server m
accSubErrors m = \case
SubInfo {server, subError = Just e} -> M.alter (Just . maybe [e] (e :)) server m
_ -> m
GetAgentSubsDetails -> CRAgentSubsDetails <$> withAgent getAgentSubscriptions
where
withChatLock name action = asks chatLock >>= \l -> withLock l name action
@ -1838,18 +1858,19 @@ processChatCommand = \case
contactMember Contact {contactId} =
find $ \GroupMember {memberContactId = cId, memberStatus = s} ->
cId == Just contactId && s /= GSMemRemoved && s /= GSMemLeft
checkSndFile :: MsgContent -> FilePath -> Integer -> m (Integer, SendFileMode)
checkSndFile mc f n = do
checkSndFile :: MsgContent -> CryptoFile -> Integer -> m (Integer, SendFileMode)
checkSndFile mc (CryptoFile f cfArgs) n = do
fsFilePath <- toFSFilePath f
unlessM (doesFileExist fsFilePath) . throwChatError $ CEFileNotFound f
ChatConfig {fileChunkSize, inlineFiles} <- asks config
xftpCfg <- readTVarIO =<< asks userXFTPFileConfig
fileSize <- getFileSize fsFilePath
fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cfArgs
when (fromInteger fileSize > maxFileSize) $ throwChatError $ CEFileSize f
let chunks = - ((- fileSize) `div` fileChunkSize)
let chunks = -((-fileSize) `div` fileChunkSize)
fileInline = inlineFileMode mc inlineFiles chunks n
fileMode = case xftpCfg of
Just cfg
| isJust cfArgs -> SendFileXFTP
| fileInline == Just IFMSent || fileSize < minFileSize cfg || n <= 0 -> SendFileSMP fileInline
| otherwise -> SendFileXFTP
_ -> SendFileSMP fileInline
@ -1876,7 +1897,7 @@ processChatCommand = \case
summary <- foldM (processAndCount user' logLevel) (UserProfileUpdateSummary 0 0 0 []) contacts
pure $ CRUserProfileUpdated user' (fromLocalProfile p) p' summary
where
processAndCount user' ll (!s@UserProfileUpdateSummary {notChanged, updateSuccesses, updateFailures, changedContacts = cts}) ct = do
processAndCount user' ll s@UserProfileUpdateSummary {notChanged, updateSuccesses, updateFailures, changedContacts = cts} ct = do
let mergedProfile = userProfileToSend user Nothing $ Just ct
ct' = updateMergedPreferences user' ct
mergedProfile' = userProfileToSend user' Nothing $ Just ct'
@ -2223,7 +2244,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
filePath <- getRcvFilePath fileId filePath_ fName True
withStoreCtx (Just "acceptFileReceive, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db user fileId connIds ConnJoined filePath
-- XFTP
(Just _xftpRcvFile, _) -> do
(Just XFTPRcvFile {cryptoArgs}, _) -> do
filePath <- getRcvFilePath fileId filePath_ fName False
(ci, rfd) <- withStoreCtx (Just "acceptFileReceive, xftpAcceptRcvFT ...") $ \db -> do
-- marking file as accepted and reading description in the same transaction
@ -2231,7 +2252,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
ci <- xftpAcceptRcvFT db user fileId filePath
rfd <- getRcvFileDescrByFileId db fileId
pure (ci, rfd)
receiveViaCompleteFD user fileId rfd
receiveViaCompleteFD user fileId rfd cryptoArgs
pure ci
-- group & direct file protocol
_ -> do
@ -2274,11 +2295,11 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
|| (rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks)
)
receiveViaCompleteFD :: ChatMonad m => User -> FileTransferId -> RcvFileDescr -> m ()
receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} =
receiveViaCompleteFD :: ChatMonad m => User -> FileTransferId -> RcvFileDescr -> Maybe CryptoFileArgs -> m ()
receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} cfArgs =
when fileDescrComplete $ do
rd <- parseFileDescription fileDescrText
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs
startReceivingFile user fileId
withStoreCtx' (Just "receiveViaCompleteFD, updateRcvFileAgentId") $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId)
@ -2545,7 +2566,7 @@ cleanupManager = do
`catchChatError` (toView . CRChatError (Just user))
cleanupMessages = do
ts <- liftIO getCurrentTime
let cutoffTs = addUTCTime (- (30 * nominalDay)) ts
let cutoffTs = addUTCTime (-(30 * nominalDay)) ts
withStoreCtx' (Just "cleanupManager, deleteOldMessages") (`deleteOldMessages` cutoffTs)
startProximateTimedItemThread :: ChatMonad m => User -> (ChatRef, ChatItemId) -> UTCTime -> m ()
@ -3597,14 +3618,14 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
processFDMessage fileId fileDescr = do
ft <- withStore $ \db -> getRcvFileTransfer db user fileId
unless (rcvFileCompleteOrCancelled ft) $ do
(rfd, RcvFileTransfer {fileStatus}) <- withStore $ \db -> do
(rfd, RcvFileTransfer {fileStatus, xftpRcvFile}) <- withStore $ \db -> do
rfd <- appendRcvFD db userId fileId fileDescr
-- reading second time in the same transaction as appending description
-- to prevent race condition with accept
ft' <- getRcvFileTransfer db user fileId
pure (rfd, ft')
case fileStatus of
RFSAccepted _ -> receiveViaCompleteFD user fileId rfd
case (fileStatus, xftpRcvFile) of
(RFSAccepted _, Just XFTPRcvFile {cryptoArgs}) -> receiveViaCompleteFD user fileId rfd cryptoArgs
_ -> pure ()
cancelMessageFile :: Contact -> SharedMsgId -> MsgMeta -> m ()
@ -3630,7 +3651,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
withStore' $ \db -> startRcvInlineFT db user ft fPath inline
pure (Just fPath, CIFSRcvAccepted)
_ -> pure (Nothing, CIFSRcvInvitation)
pure (ft, CIFile {fileId, fileName, fileSize, filePath, fileStatus, fileProtocol})
let fileSource = CF.plain <$> filePath
pure (ft, CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol})
messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m ()
messageUpdate ct@Contact {contactId, localDisplayName = c} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do
@ -3847,7 +3869,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
inline <- receiveInlineMode fInv Nothing fileChunkSize
RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol}
ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol}
ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ msgMeta (CIRcvMsgContent $ MCFile "") ciFile Nothing False
toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci)
whenContactNtfs user ct $ do
@ -3861,7 +3883,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
inline <- receiveInlineMode fInv Nothing fileChunkSize
RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvGroupFileTransfer db userId m fInv inline fileChunkSize
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol}
ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol}
ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ msgMeta (CIRcvMsgContent $ MCFile "") ciFile Nothing False
groupMsgToView gInfo m ci msgMeta
let g = groupName' gInfo
@ -4796,10 +4818,9 @@ deleteGroupCI user gInfo ci@(CChatItem msgDir deletedItem@ChatItem {file}) byUse
pure $ CRChatItemDeleted user (AChatItem SCTGroup msgDir (GroupChat gInfo) deletedItem) toCi byUser timed
deleteCIFile :: (ChatMonad m, MsgDirectionI d) => User -> Maybe (CIFile d) -> m ()
deleteCIFile user file =
forM_ file $ \CIFile {fileId, filePath, fileStatus} -> do
let fileInfo = CIFileInfo {fileId, fileStatus = Just $ AFS msgDirection fileStatus, filePath}
fileAgentConnIds <- deleteFile' user fileInfo True
deleteCIFile user file_ =
forM_ file_ $ \file -> do
fileAgentConnIds <- deleteFile' user (mkCIFileInfo file) True
deleteAgentConnectionsAsync user fileAgentConnIds
markDirectCIDeleted :: ChatMonad m => User -> Contact -> CChatItem 'CTDirect -> MessageId -> Bool -> UTCTime -> m ChatResponse
@ -4823,10 +4844,9 @@ markGroupCIDeleted user gInfo@GroupInfo {groupId} ci@(CChatItem _ ChatItem {file
gItem (CChatItem msgDir ci') = AChatItem SCTGroup msgDir (GroupChat gInfo) ci'
cancelCIFile :: (ChatMonad m, MsgDirectionI d) => User -> Maybe (CIFile d) -> m ()
cancelCIFile user file =
forM_ file $ \CIFile {fileId, filePath, fileStatus} -> do
let fileInfo = CIFileInfo {fileId, fileStatus = Just $ AFS msgDirection fileStatus, filePath}
fileAgentConnIds <- cancelFile' user fileInfo True
cancelCIFile user file_ =
forM_ file_ $ \file -> do
fileAgentConnIds <- cancelFile' user (mkCIFileInfo file) True
deleteAgentConnectionsAsync user fileAgentConnIds
createAgentConnectionAsync :: forall m c. (ChatMonad m, ConnectionModeI c) => User -> CommandFunction -> Bool -> SConnectionMode c -> m (CommandId, ConnId)
@ -5061,7 +5081,7 @@ withAgent :: ChatMonad m => (AgentClient -> ExceptT AgentErrorType m a) -> m a
withAgent action =
asks smpAgent
>>= runExceptT . action
>>= liftEither . first (\e -> ChatErrorAgent e Nothing)
>>= liftEither . first (`ChatErrorAgent` Nothing)
withStore' :: ChatMonad m => (DB.Connection -> IO a) -> m a
withStore' action = withStore $ liftIO . action
@ -5296,8 +5316,8 @@ chatCommandP =
("/fforward " <|> "/ff ") *> (ForwardFile <$> chatNameP' <* A.space <*> A.decimal),
("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal),
("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath),
("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)),
"/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal),
("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> (" encrypt=" *> onOffP <|> pure False) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)),
"/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> (" encrypt=" *> onOffP <|> pure False)),
("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal),
("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal),
"/simplex" *> (ConnectSimplex <$> incognitoP),

View File

@ -66,7 +66,7 @@ sendComposedMessage cc = sendComposedMessage' cc . contactId'
sendComposedMessage' :: ChatController -> ContactId -> Maybe ChatItemId -> MsgContent -> IO ()
sendComposedMessage' cc ctId quotedItemId msgContent = do
let cm = ComposedMessage {filePath = Nothing, quotedItemId, msgContent}
let cm = ComposedMessage {fileSource = Nothing, quotedItemId, msgContent}
sendChatCmd cc (APISendMessage (ChatRef CTDirect ctId) False Nothing cm) >>= \case
CRNewChatItem {} -> printLog cc CLLInfo $ "sent message to contact ID " <> show ctId
r -> putStrLn $ "unexpected send message response: " <> show r

View File

@ -22,8 +22,9 @@ import Control.Monad.Except
import Control.Monad.IO.Unlift
import Control.Monad.Reader
import Crypto.Random (ChaChaDRG)
import Data.Aeson (FromJSON (..), ToJSON (..))
import Data.Aeson (FromJSON (..), ToJSON (..), (.:), (.:?))
import qualified Data.Aeson as J
import qualified Data.Aeson.Types as JT
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
@ -54,16 +55,18 @@ import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig)
import Simplex.Messaging.Agent.Lock
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration)
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..))
import qualified Simplex.Messaging.Crypto.File as CF
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus)
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON)
import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType, CorrId, MsgFlags, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SProtocolType, UserProtocol, XFTPServerWithAuth)
import Simplex.Messaging.TMap (TMap)
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
import Simplex.Messaging.Transport (simplexMQVersion)
import Simplex.Messaging.Transport.Client (TransportHost)
import Simplex.Messaging.Util (allFinally, catchAllErrors, tryAllErrors)
import Simplex.Messaging.Util (allFinally, catchAllErrors, tryAllErrors, (<$$>))
import Simplex.Messaging.Version
import System.IO (Handle)
import System.Mem.Weak (Weak)
@ -389,8 +392,8 @@ data ChatCommand
| ForwardFile ChatName FileTransferId
| ForwardImage ChatName FileTransferId
| SendFileDescription ChatName FilePath
| ReceiveFile {fileId :: FileTransferId, fileInline :: Maybe Bool, filePath :: Maybe FilePath}
| SetFileToReceive FileTransferId
| ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath}
| SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Bool}
| CancelFile FileTransferId
| FileStatus FileTransferId
| ShowProfile -- UserId (not used in UI)
@ -572,7 +575,7 @@ data ChatResponse
| CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]}
| CRDebugLocks {chatLockName :: Maybe String, agentLocks :: AgentLocks}
| CRAgentStats {agentStats :: [[String]]}
| CRAgentSubs {activeSubs :: Map Text Int, distinctActiveSubs :: Map Text Int, pendingSubs :: Map Text Int, distinctPendingSubs :: Map Text Int}
| CRAgentSubs {activeSubs :: Map Text Int, pendingSubs :: Map Text Int, removedSubs :: Map Text [String]}
| CRAgentSubsDetails {agentSubs :: SubscriptionsInfo}
| CRConnectionDisabled {connectionEntity :: ConnectionEntity}
| CRAgentRcvQueueDeleted {agentConnId :: AgentConnId, server :: SMPServer, agentQueueId :: AgentQueueId, agentError_ :: Maybe AgentErrorType}
@ -725,11 +728,24 @@ data UserProfileUpdateSummary = UserProfileUpdateSummary
instance ToJSON UserProfileUpdateSummary where toEncoding = J.genericToEncoding J.defaultOptions
data ComposedMessage = ComposedMessage
{ filePath :: Maybe FilePath,
{ fileSource :: Maybe CryptoFile,
quotedItemId :: Maybe ChatItemId,
msgContent :: MsgContent
}
deriving (Show, Generic, FromJSON)
deriving (Show, Generic)
-- This instance is needed for backward compatibility, can be removed in v6.0
instance FromJSON ComposedMessage where
parseJSON (J.Object v) = do
fileSource <-
(v .:? "fileSource") >>= \case
Nothing -> CF.plain <$$> (v .:? "filePath")
f -> pure f
quotedItemId <- v .:? "quotedItemId"
msgContent <- v .: "msgContent"
pure ComposedMessage {fileSource, quotedItemId, msgContent}
parseJSON invalid =
JT.prependFailure "bad ComposedMessage, " (JT.typeMismatch "Object" invalid)
instance ToJSON ComposedMessage where
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}

View File

@ -37,6 +37,8 @@ import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgMeta (..), MsgReceiptStatus (..))
import Simplex.Messaging.Crypto.File (CryptoFile (..))
import qualified Simplex.Messaging.Crypto.File as CF
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, parseAll, sumTypeJSON)
import Simplex.Messaging.Protocol (MsgBody)
@ -459,7 +461,7 @@ data CIFile (d :: MsgDirection) = CIFile
{ fileId :: Int64,
fileName :: String,
fileSize :: Integer,
filePath :: Maybe FilePath, -- local file path
fileSource :: Maybe CryptoFile, -- local file path with optional key and nonce
fileStatus :: CIFileStatus d,
fileProtocol :: FileProtocol
}
@ -631,6 +633,14 @@ data CIFileInfo = CIFileInfo
}
deriving (Show)
mkCIFileInfo :: MsgDirectionI d => CIFile d -> CIFileInfo
mkCIFileInfo CIFile {fileId, fileStatus, fileSource} =
CIFileInfo
{ fileId,
fileStatus = Just $ AFS msgDirection fileStatus,
filePath = CF.filePath <$> fileSource
}
data CIStatus (d :: MsgDirection) where
CISSndNew :: CIStatus 'MDSnd
CISSndSent :: SndCIStatusProgress -> CIStatus 'MDSnd

View File

@ -50,7 +50,7 @@ instance FromField AMsgDirection where fromField = fromIntField_ $ fmap fromMsgD
instance ToField MsgDirection where toField = toField . msgDirectionInt
fromIntField_ :: (Typeable a) => (Int64 -> Maybe a) -> Field -> Ok a
fromIntField_ :: Typeable a => (Int64 -> Maybe a) -> Field -> Ok a
fromIntField_ fromInt = \case
f@(Field (SQLInteger i) _) ->
case fromInt i of

View File

@ -0,0 +1,20 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20230827_file_encryption where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20230827_file_encryption :: Query
m20230827_file_encryption =
[sql|
ALTER TABLE files ADD COLUMN file_crypto_key BLOB;
ALTER TABLE files ADD COLUMN file_crypto_nonce BLOB;
|]
down_m20230827_file_encryption :: Query
down_m20230827_file_encryption =
[sql|
ALTER TABLE files DROP COLUMN file_crypto_key;
ALTER TABLE files DROP COLUMN file_crypto_nonce;
|]

View File

@ -204,7 +204,9 @@ CREATE TABLE files(
agent_snd_file_id BLOB NULL,
private_snd_file_descr TEXT NULL,
agent_snd_file_deleted INTEGER DEFAULT 0 CHECK(agent_snd_file_deleted NOT NULL),
protocol TEXT NOT NULL DEFAULT 'smp'
protocol TEXT NOT NULL DEFAULT 'smp',
file_crypto_key BLOB,
file_crypto_nonce BLOB
);
CREATE TABLE snd_files(
file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE,

View File

@ -35,6 +35,8 @@ import GHC.Generics (Generic)
import Simplex.Chat
import Simplex.Chat.Controller
import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList)
import Simplex.Chat.Mobile.File
import Simplex.Chat.Mobile.Shared
import Simplex.Chat.Mobile.WebRTC
import Simplex.Chat.Options
import Simplex.Chat.Store
@ -69,6 +71,10 @@ foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: CString -> Ptr Wo
foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString
foreign export ccall "chat_write_file" cChatWriteFile :: CString -> Ptr Word8 -> CInt -> IO CJSONString
foreign export ccall "chat_read_file" cChatReadFile :: CString -> CString -> CString -> IO (Ptr Word8)
-- | check / migrate database and initialize chat controller on success
cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString
cChatMigrateInit fp key conf ctrl = do
@ -151,8 +157,6 @@ defaultMobileConfig =
logLevel = CLLError
}
type CJSONString = CString
getActiveUser_ :: SQLiteStore -> IO (Maybe User)
getActiveUser_ st = find activeUser <$> withTransaction st getUsers

View File

@ -0,0 +1,83 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module Simplex.Chat.Mobile.File
( cChatWriteFile,
cChatReadFile,
WriteFileResult (..),
ReadFileResult (..),
chatWriteFile,
chatReadFile,
)
where
import Control.Monad.Except
import Data.Aeson (ToJSON)
import qualified Data.Aeson as J
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as LB
import qualified Data.ByteString.Lazy.Char8 as LB'
import Data.Word (Word8)
import Foreign.C
import Foreign.Marshal.Alloc (mallocBytes)
import Foreign.Ptr
import GHC.Generics (Generic)
import Simplex.Chat.Mobile.Shared
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import qualified Simplex.Messaging.Crypto.File as CF
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
data WriteFileResult
= WFResult {cryptoArgs :: CryptoFileArgs}
| WFError {writeError :: String}
deriving (Generic)
instance ToJSON WriteFileResult where toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "WF"
cChatWriteFile :: CString -> Ptr Word8 -> CInt -> IO CJSONString
cChatWriteFile cPath ptr len = do
path <- peekCAString cPath
s <- getByteString ptr len
r <- chatWriteFile path s
newCAString $ LB'.unpack $ J.encode r
chatWriteFile :: FilePath -> ByteString -> IO WriteFileResult
chatWriteFile path s = do
cfArgs <- CF.randomArgs
let file = CryptoFile path $ Just cfArgs
either (WFError . show) (\_ -> WFResult cfArgs)
<$> runExceptT (CF.writeFile file $ LB.fromStrict s)
data ReadFileResult
= RFResult {fileSize :: Int}
| RFError {readError :: String}
deriving (Generic)
instance ToJSON ReadFileResult where toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RF"
cChatReadFile :: CString -> CString -> CString -> IO (Ptr Word8)
cChatReadFile cPath cKey cNonce = do
path <- peekCAString cPath
key <- B.packCString cKey
nonce <- B.packCString cNonce
(r, s) <- chatReadFile path key nonce
let r' = LB.toStrict $ J.encode r <> "\NUL"
ptr <- mallocBytes $ B.length r' + B.length s
putByteString ptr r'
unless (B.null s) $ putByteString (ptr `plusPtr` B.length r') s
pure ptr
chatReadFile :: FilePath -> ByteString -> ByteString -> IO (ReadFileResult, ByteString)
chatReadFile path keyStr nonceStr = do
either ((,"") . RFError) result <$> runExceptT readFile_
where
result s = let s' = LB.toStrict s in (RFResult $ B.length s', s')
readFile_ :: ExceptT String IO LB.ByteString
readFile_ = do
key <- liftEither $ strDecode keyStr
nonce <- liftEither $ strDecode nonceStr
let file = CryptoFile path $ Just $ CFArgs key nonce
withExceptT show $ CF.readFile file

View File

@ -0,0 +1,19 @@
module Simplex.Chat.Mobile.Shared where
import qualified Data.ByteString as B
import Data.ByteString.Internal (ByteString (PS), memcpy)
import Foreign.C (CInt, CString)
import Foreign (Ptr, Word8, newForeignPtr_, plusPtr)
import Foreign.ForeignPtr.Unsafe
type CJSONString = CString
getByteString :: Ptr Word8 -> CInt -> IO ByteString
getByteString ptr len = do
fp <- newForeignPtr_ ptr
pure $ PS fp 0 $ fromIntegral len
putByteString :: Ptr Word8 -> ByteString -> IO ()
putByteString ptr bs@(PS fp offset _) = do
let p = unsafeForeignPtrToPtr fp `plusPtr` offset
memcpy ptr p $ B.length bs

View File

@ -12,16 +12,15 @@ import Control.Monad.Except
import qualified Crypto.Cipher.Types as AES
import Data.Bifunctor (bimap)
import qualified Data.ByteArray as BA
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Base64.URL as U
import Data.ByteString.Internal (ByteString (PS), memcpy)
import Data.Either (fromLeft)
import Data.Word (Word8)
import Foreign.C (CInt, CString, newCAString)
import Foreign.ForeignPtr (newForeignPtr_)
import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
import Foreign.Ptr (Ptr, plusPtr)
import Foreign.Ptr (Ptr)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Chat.Mobile.Shared
cChatEncryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString
cChatEncryptMedia = cTransformMedia chatEncryptMedia
@ -32,16 +31,10 @@ cChatDecryptMedia = cTransformMedia chatDecryptMedia
cTransformMedia :: (ByteString -> ByteString -> ExceptT String IO ByteString) -> CString -> Ptr Word8 -> CInt -> IO CString
cTransformMedia f cKey cFrame cFrameLen = do
key <- B.packCString cKey
frame <- getFrame
frame <- getByteString cFrame cFrameLen
runExceptT (f key frame >>= liftIO . putFrame) >>= newCAString . fromLeft ""
where
getFrame = do
fp <- newForeignPtr_ cFrame
pure $ PS fp 0 $ fromIntegral cFrameLen
putFrame bs@(PS fp offset _) = do
let len = B.length bs
p = unsafeForeignPtrToPtr fp `plusPtr` offset
when (len <= fromIntegral cFrameLen) $ memcpy cFrame p len
putFrame s = when (B.length s <= fromIntegral cFrameLen) $ putByteString cFrame s
{-# INLINE cTransformMedia #-}
chatEncryptMedia :: ByteString -> ByteString -> ExceptT String IO ByteString

View File

@ -56,6 +56,7 @@ module Simplex.Chat.Store.Files
startRcvInlineFT,
xftpAcceptRcvFT,
setRcvFileToReceive,
setFileCryptoArgs,
getRcvFilesToReceive,
setRcvFTAgentDeleted,
updateRcvFileStatus,
@ -84,18 +85,21 @@ import Data.Time.Clock (UTCTime (..), getCurrentTime, nominalDay)
import Data.Type.Equality
import Database.SQLite.Simple (Only (..), (:.) (..))
import Database.SQLite.Simple.QQ (sql)
import Simplex.Chat.Messages
import Simplex.Chat.Messages.CIContent
import Simplex.Chat.Protocol
import Simplex.Chat.Store.Direct
import Simplex.Chat.Store.Messages
import Simplex.Chat.Store.Profiles
import Simplex.Chat.Store.Shared
import Simplex.Chat.Messages
import Simplex.Chat.Messages.CIContent
import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Chat.Util (week)
import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, UserId)
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import qualified Simplex.Messaging.Crypto.File as CF
getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer]
getLiveSndFileTransfers db User {userId} = do
@ -257,14 +261,14 @@ getSndFTViaMsgDelivery db User {userId} Connection {connId, agentConnId} agentMs
(\n -> SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, groupMemberId, recipientDisplayName = n, connId, agentConnId})
<$> (contactName_ <|> memberName_)
createSndFileTransferXFTP :: DB.Connection -> User -> ContactOrGroup -> FilePath -> FileInvitation -> AgentSndFileId -> Integer -> IO FileTransferMeta
createSndFileTransferXFTP db User {userId} contactOrGroup filePath FileInvitation {fileName, fileSize} agentSndFileId chunkSize = do
createSndFileTransferXFTP :: DB.Connection -> User -> ContactOrGroup -> CryptoFile -> FileInvitation -> AgentSndFileId -> Integer -> IO FileTransferMeta
createSndFileTransferXFTP db User {userId} contactOrGroup (CryptoFile filePath cryptoArgs) FileInvitation {fileName, fileSize} agentSndFileId chunkSize = do
currentTs <- getCurrentTime
let xftpSndFile = Just XFTPSndFile {agentSndFileId, privateSndFileDescr = Nothing, agentSndFileDeleted = False}
let xftpSndFile = Just XFTPSndFile {agentSndFileId, privateSndFileDescr = Nothing, agentSndFileDeleted = False, cryptoArgs}
DB.execute
db
"INSERT INTO files (contact_id, group_id, user_id, file_name, file_path, file_size, chunk_size, agent_snd_file_id, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"
(contactAndGroupIds contactOrGroup :. (userId, fileName, filePath, fileSize, chunkSize, agentSndFileId, CIFSSndStored, FPXFTP, currentTs, currentTs))
"INSERT INTO files (contact_id, group_id, user_id, file_name, file_path, file_crypto_key, file_crypto_nonce, file_size, chunk_size, agent_snd_file_id, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
(contactAndGroupIds contactOrGroup :. (userId, fileName, filePath, CF.fileKey <$> cryptoArgs, CF.fileNonce <$> cryptoArgs, fileSize, chunkSize, agentSndFileId, CIFSSndStored, FPXFTP, currentTs, currentTs))
fileId <- insertedRowId db
pure FileTransferMeta {fileId, xftpSndFile, fileName, filePath, fileSize, fileInline = Nothing, chunkSize, cancelled = False}
@ -479,7 +483,8 @@ createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@File
currentTs <- liftIO getCurrentTime
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd_
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_
-- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False, cryptoArgs = Nothing}) <$> rfd_
fileProtocol = if isJust rfd_ then FPXFTP else FPSMP
fileId <- liftIO $ do
DB.execute
@ -499,7 +504,8 @@ createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localD
currentTs <- liftIO getCurrentTime
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd_
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_
-- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False, cryptoArgs = Nothing}) <$> rfd_
fileProtocol = if isJust rfd_ then FPXFTP else FPSMP
fileId <- liftIO $ do
DB.execute
@ -600,7 +606,7 @@ getRcvFileTransfer db User {userId} fileId = do
[sql|
SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name,
f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name,
f.file_path, r.file_inline, r.rcv_file_inline, r.agent_rcv_file_id, r.agent_rcv_file_deleted, c.connection_id, c.agent_conn_id
f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline, r.agent_rcv_file_id, r.agent_rcv_file_deleted, c.connection_id, c.agent_conn_id
FROM rcv_files r
JOIN files f USING (file_id)
LEFT JOIN connections c ON r.file_id = c.rcv_file_id
@ -614,9 +620,9 @@ getRcvFileTransfer db User {userId} fileId = do
where
rcvFileTransfer ::
Maybe RcvFileDescr ->
(FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool) :. (Maybe Int64, Maybe AgentConnId) ->
(FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool) :. (Maybe Int64, Maybe AgentConnId) ->
ExceptT StoreError IO RcvFileTransfer
rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted) :. (connId_, agentConnId_)) =
rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted) :. (connId_, agentConnId_)) =
case contactName_ <|> memberName_ of
Nothing -> throwError $ SERcvFileInvalid fileId
Just name -> do
@ -629,7 +635,8 @@ getRcvFileTransfer db User {userId} fileId = do
where
ft senderDisplayName fileStatus =
let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing}
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted}) <$> rfd_
cryptoArgs = CFArgs <$> fileKey <*> fileNonce
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted, cryptoArgs}) <$> rfd_
in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId}
rfi = maybe (throwError $ SERcvFileInvalid fileId) pure =<< rfi_
rfi_ = case (filePath_, connId_, agentConnId_) of
@ -683,13 +690,21 @@ acceptRcvFT_ db User {userId} fileId filePath rcvFileInline currentTs = do
"UPDATE rcv_files SET rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?"
(rcvFileInline, FSAccepted, currentTs, fileId)
setRcvFileToReceive :: DB.Connection -> FileTransferId -> IO ()
setRcvFileToReceive db fileId = do
setRcvFileToReceive :: DB.Connection -> FileTransferId -> Maybe CryptoFileArgs -> IO ()
setRcvFileToReceive db fileId cfArgs_ = do
currentTs <- getCurrentTime
DB.execute db "UPDATE rcv_files SET to_receive = 1, updated_at = ? WHERE file_id = ?" (currentTs, fileId)
forM_ cfArgs_ $ \cfArgs -> setFileCryptoArgs_ db fileId cfArgs currentTs
setFileCryptoArgs :: DB.Connection -> FileTransferId -> CryptoFileArgs -> IO ()
setFileCryptoArgs db fileId cfArgs = setFileCryptoArgs_ db fileId cfArgs =<< getCurrentTime
setFileCryptoArgs_ :: DB.Connection -> FileTransferId -> CryptoFileArgs -> UTCTime -> IO ()
setFileCryptoArgs_ db fileId (CFArgs key nonce) currentTs =
DB.execute
db
"UPDATE rcv_files SET to_receive = 1, updated_at = ? WHERE file_id = ?"
(currentTs, fileId)
"UPDATE files SET file_crypto_key = ?, file_crypto_nonce = ?, updated_at = ? WHERE file_id = ?"
(key, nonce, currentTs, fileId)
getRcvFilesToReceive :: DB.Connection -> User -> IO [RcvFileTransfer]
getRcvFilesToReceive db user@User {userId} = do
@ -842,15 +857,16 @@ getFileTransferMeta db User {userId} fileId =
DB.query
db
[sql|
SELECT file_name, file_size, chunk_size, file_path, file_inline, agent_snd_file_id, agent_snd_file_deleted, private_snd_file_descr, cancelled
SELECT file_name, file_size, chunk_size, file_path, file_crypto_key, file_crypto_nonce, file_inline, agent_snd_file_id, agent_snd_file_deleted, private_snd_file_descr, cancelled
FROM files
WHERE user_id = ? AND file_id = ?
|]
(userId, fileId)
where
fileTransferMeta :: (String, Integer, Integer, FilePath, Maybe InlineFileMode, Maybe AgentSndFileId, Bool, Maybe Text, Maybe Bool) -> FileTransferMeta
fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileInline, aSndFileId_, agentSndFileDeleted, privateSndFileDescr, cancelled_) =
let xftpSndFile = (\fId -> XFTPSndFile {agentSndFileId = fId, privateSndFileDescr, agentSndFileDeleted}) <$> aSndFileId_
fileTransferMeta :: (String, Integer, Integer, FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe AgentSndFileId, Bool, Maybe Text, Maybe Bool) -> FileTransferMeta
fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileKey, fileNonce, fileInline, aSndFileId_, agentSndFileDeleted, privateSndFileDescr, cancelled_) =
let cryptoArgs = CFArgs <$> fileKey <*> fileNonce
xftpSndFile = (\fId -> XFTPSndFile {agentSndFileId = fId, privateSndFileDescr, agentSndFileDeleted, cryptoArgs}) <$> aSndFileId_
in FileTransferMeta {fileId, xftpSndFile, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = fromMaybe False cancelled_}
getContactFileInfo :: DB.Connection -> User -> Contact -> IO [CIFileInfo]

View File

@ -13,7 +13,6 @@
module Simplex.Chat.Store.Messages
( getContactConnIds_,
getDirectChatReactions_,
toDirectChatItem,
-- * Message and chat item functions
deleteContactCIs,
@ -122,6 +121,8 @@ import Simplex.Chat.Types
import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, MsgMeta (..), UserId)
import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import Simplex.Messaging.Util (eitherToMaybe)
import UnliftIO.STM
@ -484,7 +485,7 @@ getDirectChatPreviews_ db user@User {userId} = do
-- ChatItem
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live,
-- CIFile
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol,
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
-- DirectQuote
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
FROM contacts ct
@ -549,7 +550,7 @@ getGroupChatPreviews_ db User {userId, userContactId} = do
-- ChatItem
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live,
-- CIFile
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol,
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
-- Maybe GroupMember - sender
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category,
m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
@ -671,7 +672,7 @@ getDirectChatItemsLast db User {userId} contactId count search = ExceptT $ do
-- ChatItem
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live,
-- CIFile
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol,
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
-- DirectQuote
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
FROM chat_items i
@ -700,7 +701,7 @@ getDirectChatAfter_ db User {userId} ct@Contact {contactId} afterChatItemId coun
-- ChatItem
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live,
-- CIFile
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol,
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
-- DirectQuote
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
FROM chat_items i
@ -730,7 +731,7 @@ getDirectChatBefore_ db User {userId} ct@Contact {contactId} beforeChatItemId co
-- ChatItem
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live,
-- CIFile
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol,
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
-- DirectQuote
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
FROM chat_items i
@ -952,7 +953,7 @@ type ChatStatsRow = (Int, ChatItemId, Bool)
toChatStats :: ChatStatsRow -> ChatStats
toChatStats (unreadCount, minUnreadItemId, unreadChat) = ChatStats {unreadCount, minUnreadItemId, unreadChat}
type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe ACIFileStatus, Maybe FileProtocol)
type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe ACIFileStatus, Maybe FileProtocol)
type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe Bool)
@ -973,7 +974,7 @@ toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir
-- this function can be changed so it never fails, not only avoid failure on invalid json
toDirectChatItem :: UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect)
toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileStatus_, fileProtocol_)) :. quoteRow) =
toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) =
chatItem $ fromRight invalid $ dbParseACIContent itemContentText
where
invalid = ACIContent msgDir $ CIInvalidJSON itemContentText
@ -990,7 +991,10 @@ toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentT
maybeCIFile :: CIFileStatus d -> Maybe (CIFile d)
maybeCIFile fileStatus =
case (fileId_, fileName_, fileSize_, fileProtocol_) of
(Just fileId, Just fileName, Just fileSize, Just fileProtocol) -> Just CIFile {fileId, fileName, fileSize, filePath, fileStatus, fileProtocol}
(Just fileId, Just fileName, Just fileSize, Just fileProtocol) ->
let cfArgs = CFArgs <$> fileKey <*> fileNonce
fileSource = (`CryptoFile` cfArgs) <$> filePath
in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol}
_ -> Nothing
cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTDirect d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTDirect
cItem d chatDir ciStatus content file =
@ -1023,7 +1027,7 @@ toGroupQuote qr@(_, _, _, _, quotedSent) quotedMember_ = toQuote qr $ direction
-- this function can be changed so it never fails, not only avoid failure on invalid json
toGroupChatItem :: UTCTime -> Int64 -> ChatItemRow :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow -> Either StoreError (CChatItem 'CTGroup)
toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileStatus_, fileProtocol_)) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do
toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do
chatItem $ fromRight invalid $ dbParseACIContent itemContentText
where
member_ = toMaybeGroupMember userContactId memberRow_
@ -1043,7 +1047,10 @@ toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir,
maybeCIFile :: CIFileStatus d -> Maybe (CIFile d)
maybeCIFile fileStatus =
case (fileId_, fileName_, fileSize_, fileProtocol_) of
(Just fileId, Just fileName, Just fileSize, Just fileProtocol) -> Just CIFile {fileId, fileName, fileSize, filePath, fileStatus, fileProtocol}
(Just fileId, Just fileName, Just fileSize, Just fileProtocol) ->
let cfArgs = CFArgs <$> fileKey <*> fileNonce
fileSource = (`CryptoFile` cfArgs) <$> filePath
in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol}
_ -> Nothing
cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTGroup d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTGroup
cItem d chatDir ciStatus content file =
@ -1143,7 +1150,7 @@ updateDirectChatItemStatus db user@User {userId} contactId itemId itemStatus = d
correctDir :: CChatItem c -> Either StoreError (ChatItem c d)
correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci
updateDirectChatItem :: forall d. (MsgDirectionI d) => DB.Connection -> User -> Int64 -> ChatItemId -> CIContent d -> Bool -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d)
updateDirectChatItem :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItemId -> CIContent d -> Bool -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d)
updateDirectChatItem db user contactId itemId newContent live msgId_ = do
ci <- liftEither . correctDir =<< getDirectChatItem db user contactId itemId
liftIO $ updateDirectChatItem' db user contactId ci newContent live msgId_
@ -1151,7 +1158,7 @@ updateDirectChatItem db user contactId itemId newContent live msgId_ = do
correctDir :: CChatItem c -> Either StoreError (ChatItem c d)
correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci
updateDirectChatItem' :: forall d. (MsgDirectionI d) => DB.Connection -> User -> Int64 -> ChatItem 'CTDirect d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTDirect d)
updateDirectChatItem' :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTDirect d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTDirect d)
updateDirectChatItem' db User {userId} contactId ci newContent live msgId_ = do
currentTs <- liftIO getCurrentTime
let ci' = updatedChatItem ci newContent live currentTs
@ -1296,7 +1303,7 @@ getDirectChatItem db User {userId} contactId itemId = ExceptT $ do
-- ChatItem
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live,
-- CIFile
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol,
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
-- DirectQuote
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
FROM chat_items i
@ -1471,7 +1478,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
-- ChatItem
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live,
-- CIFile
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol,
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
-- GroupMember
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category,
m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,

View File

@ -76,6 +76,7 @@ import Simplex.Chat.Migrations.M20230621_chat_item_moderations
import Simplex.Chat.Migrations.M20230705_delivery_receipts
import Simplex.Chat.Migrations.M20230721_group_snd_item_statuses
import Simplex.Chat.Migrations.M20230814_indexes
import Simplex.Chat.Migrations.M20230827_file_encryption
import Simplex.Chat.Migrations.M20230829_connections_chat_vrange
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
@ -153,6 +154,7 @@ schemaMigrations =
("20230705_delivery_receipts", m20230705_delivery_receipts, Just down_m20230705_delivery_receipts),
("20230721_group_snd_item_statuses", m20230721_group_snd_item_statuses, Just down_m20230721_group_snd_item_statuses),
("20230814_indexes", m20230814_indexes, Just down_m20230814_indexes),
("20230827_file_encryption", m20230827_file_encryption, Just down_m20230827_file_encryption),
("20230829_connections_chat_vrange", m20230829_connections_chat_vrange, Just down_m20230829_connections_chat_vrange)
]

View File

@ -42,6 +42,7 @@ import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Util
import Simplex.FileTransfer.Description (FileDigest)
import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, SAEntity (..), UserId)
import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, sumTypeJSON, taggedObjectJSON)
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI)
@ -963,7 +964,8 @@ instance ToJSON RcvFileTransfer where toEncoding = J.genericToEncoding J.default
data XFTPRcvFile = XFTPRcvFile
{ rcvFileDescription :: RcvFileDescr,
agentRcvFileId :: Maybe AgentRcvFileId,
agentRcvFileDeleted :: Bool
agentRcvFileDeleted :: Bool,
cryptoArgs :: Maybe CryptoFileArgs
}
deriving (Eq, Show, Generic)
@ -1118,7 +1120,8 @@ instance ToJSON FileTransferMeta where toEncoding = J.genericToEncoding J.defaul
data XFTPSndFile = XFTPSndFile
{ agentSndFileId :: AgentSndFileId,
privateSndFileDescr :: Maybe Text,
agentSndFileDeleted :: Bool
agentSndFileDeleted :: Bool,
cryptoArgs :: Maybe CryptoFileArgs
}
deriving (Eq, Show, Generic)

View File

@ -20,6 +20,7 @@ import Data.Int (Int64)
import Data.List (groupBy, intercalate, intersperse, partition, sortOn)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe)
import Data.Text (Text)
@ -50,6 +51,7 @@ import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..))
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON)
@ -161,7 +163,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
CRRcvFileDescrReady _ _ -> []
CRRcvFileDescrNotReady _ _ -> []
CRRcvFileProgressXFTP {} -> []
CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci
CRRcvFileAccepted u ci -> ttyUser u $ savingFile' testView ci
CRRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft
CRSndFileCancelled u _ ftm fts -> ttyUser u $ viewSndFileCancelled ftm fts
CRRcvFileCancelled u _ ft -> ttyUser u $ receivingFile_ "cancelled" ft
@ -252,7 +254,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
CRSQLResult rows -> map plain rows
CRSlowSQLQueries {chatQueries, agentQueries} ->
let viewQuery SlowSQLQuery {query, queryStats = SlowQueryStats {count, timeMax, timeAvg}} =
"count: " <> sShow count
("count: " <> sShow count)
<> (" :: max: " <> sShow timeMax <> " ms")
<> (" :: avg: " <> sShow timeAvg <> " ms")
<> (" :: " <> plain (T.unwords $ T.lines query))
@ -262,20 +264,21 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
plain $ "agent locks: " <> LB.unpack (J.encode agentLocks)
]
CRAgentStats stats -> map (plain . intercalate ",") stats
CRAgentSubs {activeSubs, distinctActiveSubs, pendingSubs, distinctPendingSubs} ->
[plain $ "Subscriptions: active = " <> show (sum activeSubs) <> ", distinct active = " <> show (sum distinctActiveSubs) <> ", pending = " <> show (sum pendingSubs) <> ", distinct pending = " <> show (sum distinctPendingSubs)]
CRAgentSubs {activeSubs, pendingSubs, removedSubs} ->
[plain $ "Subscriptions: active = " <> show (sum activeSubs) <> ", pending = " <> show (sum pendingSubs) <> ", removed = " <> show (sum $ M.map length removedSubs)]
<> ("active subscriptions:" : listSubs activeSubs)
<> ("distinct active subscriptions:" : listSubs distinctActiveSubs)
<> ("pending subscriptions:" : listSubs pendingSubs)
<> ("distinct pending subscriptions:" : listSubs distinctPendingSubs)
<> ("removed subscriptions:" : listSubs removedSubs)
where
listSubs = map (\(srv, count) -> plain $ srv <> ": " <> tshow count) . M.assocs
CRAgentSubsDetails SubscriptionsInfo {activeSubscriptions, pendingSubscriptions} ->
listSubs :: Show a => Map Text a -> [StyledString]
listSubs = map (\(srv, info) -> plain $ srv <> ": " <> tshow info) . M.assocs
CRAgentSubsDetails SubscriptionsInfo {activeSubscriptions, pendingSubscriptions, removedSubscriptions} ->
("active subscriptions:" : map sShow activeSubscriptions)
<> ("pending subscriptions: " : map sShow pendingSubscriptions)
<> ("removed subscriptions: " : map sShow removedSubscriptions)
CRConnectionDisabled entity -> viewConnectionEntityDisabled entity
CRAgentRcvQueueDeleted acId srv aqId err_ ->
[ "completed deleting rcv queue, agent connection id: " <> sShow acId
[ ("completed deleting rcv queue, agent connection id: " <> sShow acId)
<> (", server: " <> sShow srv)
<> (", agent queue id: " <> sShow aqId)
<> maybe "" (\e -> ", error: " <> sShow e) err_
@ -328,7 +331,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
Just CIQuote {chatDir = quoteDir, content} ->
Just (msgDirectionInt $ quoteMsgDirection quoteDir, msgContentText content)
fPath = case file of
Just CIFile {filePath = Just fp} -> Just fp
Just CIFile {fileSource = Just (CryptoFile fp _)} -> Just fp
_ -> Nothing
testViewItem :: CChatItem c -> Maybe GroupMember -> Text
testViewItem (CChatItem _ ci@ChatItem {meta = CIMeta {itemText}}) membership_ =
@ -951,7 +954,8 @@ viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} =
viewContactInfo :: Contact -> ConnectionStats -> Maybe Profile -> [StyledString]
viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn} stats incognitoProfile =
["contact ID: " <> sShow contactId] <> viewConnectionStats stats
["contact ID: " <> sShow contactId]
<> viewConnectionStats stats
<> maybe [] (\l -> ["contact address: " <> (plain . strEncode) l]) contactLink
<> maybe
["you've shared main profile with this contact"]
@ -1275,8 +1279,8 @@ viewSentBroadcast mc s f ts tz time = prependFirst (highlight' "/feed" <> " (" <
| otherwise = ""
viewSentFileInvitation :: StyledString -> CIFile d -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString]
viewSentFileInvitation to CIFile {fileId, filePath, fileStatus} ts tz = case filePath of
Just fPath -> sentWithTime_ ts tz $ ttySentFile fPath
viewSentFileInvitation to CIFile {fileId, fileSource, fileStatus} ts tz = case fileSource of
Just (CryptoFile fPath _) -> sentWithTime_ ts tz $ ttySentFile fPath
_ -> const []
where
ttySentFile fPath = ["/f " <> to <> ttyFilePath fPath] <> cancelSending
@ -1344,14 +1348,20 @@ humanReadableSize size
mB = kB * 1024
gB = mB * 1024
savingFile' :: AChatItem -> [StyledString]
savingFile' (AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, filePath = Just filePath}, chatDir = CIDirectRcv}) =
["saving file " <> sShow fileId <> " from " <> ttyContact c <> " to " <> plain filePath]
savingFile' (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, filePath = Just filePath}, chatDir = CIGroupRcv GroupMember {localDisplayName = m}}) =
["saving file " <> sShow fileId <> " from " <> ttyContact m <> " to " <> plain filePath]
savingFile' (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, filePath = Just filePath}}) =
["saving file " <> sShow fileId <> " to " <> plain filePath]
savingFile' _ = ["saving file"] -- shouldn't happen
savingFile' :: Bool -> AChatItem -> [StyledString]
savingFile' testView (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileSource = Just (CryptoFile filePath cfArgs_)}, chatDir}) =
let from = case (chat, chatDir) of
(DirectChat Contact {localDisplayName = c}, CIDirectRcv) -> " from " <> ttyContact c
(_, CIGroupRcv GroupMember {localDisplayName = m}) -> " from " <> ttyContact m
_ -> ""
in ["saving file " <> sShow fileId <> from <> " to " <> plain filePath] <> cfArgsStr
where
cfArgsStr = case cfArgs_ of
Just cfArgs@(CFArgs key nonce)
| testView -> [plain $ LB.unpack $ J.encode cfArgs]
| otherwise -> [plain $ "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce]
_ -> []
savingFile' _ _ = ["saving file"] -- shouldn't happen
receivingFile_' :: StyledString -> AChatItem -> [StyledString]
receivingFile_' status (AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectRcv}) =
@ -1403,7 +1413,7 @@ viewFileTransferStatus (FTRcv ft@RcvFileTransfer {fileId, fileInvitation = FileI
RFSCancelled Nothing -> "cancelled"
viewFileTransferStatusXFTP :: AChatItem -> [StyledString]
viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, fileName, fileSize, fileStatus, filePath}}) =
viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, fileName, fileSize, fileStatus, fileSource}}) =
case fileStatus of
CIFSSndStored -> ["sending " <> fstr <> " just started"]
CIFSSndTransfer progress total -> ["sending " <> fstr <> " in progress " <> fileProgressXFTP progress total fileSize]
@ -1413,7 +1423,7 @@ viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId
CIFSRcvInvitation -> ["receiving " <> fstr <> " not accepted yet, use " <> highlight ("/fr " <> show fileId) <> " to receive file"]
CIFSRcvAccepted -> ["receiving " <> fstr <> " just started"]
CIFSRcvTransfer progress total -> ["receiving " <> fstr <> " progress " <> fileProgressXFTP progress total fileSize]
CIFSRcvComplete -> ["receiving " <> fstr <> " complete" <> maybe "" (\fp -> ", path: " <> plain fp) filePath]
CIFSRcvComplete -> ["receiving " <> fstr <> " complete" <> maybe "" (\(CryptoFile fp _) -> ", path: " <> plain fp) fileSource]
CIFSRcvCancelled -> ["receiving " <> fstr <> " cancelled"]
CIFSRcvError -> ["receiving " <> fstr <> " error"]
CIFSInvalid text -> [fstr <> " invalid status: " <> plain text]

View File

@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq
- github: simplex-chat/simplexmq
commit: 4c0b8a31d20870a23e120e243359901d8240f922
commit: 980e5c4d1ec15f44290542fd2a5d1c08456f00d1
- github: kazu-yamamoto/http2
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
# - ../direct-sqlcipher

View File

@ -8,14 +8,19 @@ import ChatClient
import ChatTests.Utils
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (concurrently_)
import qualified Data.Aeson as J
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Simplex.Chat (roundedFDCount)
import Simplex.Chat.Controller (ChatConfig (..), InlineFilesConfig (..), XFTPFileConfig (..), defaultInlineFilesConfig)
import Simplex.Chat.Mobile.File
import Simplex.Chat.Options (ChatOpts (..))
import Simplex.FileTransfer.Client.Main (xftpClientCLI)
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..))
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Util (unlessM)
import System.Directory (copyFile, doesFileExist)
import System.Directory (copyFile, createDirectoryIfMissing, doesFileExist, getFileSize)
import System.Environment (withArgs)
import System.IO.Silently (capture_)
import Test.Hspec
@ -59,6 +64,7 @@ chatFileTests = do
describe "file transfer over XFTP" $ do
it "round file description count" $ const testXFTPRoundFDCount
it "send and receive file" testXFTPFileTransfer
it "send and receive locally encrypted files" testXFTPFileTransferEncrypted
it "send and receive file, accepting after upload" testXFTPAcceptAfterUpload
it "send and receive file in group" testXFTPGroupFileTransfer
it "delete uploaded file" testXFTPDeleteUploadedFile
@ -1012,6 +1018,35 @@ testXFTPFileTransfer =
where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
testXFTPFileTransferEncrypted :: HasCallStack => FilePath -> IO ()
testXFTPFileTransferEncrypted =
testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
src <- B.readFile "./tests/fixtures/test.pdf"
srcLen <- getFileSize "./tests/fixtures/test.pdf"
let srcPath = "./tests/tmp/alice/test.pdf"
createDirectoryIfMissing True "./tests/tmp/alice/"
createDirectoryIfMissing True "./tests/tmp/bob/"
WFResult cfArgs <- chatWriteFile srcPath src
let fileJSON = LB.unpack $ J.encode $ CryptoFile srcPath $ Just cfArgs
withXFTPServer $ do
connectUsers alice bob
alice ##> ("/_send @2 json {\"msgContent\":{\"type\":\"file\", \"text\":\"\"}, \"fileSource\": " <> fileJSON <> "}")
alice <# "/f @bob ./tests/tmp/alice/test.pdf"
alice <## "use /fc 1 to cancel sending"
bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
bob ##> "/fr 1 encrypt=on ./tests/tmp/bob/"
bob <## "saving file 1 from alice to ./tests/tmp/bob/test.pdf"
Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob
alice <## "completed uploading file 1 (test.pdf) for bob"
bob <## "started receiving file 1 (test.pdf) from alice"
bob <## "completed receiving file 1 (test.pdf) from alice"
(RFResult destLen, dest) <- chatReadFile "./tests/tmp/bob/test.pdf" (strEncode key) (strEncode nonce)
fromIntegral destLen `shouldBe` srcLen
dest `shouldBe` src
where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
testXFTPAcceptAfterUpload :: HasCallStack => FilePath -> IO ()
testXFTPAcceptAfterUpload =
testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
@ -1446,7 +1481,7 @@ startFileTransfer alice bob =
startFileTransfer' alice bob "test.jpg" "136.5 KiB / 139737 bytes"
startFileTransfer' :: HasCallStack => TestCC -> TestCC -> String -> String -> IO ()
startFileTransfer' cc1 cc2 fileName fileSize = startFileTransferWithDest' cc1 cc2 fileName fileSize $ Just "./tests/tmp"
startFileTransfer' cc1 cc2 fName fSize = startFileTransferWithDest' cc1 cc2 fName fSize $ Just "./tests/tmp"
checkPartialTransfer :: HasCallStack => String -> IO ()
checkPartialTransfer fileName = do

View File

@ -1,22 +1,46 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module MobileTests where
import ChatTests.Utils
import Control.Monad.Except
import Crypto.Random (getRandomBytes)
import Data.Aeson (FromJSON (..))
import qualified Data.Aeson as J
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Word (Word8)
import Foreign.C
import Foreign.Marshal.Alloc (mallocBytes)
import Foreign.Ptr
import Simplex.Chat.Mobile
import Simplex.Chat.Mobile.File
import Simplex.Chat.Mobile.Shared
import Simplex.Chat.Mobile.WebRTC
import Simplex.Chat.Store
import Simplex.Chat.Store.Profiles
import Simplex.Chat.Types (AgentUserId (..), Profile (..))
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
import System.FilePath ((</>))
import Test.Hspec
mobileTests :: SpecWith FilePath
mobileTests :: HasCallStack => SpecWith FilePath
mobileTests = do
describe "mobile API" $ do
it "start new chat without user" testChatApiNoUser
it "start new chat with existing user" testChatApi
it "should encrypt/decrypt WebRTC frames" testMediaApi
it "should encrypt/decrypt WebRTC frames via C API" testMediaCApi
it "should read/write encrypted files via C API" testFileCApi
noActiveUser :: String
#if defined(darwin_HOST_OS) && defined(swiftJSON)
@ -113,3 +137,65 @@ testChatApi tmp = do
chatRecvMsgWait cc 10000 `shouldReturn` ""
chatParseMarkdown "hello" `shouldBe` "{}"
chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown
testMediaApi :: HasCallStack => FilePath -> IO ()
testMediaApi _ = do
key :: ByteString <- getRandomBytes 32
frame <- getRandomBytes 100
let keyStr = strEncode key
reserved = B.replicate (C.authTagSize + C.gcmIVSize) 0
frame' = frame <> reserved
Right encrypted <- runExceptT $ chatEncryptMedia keyStr frame'
encrypted `shouldNotBe` frame'
B.length encrypted `shouldBe` B.length frame'
runExceptT (chatDecryptMedia keyStr encrypted) `shouldReturn` Right frame'
testMediaCApi :: HasCallStack => FilePath -> IO ()
testMediaCApi _ = do
key :: ByteString <- getRandomBytes 32
frame <- getRandomBytes 100
let keyStr = strEncode key
reserved = B.replicate (C.authTagSize + C.gcmIVSize) 0
frame' = frame <> reserved
encrypted <- test cChatEncryptMedia keyStr frame'
encrypted `shouldNotBe` frame'
test cChatDecryptMedia keyStr encrypted `shouldReturn` frame'
where
test :: HasCallStack => (CString -> Ptr Word8 -> CInt -> IO CString) -> ByteString -> ByteString -> IO ByteString
test f keyStr frame = do
let len = B.length frame
cLen = fromIntegral len
ptr <- mallocBytes len
putByteString ptr frame
cKeyStr <- newCAString $ BS.unpack keyStr
(f cKeyStr ptr cLen >>= peekCAString) `shouldReturn` ""
getByteString ptr cLen
instance FromJSON WriteFileResult where parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "WF"
instance FromJSON ReadFileResult where parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "RF"
testFileCApi :: FilePath -> IO ()
testFileCApi tmp = do
src <- B.readFile "./tests/fixtures/test.pdf"
cPath <- newCAString $ tmp </> "test.pdf"
let len = B.length src
cLen = fromIntegral len
ptr <- mallocBytes $ B.length src
putByteString ptr src
r <- peekCAString =<< cChatWriteFile cPath ptr cLen
Just (WFResult (CFArgs key nonce)) <- jDecode r
cKey <- encodedCString key
cNonce <- encodedCString nonce
ptr' <- cChatReadFile cPath cKey cNonce
-- the returned pointer contains NUL-terminated JSON string of ReadFileResult followed by the file contents
r' <- peekCAString $ castPtr ptr'
Just (RFResult sz) <- jDecode r'
contents <- getByteString (ptr' `plusPtr` (length r' + 1)) $ fromIntegral sz
contents `shouldBe` src
sz `shouldBe` len
where
jDecode :: FromJSON a => String -> IO (Maybe a)
jDecode = pure . J.decode . LB.pack
encodedCString :: StrEncoding a => a -> IO CString
encodedCString = newCAString . BS.unpack . strEncode

View File

@ -157,10 +157,10 @@
"comparison-section-list-point-7": "شبكات P2P إما لديها سلطة مركزية أو أن الشبكة كلها يمكن عرضة للخطر",
"see-here": "اقرأ هنا",
"no-secure": "لا - آمن",
"comparison-section-list-point-5": "لا يحمي المعلومات الوصفية للمستخدمين",
"comparison-section-list-point-5": "لا يحمي خصوصية البيانات الوصفية للمستخدمين",
"comparison-section-list-point-6": "على الرغم من أن الـP2P موزعة، إلا أنها ليست فدرالية - يعملون كشبكة واحدة",
"comparison-section-list-point-1": "عادة ما يكون مكوناً من رقم الهاتف، أو اسم المستخدم في بعض الأحيان",
"comparison-section-list-point-4": "إذا خوادم المشغّل مُخترقة",
"comparison-section-list-point-4": "إذا خوادم المشغّل مُخترقة. تحقق من رمز الأمان في Signal وبعض التطبيقات الأخرى للتخفيف منه",
"simplex-unique-card-3-p-1": "يخزن SimpleX جميع بيانات المستخدم على الأجهزة العميلة<strong> بتنسيق قاعدة بيانات محمولة مشفرة &mdash; </strong>يمكن نقله إلى جهاز آخر.",
"simplex-unique-card-4-p-1": "شبكة SimpleX لا مركزية بالكامل ومستقلة عن أي عملة مشفرة أو أي منصة أخرى، بخلاف الإنترنت.",
"simplex-unique-card-4-p-2": "يمكنك<strong> استخدام SimpleX مع خوادمك الخاصة </strong> أو مع الخوادم التي نوفرها &mdash; ولا يزال الاتصال ممكن بأي مستخدم.",
@ -240,5 +240,8 @@
"signing-key-fingerprint": "توقيع مفتاح البصمة (SHA-256)",
"f-droid-org-repo": "مستودع F-Droid.org",
"stable-versions-built-by-f-droid-org": "الإصدارات الثابتة التي تم إنشاؤها بواسطة F-Droid.org",
"releases-to-this-repo-are-done-1-2-days-later": "يتم إصدار الإصدارات إلى هذا المستودع بعد يوم أو يومين"
}
"releases-to-this-repo-are-done-1-2-days-later": "يتم إصدار الإصدارات إلى هذا المستودع بعد يوم أو يومين",
"f-droid-page-simplex-chat-repo-section-text": "لإضافته إلى عميل F-Droid، <span class='hide-on-mobile'>امسح رمز QR أو</span> استخدم عنوان URL هذا:",
"f-droid-page-f-droid-org-repo-section-text": "مستودعات SimpleX Chat و F-Droid.org مبنية على مفاتيح مختلفة. للتبديل، يرجى <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>تصدير</a> قاعدة بيانات الدردشة وإعادة تثبيت التطبيق.",
"comparison-section-list-point-4a": "مُرحلات SimpleX لا يمكنها أن تتنازل عن تشفير بين الطرفين. تحقق من رمز الأمان للتخفيف من الهجوم على القناة خارج النطاق"
}

View File

@ -170,7 +170,7 @@
"no-federated": "Nein - föderiert",
"comparison-section-list-point-2": "DNS-basierte Adressen",
"comparison-section-list-point-3": "Öffentlicher Schlüssel oder eine andere weltweit eindeutige ID",
"comparison-section-list-point-4": "Wenn die Server des Betreibers kompromittiert werden",
"comparison-section-list-point-4": "Wenn die Server des Betreibers kompromittiert werden. In Signal und weiteren Apps kann der Securitycode überprüft werden, um dies zu entschärfen",
"comparison-section-list-point-6": "P2P sind zwar verteilt, aber nicht föderiert - sie arbeiten als ein einziges Netzwerk",
"comparison-section-list-point-7": "P2P-Netzwerke haben entweder eine zentrale Verwaltung oder das gesamte Netzwerk kann kompromittiert werden",
"see-here": "Siehe hier",
@ -194,7 +194,7 @@
"comparison-section-list-point-1": "Normalerweise auf der Grundlage einer Telefonnummer, in einigen Fällen auf der Grundlage von Benutzernamen",
"comparison-point-5-text": "Zentrale Komponente oder andere Netzwerk-weite Angriffe",
"no-decentralized": "Nein - dezentralisiert",
"comparison-section-list-point-5": "Metadaten des Nutzers werden nicht geschützt",
"comparison-section-list-point-5": "Die Privatsphäre-Metadaten des Nutzers werden nicht geschützt",
"simplex-network-overlay-card-1-li-1": "P2P-Netzwerke vertrauen auf Varianten von <a href='https://en.wikipedia.org/wiki/Distributed_hash_table'>DHT</a>, um Nachrichten zu routen. DHT-Designs müssen zwischen Zustellungsgarantie und Latenz ausgleichen. Verglichen mit P2P bietet SimpleX sowohl eine bessere Zustellungsgarantie, als auch eine niedrigere Latenz, weil eine Nachricht redundant und parallel über mehrere Server gesendet werden kann, wobei die durch den Empfänger ausgewählten Server genutzt werden. In P2P-Netzwerken werden Nachrichten sequentiell über <em>O(log N)</em> Knoten gesendet, wobei die Knoten durch einen Algorithmus ausgewählt werden.",
"simplex-unique-overlay-card-3-p-4": "Zwischen dem gesendeten und empfangenen Serververkehr gibt es keine gemeinsamen Kennungen oder Chiffriertexte &mdash; sodass ein Beobachter nicht ohne weiteres feststellen kann, wer mit wem kommuniziert, selbst wenn TLS kompromittiert wurde.",
"simplex-unique-overlay-card-4-p-3": "Wenn Sie darüber nachdenken, für die SimpleX-Plattform entwickeln zu wollen, z.B. einen Chatbot für SimpleX-App-Nutzer oder die Integration der SimpleX-Chat-Bibliothek in Ihre mobilen Apps, <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>kontaktieren Sie uns bitte</a> für eine weitere Beratung und Unterstützung.",
@ -242,5 +242,6 @@
"simplex-chat-via-f-droid": "SimpleX Chat per F-Droid",
"simplex-chat-repo": "SimpleX Chat Repository",
"stable-and-beta-versions-built-by-developers": "Von den Entwicklern erstellte stabile und Beta-Versionen",
"f-droid-page-simplex-chat-repo-section-text": "Um es Ihrem F-Droid-Client hinzuzufügen <span class='hide-on-mobile'>scannen Sie den QR-Code oder</span> nutzen Sie diese URL:"
}
"f-droid-page-simplex-chat-repo-section-text": "Um es Ihrem F-Droid-Client hinzuzufügen <span class='hide-on-mobile'>scannen Sie den QR-Code oder</span> nutzen Sie diese URL:",
"comparison-section-list-point-4a": "SimpleX-Relais können die E2E-Verschlüsselung nicht kompromittieren. Überprüfen Sie den Sicherheitscode, um einen möglichen Angriff auf den Out-of-Band-Kanal zu entschärfen"
}

View File

@ -179,8 +179,8 @@
"no-federated": "No - federado",
"comparison-section-list-point-1": "Generalmente basado en un número de teléfono, en algunos casos en nombres de usuario",
"comparison-section-list-point-2": "Direcciones basadas en DNS",
"comparison-section-list-point-4": "Si los servidores del operador se ven comprometidos",
"comparison-section-list-point-5": "No protege los metadatos del usuario",
"comparison-section-list-point-4": "Si los servidores del operador se ven comprometidos. Verifique el código de seguridad en Signal y alguna otra aplicación para mitigarlo",
"comparison-section-list-point-5": "No protege la privacidad de los metadatos del usuario",
"comparison-section-list-point-3": "Clave pública o algun otro ID único a nivel global",
"comparison-section-list-point-6": "A pesar de que las redes P2P son distribuidas, no son federadas - funcionan como una única red",
"comparison-section-list-point-7": "Las redes P2P o bien tienen una autoridad central o toda la red puede verse comprometida",
@ -242,5 +242,6 @@
"stable-versions-built-by-f-droid-org": "Versión estable compilada por F-Droid.org",
"f-droid-page-f-droid-org-repo-section-text": "Los repositorios de SimpleX Chat y F-Droid.org firman con distinto certificado. Para cambiar, por favor <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>exportar</a> la base de datos y reinstala la aplicación.",
"signing-key-fingerprint": "Huella digital de la clave de firma (SHA-256)",
"releases-to-this-repo-are-done-1-2-days-later": "Las versiones aparecen 1-2 días más tarde en este repositorio"
}
"releases-to-this-repo-are-done-1-2-days-later": "Las versiones aparecen 1-2 días más tarde en este repositorio",
"comparison-section-list-point-4a": "Los servidores de retransmisión no pueden comprometer la encriptación e2e. Para evitar posibles ataques, verifique el código de seguridad mediante un canal alternativo"
}

247
website/langs/fi.json Normal file
View File

@ -0,0 +1,247 @@
{
"home": "Koti",
"developers": "Kehittäjät",
"reference": "Lisätietoja",
"blog": "Blogi",
"features": "Ominaisuudet",
"why-simplex": "Miksi SimpleX",
"simplex-privacy": "SimpleX yksityisyys",
"simplex-network": "SimpleX verkko",
"simplex-explained": "SimpleX selitetynä",
"simplex-explained-tab-1-text": "1. Mitä käyttäjät kokevat",
"simplex-explained-tab-2-text": "2. Miten se toimii",
"simplex-explained-tab-3-text": "3. Mitä palvelimet näkevät",
"simplex-explained-tab-1-p-2": "Kuinka se voi toimia yksisuuntaisten jonotusten kanssa ilman käyttäjäprofiilin tunnisteita?",
"simplex-explained-tab-2-p-1": "Jokaista yhteyttä varten käytetään kahta erillistä viestintäjonon mahdollistavaa palvelinta viestien lähettämiseen ja vastaanottamiseen.",
"simplex-explained-tab-2-p-2": "Palvelimet välittävät viestejä vain yhteen suuntaan eivätkä saa kokonaiskuvaa käyttäjän keskustelusta tai yhteyksistä.",
"simplex-explained-tab-3-p-2": "Käyttäjät voivat lisäksi parantaa metadata-yksityisyyttään käyttämällä Tor-verkkoa palvelimiin yhdistämiseen, mikä estää IP-osoitteen perusteella tapahtuvan yhteyksien tekemisen.",
"chat-bot-example": "Keskustelu­botti-esimerkki",
"smp-protocol": "SMP-protokolla",
"chat-protocol": "Keskustelu­protokolla",
"simplex-chat-protocol": "SimpleX Keskustelu­protokolla",
"terminal-cli": "Pääte CLI",
"terms-and-privacy-policy": "Käyttöehdot ja yksityisyys­käytäntö",
"hero-header": "Yksityisyys uudelleen määritelty",
"hero-subheader": "Ensimmäinen viestisovellus<br>ilman käyttäjätunnuksia",
"hero-overlay-1-textlink": "Miksi käyttäjätunnukset ovat huonoja yksityisyydelle?",
"hero-overlay-1-title": "Kuinka SimpleX toimii?",
"hero-overlay-2-title": "Miksi käyttäjätunnukset ovat huonoja yksityisyydelle?",
"feature-1-title": "Päästä päähän salattuja viestejä markdownin ja muokkaamisen kera",
"feature-2-title": "Päästä päähän salattuja<br>kuvia ja tiedostoja",
"feature-3-title": "Hajautetut salaiset ryhmät &mdash;<br>vain käyttäjät tietävät niiden olemassaolosta",
"feature-4-title": "Päästä päähän salattuja ääniviestejä",
"feature-5-title": "Katoavia viestejä",
"feature-8-title": "Incognito-tila &mdash;<br>ainutlaatuinen SimpleX Chatille",
"simplex-network-overlay-1-title": "Verrattuna P2P-viestintäprotokolliin",
"simplex-private-7-title": "Viestin eheys<br>vahvistus",
"simplex-private-9-title": "Yksisuuntaisia<br>viestijonoja",
"simplex-private-10-title": "Väliaikaiset nimettömät parittaiset tunnisteet",
"simplex-private-card-3-point-1": "Vain TLS 1.2/1.3 vahvoilla algoritmeilla käytetään asiakas-palvelin-yhteyksiin.",
"simplex-private-card-4-point-2": "Käyttääksesi SimpleX:ää Torin kautta, asenna <a href=\"https://guardianproject.info/apps/org.torproject.android/\" target=\"_blank\">Orbot-sovellus</a> ja ota käyttöön SOCKS5-välityspalvelin (tai VPN <a href=\"https://apps.apple.com/us/app/orbot/id1609461599?platform=iphone\" target=\"_blank\">iOS:lla</a>).",
"simplex-private-card-7-point-1": "Viestien eheyden varmistamiseksi ne numeroituvat peräkkäin ja sisältävät edellisen viestin tiivisteen.",
"simplex-private-card-7-point-2": "Jos viestiä lisätään, poistetaan tai muutetaan, vastaanottaja saa ilmoituksen.",
"simplex-private-card-8-point-1": "SimpleX-palvelimet toimivat matalan viiveen sekoitussolmuina &mdash; saapuvilla ja lähtevillä viesteillä on erilainen järjestys.",
"simplex-private-card-10-point-1": "SimpleX käyttää väliaikaisia nimettömiä parittaisia osoitteita ja tunnistetietoja jokaiselle käyttäjäkontaktille tai ryhmän jäsenelle.",
"privacy-matters-2-title": "Vaalien manipulointi",
"privacy-matters-2-overlay-1-title": "Yksityisyys antaa sinulle valtaa",
"privacy-matters-2-overlay-1-linkText": "Yksityisyys antaa sinulle valtaa",
"privacy-matters-3-title": "Syyte viattomasta yhteydestä",
"privacy-matters-3-overlay-1-title": "Yksityisyys suojaa vapauttasi",
"simplex-unique-3-title": "Sinä hallitset tietojasi",
"hero-overlay-card-1-p-4": "Tämä ratkaisu estää kaikkien käyttäjien metadatan vuotamisen sovellustason tasolla. Lisätäksesi yksityisyyttä ja suojataksesi IP-osoitteesi, voit yhdistää viestintäpalvelimiin Tor-verkon kautta.",
"hero-overlay-card-1-p-5": "Vain asiakaslaitteet tallentavat käyttäjäprofiilit, yhteystiedot ja ryhmät; viestit lähetetään 2-kerroksisella päästä päähän salauksella.",
"hero-overlay-card-2-p-1": "Kun käyttäjillä on pysyvät tunnisteet, vaikka ne olisivat vain satunnaisia numeroita, kuten istunnon tunniste, on riski, että palveluntarjoaja tai hyökkääjä voi havaita miten käyttäjät ovat yhteydessä toisiinsa ja kuinka monta viestiä he lähettävät.",
"simplex-network-overlay-card-1-p-1": "<a href='https://fi.wikipedia.org/wiki/Vertaisverkko'>P2P</a> viestintäprotokollilla ja sovelluksilla on erilaisia ongelmia, jotka tekevät niistä vähemmän luotettavia kuin SimpleX, monimutkaisempia analysoida ja alttiita useille hyökkäystyypeille.",
"privacy-matters-overlay-card-1-p-1": "Monet suuret yritykset käyttävät tietoa siitä, keiden kanssa olet yhteydessä, arvioidakseen tulojasi, myydäkseen sinulle tarpeettomia tuotteita ja määrittääkseen hinnat.",
"privacy-matters-overlay-card-1-p-2": "Verkkokauppiaat tietävät, että alhaisempiin tuloluokkiin kuuluvat ihmiset todennäköisemmin tekevät kiireellisiä ostoksia, joten he voivat veloittaa korkeampia hintoja tai poistaa alennukset.",
"simplex-unique-overlay-card-3-p-1": "SimpleX Chat tallentaa kaikki käyttäjätiedot vain asiakaslaitteille käyttäen <strong>siirrettävää salattua tietokantamuotoa</strong>, joka voidaan viedä ja siirtää mihin tahansa tuettuun laitteeseen.",
"simplex-unique-overlay-card-3-p-2": "Päästä päähän salatut viestit säilytetään väliaikaisesti SimpleX-releay-palvelimilla, kunnes ne vastaanotetaan, minkä jälkeen ne poistetaan pysyvästi.",
"simplex-unique-card-1-p-1": "SimpleX suojaa profiilisi, yhteystietosi ja metatietosi yksityisyyden, piilottaen ne SimpleX-alustan palvelimilta ja kaikilta havainnoijilta.",
"simplex-unique-card-1-p-2": "Toisin kuin millään muulla olemassa olevalla viestintäalustalla, SimpleX:llä ei ole tunnisteita käyttäjille &mdash; <strong>ei edes satunnaisia numeroita</strong>.",
"simplex-unique-card-4-p-1": "SimpleX-verkko on täysin hajautettu ja riippumaton mistään kryptovaluutasta tai mistään muusta alustasta paitsi Internetistä.",
"simplex-unique-card-4-p-2": "Voit <strong>käyttää SimpleX:ää omien palvelimiesi kanssa</strong> tai meidän tarjoamillamme palvelimilla &mdash; ja silti yhdistyä mihin tahansa käyttäjään.",
"join": "Liity",
"hide-info": "Piilota tiedot",
"contact-hero-header": "Sait osoitteen yhdistämistä varten SimpleX Chatissa",
"invitation-hero-header": "Sait kertalinkini yhdistämistä varten SimpleX Chatissa",
"contact-hero-p-2": "Et ole vielä ladannut SimpleX Chat -sovellusta?",
"privacy-matters-section-subheader": "Metatietojesi yksityisyyden säilyttäminen &mdash; <span class='text-active-blue'>keneen puhut</span> &mdash; suojaa sinua seuraavilta:",
"privacy-matters-section-label": "Varmista, ettei viestintäsovelluksesi pääse käsiksi tietoihisi!",
"simplex-private-section-header": "Mikä tekee SimpleX:stä <span class='gradient-text'>yksityisen</span>",
"simplex-network-1-header": "Toisin kuin P2P-verkot",
"simplex-network-1-overlay-linktext": "P2P-verkkojen ongelmia",
"protocol-1-text": "Signal, suuret alustat",
"protocol-2-text": "XMPP, Matrix",
"protocol-3-text": "P2P-protokollat",
"comparison-point-1-text": "Vaati globaalin identiteetin",
"comparison-point-2-text": "Mahdollisuus MITM-hyökkäykseen",
"comparison-point-3-text": "Riippuvuus DNS:stä",
"comparison-point-4-text": "Yksittäinen tai keskitetty verkko",
"yes": "Kyllä",
"no": "Ei",
"no-private": "Ei - yksityinen",
"no-secure": "Ei - turvallinen",
"no-resilient": "Ei - joustava",
"comparison-section-list-point-7": "P2P-verkoilla on joko keskitetty auktoriteetti tai koko verkko voidaan vaarantaa",
"see-here": "katso täältä",
"guide-dropdown-1": "Nopea aloitus",
"guide-dropdown-2": "Viestien lähettäminen",
"guide-dropdown-3": "Salaiset ryhmät",
"guide-dropdown-4": "Keskusteluprofiilit",
"guide-dropdown-7": "Yksityisyys ja turvallisuus",
"guide-dropdown-8": "Sovellusasetukset",
"guide-dropdown-9": "Yhteyksien luominen",
"docs-dropdown-5": "XFTP-palvelimen isännöiminen",
"docs-dropdown-6": "WebRTC-palvelimet",
"docs-dropdown-7": "Käännä SimpleX Chat",
"docs-dropdown-8": "SimpleX Hakupalvelu",
"on-this-page": "Tällä sivulla",
"back-to-top": "Takaisin ylös",
"glossary": "Sanasto",
"simplex-chat-repo": "SimpleX Chat -varasto",
"signing-key-fingerprint": "Allekirjoitusavaimen sormenjälki (SHA-256)",
"f-droid-org-repo": "F-Droid.org -varasto",
"stable-versions-built-by-f-droid-org": "Vakioversiot luotu F-Droid.org -varastoon",
"releases-to-this-repo-are-done-1-2-days-later": "Julkaisut tälle varastolle tehdään 1-2 päivää myöhemmin",
"f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat ja F-Droid.org -varastot allekirjoittavat buildit eri avaimilla. Vaihtaaksesi, <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>vienti</a> keskustelutietokanta ja asenna sovellus uudelleen.",
"hero-overlay-2-textlink": "Kuinka SimpleX toimii?",
"hero-2-header": "Luo yksityinen yhteys",
"hero-2-header-desc": "Video näyttää, kuinka muodostat yhteyden ystävääsi heidän kertakäyttöiseen QR-koodiinsa, henkilökohtaisesti tai videolinkin kautta. Voit myös liittyä jakamalla kutsulinkin kautta.",
"feature-6-title": "Päästä päähän salattuja<br>puheluita ja videopuheluja",
"feature-7-title": "Siirrettävä salattu tietokanta &mdash; siirrä profiilisi toiselle laitteelle",
"simplex-explained-tab-1-p-1": "Voit luoda yhteyshenkilöitä ja ryhmiä sekä käydä kaksisuuntaisia keskusteluja kuten missä tahansa muussa viestisovelluksessa.",
"simplex-explained-tab-3-p-1": "Palvelimilla on erilliset anonyymit tunnistetiedot kullekin jonolle, eivätkä ne tiedä, mille käyttäjille ne kuuluvat.",
"donate": "Lahjoita",
"copyright-label": "© 2020-2023 SimpleX | Avoin projekti",
"hero-p-1": "Muissa sovelluksissa on käyttäjätunnuksia: Signal, Matrix, Session, Briar, Jami, Cwtch, jne.<br> SimpleX ei käytä niitä, <strong>ei edes satunnaisia numeroita</strong>.<br> Tämä parantaa yksityisyyttäsi radikaalisti.",
"simplex-private-1-title": "2 kerrosta päästä päähän salattua viestintää",
"simplex-private-2-title": "Lisäkerros palvelimen salaukselle",
"simplex-private-3-title": "Turvallinen tunnistettu<br>TLS-tiedonsiirto",
"simplex-private-4-title": "Vaihtoehtoinen pääsy Torin kautta",
"simplex-private-5-title": "Useita tasoja<br>sisältöpakkauksia",
"simplex-private-6-title": "Avainvaihto kanavan ulkopuolella<br> (out-of-band)",
"simplex-private-8-title": "Viestien sekoitus<br>korrelaation vähentämiseksi",
"simplex-private-card-1-point-1": "Kaksoisruuviprotokolla &mdash;<br>OTR-viestintä täydellisellä eteenpäinsalauksella ja murron palautuksella.",
"simplex-private-card-1-point-2": "NaCL-kryptolaatikko kussakin jonossa estämään liikenteen korrelaatiota viestijonojen välillä, jos TLS vaarantuu.",
"simplex-private-card-2-point-1": "Lisäkerros palvelimen salaukselle vastaanottajalle toimittaessa, jotta lähetetyn ja vastaanotetun palvelinliikenteen korrelaatio estyy, jos TLS vaarantuu.",
"simplex-private-card-3-point-2": "Palvelimen sormenjälki ja kanavan sitominen estävät välikäden hyökkäykset ja toistohyökkäykset.",
"simplex-private-card-3-point-3": "Yhteyden jatkaminen on estetty istuntohyökkäysten estämiseksi.",
"simplex-private-card-4-point-1": "Suojataksesi IP-osoitettasi, voit käyttää palvelimia Tor-verkon tai jonkin muun kuljetuskerroksen päällä.",
"simplex-private-card-5-point-1": "SimpleX käyttää sisältöpakkauksia jokaiselle salauskerrokselle estämään viestikoon hyökkäyksiä.",
"simplex-private-card-5-point-2": "Se saa erikokoiset viestit näyttämään samalta palvelimille ja verkon tarkkailijoille.",
"simplex-private-card-6-point-1": "Monet viestintäalustat ovat alttiita välikäden hyökkäyksille palvelimilta tai verkko-operaattoreilta.",
"simplex-private-card-6-point-2": "Estääkseen sen, SimpleX-sovellukset siirtävät yksittäiset avaimet kanavan ulkopuolella, kun jaat osoitteen linkkinä tai QR-koodina.",
"simplex-private-card-9-point-1": "Jokainen viestijono siirtää viestejä yhteen suuntaan, eri lähettävillä ja vastaanottavilla osoitteilla.",
"simplex-private-card-9-point-2": "Se vähentää hyökkäysvektoreita verrattuna perinteisiin viestivälittimiin ja saatavilla oleviin metatietoihin.",
"simplex-private-card-10-point-2": "Se mahdollistaa viestien toimittamisen ilman käyttäjäprofiilitunnisteita, tarjoten paremman metatietosuojan kuin vaihtoehdot.",
"privacy-matters-1-title": "Mainonnan ja hintasyrjinnän estäminen",
"privacy-matters-1-overlay-1-title": "Yksityisyys säästää rahaa",
"privacy-matters-1-overlay-1-linkText": "Yksityisyys säästää rahaa",
"privacy-matters-3-overlay-1-linkText": "Yksityisyys suojaa vapauttasi",
"simplex-unique-1-title": "Sinulla on täydellinen yksityisyys",
"simplex-unique-1-overlay-1-title": "Täysi yksityisyys identiteetistäsi, profiilistasi, kontakteistasi ja metatiedoista",
"simplex-unique-2-title": "Olet suojattu roskapostilta ja väärinkäytöksiltä",
"simplex-unique-2-overlay-1-title": "Paras suoja roskapostilta ja väärinkäytöksiltä",
"simplex-unique-3-overlay-1-title": "Omistusoikeus, hallinta ja tietojesi turvallisuus",
"simplex-unique-4-title": "Omistat SimpleX-verkon",
"simplex-unique-4-overlay-1-title": "Täysin hajautettu &mdash; käyttäjät omistavat SimpleX-verkon",
"hero-overlay-card-1-p-1": "Monet käyttäjät ovat kysyneet: <em>jos SimpleX ei käytä käyttäjätunnisteita, miten se tietää minne viestit toimitetaan?</em>",
"hero-overlay-card-1-p-3": "Määrität, minkä palvelimen(t) valitset viestien vastaanottamiseen, sekä kontaktisi &mdash; palvelimet, joita käytät viestien lähettämiseen heille. Jokainen keskustelu käyttää todennäköisesti kahta eri palvelinta.",
"hero-overlay-card-1-p-6": "Lue lisää <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>SimpleX whitepaperista</a>.",
"hero-overlay-card-1-p-2": "Viestien toimittamiseen SimpleX ei käytä muiden alustojen käyttäjätunnuksia, vaan sen sijaan se käyttää väliaikaisia nimettömiä parittaisia tunnisteita viestijonoille, jotka ovat erillisiä jokaiselle yhteydelle &mdash; pitkäaikaisia tunnisteita ei ole.",
"hero-overlay-card-2-p-3": "Jopa yksityisimmillä sovelluksilla, jotka käyttävät Tor v3 -palveluja, jos puhut kahdelle eri yhteyshenkilölle saman profiilin kautta, he voivat todistaa olevansa yhteydessä samaan henkilöön.",
"hero-overlay-card-2-p-4": "SimpleX suojaa näitä hyökkäyksiä vastaan, sillä siinä ei ole käyttäjätunnuksia toteutuksessaan. Ja jos käytät Incognito-tilaa, sinulla on eri näyttönimi jokaiselle yhteyshenkilölle, mikä estää yhteisten tietojen jakamisen heidän välillään.",
"hero-overlay-card-2-p-2": "Tämän tiedon avulla he voisivat yhdistää sen olemassa oleviin julkisiin sosiaalisiin verkostoihin ja määrittää joitakin todellisia identiteettejä.",
"simplex-network-overlay-card-1-li-1": "P2P-verkot luottavat jonkinlaiseen <a href='https://fi.wikipedia.org/wiki/Hajautettu_tietokanta'>DHT</a>-varianttiin viestien reitittämiseksi. DHT-suunnittelun on tasapainotettava toimituksen varmuutta ja latenssia. SimpleX:llä on parempi toimitusvarmuus ja pienempi latenssi kuin P2P-verkoilla, koska viesti voidaan toimittaa redundanssina useiden palvelimien kautta samanaikaisesti, käyttäen vastaanottajan valitsemia palvelimia. P2P-verkoissa viesti kulkee läpi <em>O(log N)</em> solmun sekvenssissä, käyttäen algoritmin valitsemia solmuja.",
"simplex-network-overlay-card-1-li-2": "SimpleX-toteutus, toisin kuin useimmat P2P-verkot, ei käytä globaaleja käyttäjätunnisteita millään tavalla, ei edes tilapäisiä, ja käyttää ainoastaan tilapäisiä parillisia tunnisteita, tarjoten paremman anonymiteetin ja metadatansuojan.",
"simplex-network-overlay-card-1-li-6": "P2P-verkot saattavat olla alttiita <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'>DRDoS-hyökkäyksille</a>, kun asiakkaat voivat lähettää uudelleen ja voimistaa liikennettä, mikä johtaa verkko-laajuiseen palvelunestohyökkäykseen. SimpleX-asiakkaat välittävät liikennettä vain tunnetuilta yhteyksiltä eivätkä voi olla käytettävissä hyökkääjänä liikenteen voimistamiseen koko verkossa.",
"simplex-network-overlay-card-1-li-3": "P2P ei ratkaise <a href='https://fi.wikipedia.org/wiki/V%C3%A4lik%C3%A4den_hy%C3%B6kk%C3%A4ys'>välikäden hyökkäys</a> -ongelmaa, ja useimmat olemassa olevat toteutukset eivät käytä kanavan ulkopuolisia viestejä alkuperäiseen avaimenvaihtoon. SimpleX käyttää kanavan ulkopuolisia viestejä tai, joissakin tapauksissa, jo valmiiksi turvallisia ja luotettuja yhteyksiä alkuperäiseen avaimenvaihtoon.",
"simplex-network-overlay-card-1-li-4": "P2P-toteutukset voivat estyä joidenkin Internet-palveluntarjoajien (kuten <a href='https://fi.wikipedia.org/wiki/BitTorrent'>BitTorrent</a>) toimesta. SimpleX on kuljetusprotokollasta riippumaton - se voi toimia yleisten verkkoprotokollien kautta, kuten esimerkiksi WebSockets.",
"simplex-network-overlay-card-1-li-5": "Kaikki tunnetut P2P-verkot saattavat olla alttiita <a href='https://fi.wikipedia.org/wiki/Sybil-hy%C3%B6kk%C3%A4ys'>Sybil-hyökkäykselle</a>, koska jokainen solmu on löydettävissä, ja verkko toimii kokonaisuutena. Tunnetut keinot sen lieventämiseksi vaativat joko keskitetyn komponentin tai kalliin <a href='https://fi.wikipedia.org/wiki/Proof_of_work'>työn todistuksen</a>. SimpleX-verkolla ei ole palvelimen löydettävyyttä, se on fragmentoitunut ja toimii useina eristettyinä aliverkkoina, mikä tekee verkko-laajuisista hyökkäyksistä mahdottomia.",
"privacy-matters-overlay-card-1-p-3": "Jotkut rahoitus- ja vakuutusyhtiöt käyttävät sosiaalisia verkostoja määrittääkseen korkoja ja vakuutusmaksuja. Se tekee usein alhaisempiin tuloihin kuuluvien ihmisten maksavan enemmän &mdash; sitä kutsutaan <a href='https://fairbydesign.com/povertypremium/' target='_blank'>'köyhyyslisäksi'</a>.",
"privacy-matters-overlay-card-1-p-4": "SimpleX-alusta suojaa yhteyksiesi yksityisyyttä paremmin kuin mikään vaihtoehto, estäen täysin yhteysverkkosi tulemisen saataville mille tahansa yrityksille tai organisaatioille. Vaikka ihmiset käyttävät SimpleX Chatin tarjoamia palvelimia, emme tiedä käyttäjien määrää tai heidän yhteyksiään.",
"privacy-matters-overlay-card-2-p-3": "SimpleX on ensimmäinen alusta, jolla ei ole mitään käyttäjätunnisteita suunnittelussaan, suojaten siten yhteyksesi verkkoa paremmin kuin mikään tunnettu vaihtoehto.",
"privacy-matters-overlay-card-2-p-1": "Ei niin kauan sitten huomasimme merkittävien vaalien olevan manipuloitavissa <a href='https://en.wikipedia.org/wiki/FacebookCambridge_Analytica_data_scandal' target='_blank'>kunnioitetun konsulttiyrityksen</a> toimesta, joka käytti sosiaalisia verkostoja vääristämään käsitystämme todellisesta maailmasta ja manipuloimaan ääniämme.",
"privacy-matters-overlay-card-3-p-1": "Kaikkien pitäisi välittää viestinnän yksityisyydestä ja turvallisuudesta &mdash; harmittomat keskustelut voivat asettaa sinut vaaraan, vaikka sinulla ei olisi mitään piilotettavaa.",
"privacy-matters-overlay-card-2-p-2": "Ollaksesi objektiivinen ja tehdäksesi itsenäisiä päätöksiä, sinun on hallittava tietotilaasi. Se on mahdollista vain, jos käytät yksityistä viestintäalustaa, jolla ei ole pääsyä sosiaaliseen verkostoosi.",
"privacy-matters-overlay-card-3-p-2": "Yksi järkyttävimmistä tarinoista on <a href='https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi' target='_blank'>Mohamedou Ould Salahi'n</a> kokemus, joka on kuvattu hänen muistelmissaan ja esitetty The Mauritanian -elokuvassa. Hänet laitettiin Guantanamo-leirille ilman oikeudenkäyntiä ja häntä kidutettiin siellä 15 vuotta puhelun jälkeen sukulaiselleen Afganistanissa, epäiltynä osallisuudesta 9/11-iskuihin, vaikka hän oli asunut Saksassa edelliset 10 vuotta.",
"privacy-matters-overlay-card-3-p-3": "Tavalliset ihmiset pidätetään siitä, mitä he jakavat verkossa, jopa 'anonyymien' tiliensä kautta, <a href='https://www.dailymail.co.uk/news/article-11282263/Moment-police-swoop-house-devout-catholic-mother-malicious-online-posts.html' target='_blank'>jopa demokraattisissa maissa</a>.",
"privacy-matters-overlay-card-3-p-4": "Ei riitä, että käytät päästä päähän salattua viestintäsovellusta, meidän kaikkien pitäisi käyttää viestintäsovelluksia, jotka suojelevat henkilökohtaisten verkostojemme yksityisyyttä &mdash; keiden kanssa olemme yhteydessä.",
"simplex-unique-overlay-card-1-p-1": "Toisin kuin muut viestintäalustat, SimpleX:llä ei ole <strong>mitään tunnisteita käyttäjille</strong>. Se ei luota puhelinnumeroihin, verkkotunnuksiin perustuviin osoitteisiin (kuten sähköposti tai XMPP), käyttäjänimiin, julkisiin avaimiin tai edes satunnaisiin numeroihin tunnistaakseen käyttäjänsä &mdash; emme tiedä kuinka monta ihmistä käyttää SimpleX-palvelimiamme.",
"simplex-unique-overlay-card-1-p-2": "Viestien toimittamiseksi SimpleX käyttää <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>parittaisia nimettömiä osoitteita</a> kaksisuuntaisille viestijonoille, jotka ovat erilliset vastaanotetuille ja lähetetyille viesteille, yleensä eri palvelimien kautta. SimpleX:n käyttö on kuin <strong>eri &ldquo;kertakäyttöinen&rdquo; sähköposti tai puhelin jokaiselle yhteydelle</strong>, eikä sinun tarvitse vaivautua niiden hallitsemiseen.",
"simplex-unique-overlay-card-1-p-3": "Tämä suunnittelu suojaa sitä, kenen kanssa kommunikoit, piilottamalla sen SimpleX-alustan palvelimilta ja kaikilta havainnoijilta. Piilottaaksesi IP-osoitteesi palvelimilta, voit <strong>yhdistää SimpleX-palvelimiin Tor-verkon kautta</strong>.",
"simplex-unique-overlay-card-2-p-1": "Koska sinulla ei ole tunnistetta SimpleX-alustalla, kukaan ei voi ottaa sinuun yhteyttä, ellei jaa kertakäyttöistä tai väliaikaista käyttäjäosoitetta, kuten QR-koodia tai linkkiä.",
"simplex-unique-overlay-card-2-p-2": "Jopa valinnaisen käyttäjäosoitteen kanssa, vaikka sitä voitaisiin käyttää roskapostiyhteyspyyntöjen lähettämiseen, voit vaihtaa sen tai poistaa sen kokonaan menettämättä mitään yhteyksiäsi.",
"simplex-unique-overlay-card-3-p-3": "Toisin kuin liitettyjen verkkojen palvelimet (sähköposti, XMPP tai Matrix), SimpleX-palvelimet eivät tallenna käyttäjätilejä, ne vain välittävät viestejä, suojaten molempien osapuolien yksityisyyttä.",
"simplex-unique-overlay-card-3-p-4": "Lähetetyssä ja vastaanotetussa palvelimen liikenteessä ei ole yhteisiä tunnisteita tai salaustekstiä &mdash; jos joku havaitsee sen, he eivät voi helposti selvittää kuka kommunikoi kenen kanssa, vaikka TLS olisi vaarantunut.",
"simplex-unique-overlay-card-4-p-3": "Jos harkitset kehittämistä SimpleX-alustalle, esimerkiksi chat-botin luomista SimpleX-sovelluksen käyttäjille tai SimpleX Chat -kirjaston integrointia mobiilisovelluksiisi, ole hyvä ja <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>ota yhteyttä</a> saadaksesi neuvoja ja tukea.",
"simplex-unique-overlay-card-4-p-1": "Voit <strong>käyttää SimpleX:ää omien palvelimiesi kanssa</strong> ja silti kommunikoida ihmisten kanssa, jotka käyttävät meille tarjottuja valmiiksi määritettyjä palvelimia.",
"simplex-unique-overlay-card-4-p-2": "SimpleX-alusta käyttää <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>avoimeen protokollaan</a> ja tarjoaa <a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>SDK:n chatbotien luomiseen</a>, mahdollistaen palvelujen toteuttamisen, joiden kanssa käyttäjät voivat olla vuorovaikutuksessa SimpleX Chat -sovellusten kautta &mdash; odotamme innolla nähdä, millaisia SimpleX-palveluja voit rakentaa.",
"simplex-unique-card-3-p-1": "SimpleX tallentaa kaikki käyttäjätiedot asiakaslaitteille <strong>siirrettävässä salatussa tietokannan muodossa</strong> &mdash; se voidaan siirtää toiseen laitteeseen.",
"simplex-unique-card-3-p-2": "Päästä päähän salatut viestit säilytetään väliaikaisesti SimpleX-releay-palvelimilla, kunnes ne vastaanotetaan, minkä jälkeen ne poistetaan pysyvästi.",
"we-invite-you-to-join-the-conversation": "Kutsumme sinut mukaan keskusteluun",
"join-the-REDDIT-community": "Liity REDDIT-yhteisöön",
"simplex-unique-card-2-p-1": "Koska sinulla ei ole tunnistetta tai kiinteää osoitetta SimpleX-alustalla, kukaan ei voi ottaa sinuun yhteyttä, ellet jaa kertakäyttöistä tai väliaikaista käyttäjäosoitetta, kuten QR-koodia tai linkkiä.",
"join-us-on-GitHub": "Liity GitHubissa",
"donate-here-to-help-us": "Tue meitä täällä lahjoituksilla",
"sign-up-to-receive-our-updates": "Tilaa päivityksemme",
"enter-your-email-address": "Syötä sähköpostiosoitteesi",
"get-simplex": "Hanki SimpleX",
"why-simplex-is": "Miksi SimpleX on",
"unique": "ainutlaatuinen",
"learn-more": "Lue lisää",
"more-info": "Lisätietoja",
"contact-hero-subheader": "Skannaa QR-koodi SimpleX Chat -sovelluksella puhelimessasi tai tabletissasi.",
"contact-hero-p-1": "Tämän linkin julkiset avaimet ja viestijonon osoite eivät lähetä verkkoa pitkin, kun katsot tätä sivua &mdash; ne sisältyvät linkin URL:n hash-osaan.",
"contact-hero-p-3": "Käytä alla olevia linkkejä ladataksesi sovelluksen.",
"scan-qr-code-from-mobile-app": "Skannaa QR-koodi mobiilisovelluksesta",
"to-make-a-connection": "Yhteyden muodostaminen:",
"install-simplex-app": "Asenna SimpleX-sovellus",
"connect-in-app": "Yhdisty sovelluksessa",
"open-simplex-app": "Avaa Simplex-sovellus",
"tap-the-connect-button-in-the-app": "Napauta sovelluksessa olevaa <span class='text-active-blue'>yhdistä</span>-painiketta",
"scan-the-qr-code-with-the-simplex-chat-app": "Skannaa QR-koodi SimpleX Chat -sovelluksella",
"scan-the-qr-code-with-the-simplex-chat-app-description": "Tämän linkin julkiset avaimet ja viestijonon osoite eivät lähetä verkkoa pitkin, kun katsot tätä sivua &mdash;<br> ne sisältyvät linkin URL:n hash-osaan.",
"installing-simplex-chat-to-terminal": "Asentaminen SimpleX Chat terminaaliin",
"see-simplex-chat": "Katso SimpleX Chat",
"use-this-command": "Käytä tätä komentoa:",
"github-repository": "GitHub-varasto",
"the-instructions--source-code": "ohjeet, miten ladata tai kääntää se lähdekoodista.",
"if-you-already-installed-simplex-chat-for-the-terminal": "Jos olet jo asentanut SimpleX Chatin terminaaliin",
"if-you-already-installed": "Jos olet jo asentanut",
"simplex-chat-for-the-terminal": "SimpleX Chatin terminaaliin",
"copy-the-command-below-text": "kopioi alla oleva komento ja käytä sitä keskustelussa:",
"privacy-matters-section-header": "Miksi yksityisyys on <span class='gradient-text'>tärkeää</span>",
"tap-to-close": "Napauta sulkeaksesi",
"simplex-network-section-header": "SimpleX <span class='gradient-text'>Verkko</span>",
"simplex-network-section-desc": "Simplex Chat tarjoaa parhaan yksityisyyden yhdistämällä P2P-verkkojen ja liitettävien verkkojen edut.",
"simplex-network-1-desc": "Kaikki viestit lähetetään palvelimien kautta, mikä sekä parantaa metatietojen yksityisyyttä että mahdollistaa luotettavan asynkronisen viestien toimituksen, samalla välttäen monia",
"simplex-network-2-header": "Toisin kuin liitettävät verkot",
"simplex-network-3-desc": "palvelimet tarjoavat <span class='text-active-blue'>yksisuuntaisia jonopalveluja</span> yhdistääkseen käyttäjät, mutta niillä ei ole näkyvyyttä verkon yhteyskarttaan &mdash; ainoastaan käyttäjillä on.",
"comparison-section-header": "Vertailu muihin protokolliin",
"simplex-network-2-desc": "SimpleXin relea-palvelimet EIVÄT tallenna käyttäjäprofiileja, yhteystietoja ja toimitettuja viestejä, ne EIVÄT yhdisty toisiinsa, eikä ole OLE olemassa palvelinluetteloa.",
"simplex-network-3-header": "SimpleX-verkko",
"comparison-point-5-text": "Keskuskomponentti tai muu verkkoa koskeva hyökkäys",
"no-decentralized": "Ei - hajautettu",
"no-federated": "Ei - liitetty",
"comparison-section-list-point-1": "Yleensä pohjautuu puhelinnumeroon, joissain tapauksissa käyttäjänimiin",
"comparison-section-list-point-2": "DNS-pohjaiset osoitteet",
"comparison-section-list-point-3": "Julkinen avain tai jokin muu maailmanlaajuisesti uniikki tunniste",
"comparison-section-list-point-4a": "SimpleXin releapalvelimet eivät voi vaarantaa päästä päähän -salausta. Tarkista turvakoodi hyödyntääksesi hyökkäyssuojaa ylitys-kanavalla",
"comparison-section-list-point-5": "Ei suojaa käyttäjien metatietojen yksityisyyttä",
"comparison-section-list-point-6": "Vaikka P2P-verkot ovat hajautettuja, ne eivät ole liitettäviä - ne toimivat yhtenä verkostona",
"guide-dropdown-5": "Datan hallinta",
"guide-dropdown-6": "Ääni- ja videopuhelut",
"comparison-section-list-point-4": "Jos operaattorin palvelimet ovat vaarantuneet. Tarkista turvakoodi Signalissa ja joissakin muissa sovelluksissa suojautuaksesi hyökkäykseltä",
"guide": "Opas",
"docs-dropdown-1": "SimpleX-alusta",
"docs-dropdown-2": "Android-tiedostoihin pääseminen",
"docs-dropdown-3": "Keskustelutietokantaan pääseminen",
"docs-dropdown-4": "SMP-palvelimen isännöiminen",
"newer-version-of-eng-msg": "Tästä sivusta on uudempi versio englanniksi.",
"click-to-see": "Näytä napsauttamalla",
"menu": "Valikko",
"simplex-chat-via-f-droid": "SimpleX Chat F-Droidin kautta",
"stable-and-beta-versions-built-by-developers": "Kehittäjien luomat vakaat ja beta-versiot",
"f-droid-page-simplex-chat-repo-section-text": "Lisätäksesi sen F-Droid-asiakkaaseesi, <span class='hide-on-mobile'>skannaa QR-koodi tai</span> käytä tätä URL-osoitetta:"
}

View File

@ -205,8 +205,8 @@
"comparison-section-list-point-1": "Généralement basé sur un numéro de téléphone, dans certains cas sur des noms d'utilisateur",
"comparison-section-list-point-2": "Adresses basées sur le DNS",
"comparison-section-list-point-3": "Clé publique ou tout autre identifiant global unique",
"comparison-section-list-point-4": "Si les serveurs de l'opérateur sont compromis",
"comparison-section-list-point-5": "Ne protège pas les métadonnées des utilisateurs",
"comparison-section-list-point-4": "Si les serveurs de l'opérateur sont compromis. Vérifier les codes de sécurités sur Signal et d'autres applications pour limiter les risques",
"comparison-section-list-point-5": "Ne protège pas la confidentialité des métadonnées des utilisateurs",
"comparison-section-list-point-6": "Bien que les P2P soient distribués, ils ne sont pas fédérés - ils fonctionnent comme un seul réseau",
"comparison-section-list-point-7": "Les réseaux P2P ont soit une autorité centrale, soit l'ensemble du réseau peut être compromis",
"voir-ici": "voir ici",
@ -240,8 +240,9 @@
"simplex-chat-via-f-droid": "SimpleX Chat via F-Droid",
"simplex-chat-repo": "Dépot SimpleX Chat",
"stable-and-beta-versions-built-by-developers": "Versions stables et bêta crées par les développeurs",
"f-droid-page-simplex-chat-repo-section-text": "Pour l'ajouter à votre client F-Droid <span class='hide-on-mobile'>scannez le code QR ou</span> utilisez cette URL:",
"f-droid-page-simplex-chat-repo-section-text": "Pour l'ajouter à votre client F-Droid <span class='hide-on-mobile'>scannez le code QR ou</span> utilisez cette URL :",
"signing-key-fingerprint": "Empreinte de signature numérique (SHA-256)",
"f-droid-org-repo": "Dépot F-Droid.org",
"stable-versions-built-by-f-droid-org": "Versions stables créées par F-Droid.org"
}
"stable-versions-built-by-f-droid-org": "Versions stables créées par F-Droid.org",
"comparison-section-list-point-4a": "Les relais SimpleX ne peuvent pas compromettre le chiffrement e2e. Vérifier le code de sécurité pour limiter les attaques sur le canal hors bande"
}

Some files were not shown because too many files have changed in this diff Show More