Compare commits

..

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] deb4e356bd Fix race condition by tracking removed contacts explicitly
Instead of comparing current members at result time (which can change),
ContactMultiSelectionActivity now tracks preselected contacts and
explicitly returns which ones were removed. This eliminates the race
condition where member list changes could cause accidental removals.

Changes:
- ContactMultiSelectionActivity: Store preselected contacts, calculate and return removed contacts
- ProfileFragment: Use explicitly removed contacts instead of calculating from current member list

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-02-02 23:03:32 +00:00
copilot-swe-agent[bot] 88d6992a9e Optimize member removal with HashSet for O(1) lookup
- Use HashSet instead of List for checking member selection
- Improves performance from O(n²) to O(n) for large member lists

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-02-02 17:20:55 +00:00
copilot-swe-agent[bot] 5e6b09031a Fix member removal in Add Members screen
- Modified ProfileFragment.onActivityResult() to handle both adding and removing members
- When unchecking members in the Add Members screen, they are now properly removed from the group
- Preserves existing functionality for adding new members

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-02-02 17:19:59 +00:00
copilot-swe-agent[bot] 115ebf52b6 Initial plan 2026-02-02 17:17:22 +00:00
142 changed files with 1890 additions and 4012 deletions
+2 -14
View File
@@ -1,7 +1,6 @@
# Delta Chat Android Changelog
## v2.43.0
2026-02
## Unreleased
* Improve switch speed when changing profiles
* Allow to switch profile when sharing or forwarding
@@ -13,24 +12,13 @@
* Mark external links with " ↗" to make them clear
* Make QR code larger on "Add Second Device" screen
* Add indication for blocked contacts in user profile
* Allow to start calls with video disabled
* Show hint for empty contact search results
* Add background playing for voice messages and other audio files
* Allow scanning Invitation Code when creating a new profile
* Add context menu in long-pressing relays items instead of showing buttons
* Enhanced video player UI
* Fix: Show dialog if pasted QR codes are invalid
* Fix: Refresh chat list when returning from conversation if selected profile changed
* Fix: Update menu when using "select all" in contact selection
* Fix: Avoid empty profiles after using "add as second device" from welcome screen
* Fix: Remove from group deselected members in the contact selection list
* Fix multi-device seen messages synchronization when using multiple relays
* Fix mailto handling
* Fix layout problems inside in-chat apps
* Fix real-time for in-chat apps that need it
* Avoid crash when the app is minimized with profile switcher or reactions dialogs open
* Remove "trash icon" option from contact selection list when adding members to group
* Update to core 2.43.0
* Update to core 2.39.0
## v2.35.0
2026-01
+3 -8
View File
@@ -33,8 +33,8 @@ android {
useLibrary 'org.apache.http.legacy'
defaultConfig {
versionCode 30000737
versionName "2.43.0"
versionCode 30000736
versionName "2.36.0"
applicationId "chat.delta.lite"
multiDexEnabled true
@@ -211,8 +211,6 @@ dependencies {
implementation "io.noties.markwon:inline-parser:$markwon_version"
implementation 'com.airbnb.android:lottie:4.2.2' // Lottie animations support.
def media3_version = "1.8.0" // 1.9.0 need minSdkVersion 23
implementation 'androidx.concurrent:concurrent-futures:1.3.0'
implementation 'androidx.sharetarget:sharetarget:1.2.0'
implementation 'androidx.webkit:webkit:1.14.0'
@@ -233,11 +231,8 @@ dependencies {
implementation 'androidx.work:work-runtime:2.9.1'
implementation 'androidx.emoji2:emoji2-emojipicker:1.5.0'
implementation 'com.google.guava:guava:31.1-android'
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' // FIXME: exoplayer dependencies kept for Video, but we shall migrate them at some point
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' // plays video and audio
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
implementation "androidx.media3:media3-exoplayer:$media3_version"
implementation "androidx.media3:media3-session:$media3_version"
implementation "androidx.media3:media3-ui:$media3_version"
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation 'com.google.zxing:core:3.3.0' // fixed version to support SDK<24
implementation ('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false } // QR Code scanner
+2 -38
View File
@@ -153,42 +153,6 @@ static uint32_t* jintArray2uint32Pointer(JNIEnv* env, jintArray ja, int* ret_icn
}
/************************************************************
* DcEventChannel
************************************************************/
static dc_event_channel_t* get_dc_event_channel(JNIEnv *env, jobject obj)
{
static jfieldID fid = 0;
if (fid==0) {
jclass cls = (*env)->GetObjectClass(env, obj);
fid = (*env)->GetFieldID(env, cls, "eventChannelCPtr", "J" /*Signature, J=long*/);
}
if (fid) {
return (dc_event_channel_t*)(*env)->GetLongField(env, obj, fid);
}
return NULL;
}
JNIEXPORT jlong Java_com_b44t_messenger_DcEventChannel_createEventChannelCPtr(JNIEnv *env, jobject obj)
{
return (jlong)dc_event_channel_new();
}
JNIEXPORT void Java_com_b44t_messenger_DcEventChannel_unrefEventChannelCPtr(JNIEnv *env, jobject obj)
{
dc_event_channel_unref(get_dc_event_channel(env, obj));
}
JNIEXPORT jlong Java_com_b44t_messenger_DcEventChannel_getEventEmitterCPtr(JNIEnv *env, jobject obj)
{
return (jlong)dc_event_channel_get_event_emitter(get_dc_event_channel(env, obj));
}
/*******************************************************************************
* DcAccounts
******************************************************************************/
@@ -208,11 +172,11 @@ static dc_accounts_t* get_dc_accounts(JNIEnv *env, jobject obj)
}
JNIEXPORT jlong Java_com_b44t_messenger_DcAccounts_createAccountsCPtr(JNIEnv *env, jobject obj, jstring dir, jobject chanObj)
JNIEXPORT jlong Java_com_b44t_messenger_DcAccounts_createAccountsCPtr(JNIEnv *env, jobject obj, jstring dir)
{
CHAR_REF(dir);
int writable = 1;
jlong accountsCPtr = (jlong)dc_accounts_new_with_event_channel(dirPtr, writable, get_dc_event_channel(env, chanObj));
jlong accountsCPtr = (jlong)dc_accounts_new(dirPtr, writable);
CHAR_UNREF(dir);
return accountsCPtr;
}
@@ -1,17 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="228"
android:viewportHeight="280">
<group android:scaleX="0.35014287"
android:scaleY="0.43"
android:translateX="74.08372"
android:translateY="79.8">
<path
android:pathData="m10.03,234.14c0.3,-0.01 0.6,-0.02 0.9,-0.04 -0.07,-0.49 -0.14,-0.97 -0.2,-1.46 -0.15,-1.34 -0.26,-2.69 -0.25,-4.04 -0.02,-0.86 -0.05,-1.71 -0.07,-2.57 -0.09,-0.06 -0.18,-0.13 -0.27,-0.19 -0.02,-0.02 -0.04,-0.03 -0.07,-0.05zM44.87,232.95c11.71,-8.35 26.86,-14.79 46.21,-15.9 0,0 39.93,-0.27 47.91,-3.53 7.98,-3.26 68.68,-14.69 82.94,-98.43 14.26,-83.74 -1.06,-115.09 -1.06,-115.09 0,0 -21.14,55.68 -81.02,59.81 0,0 -14.5,1.03 -38.82,1.42 -24.32,0.39 -75.77,20.65 -90.55,85.62l-0.22,43.44c2.5,4.22 5.49,8.12 8.91,11.66 3.99,4.11 8.11,8.12 12.79,11.45 2.26,1.65 4.65,3.2 6.51,5.33 1.94,2.34 3.33,5 4.2,7.93 0.71,2.1 1.45,4.2 2.2,6.28z"
android:fillColor="@color/ic_launcher_background"/>
<path
android:pathData="m217.97,45.86c-0.3,0.01 -0.6,0.02 -0.9,0.04 0.07,0.49 0.14,0.97 0.2,1.46 0.15,1.34 0.26,2.69 0.25,4.04 0.02,0.86 0.05,1.71 0.07,2.57 0.09,0.06 0.18,0.13 0.27,0.19 0.02,0.02 0.04,0.03 0.07,0.05zM183.13,47.05c-11.71,8.35 -26.86,14.79 -46.21,15.9 0,0 -39.93,0.27 -47.91,3.53 -7.98,3.26 -68.68,14.69 -82.94,98.43 -14.26,83.74 1.06,115.09 1.06,115.09 0,0 21.14,-55.68 81.02,-59.81 0,0 14.5,-1.03 38.82,-1.42 24.32,-0.39 75.77,-20.65 90.55,-85.62l0.22,-43.44c-2.5,-4.22 -5.49,-8.12 -8.91,-11.66 -3.99,-4.11 -8.11,-8.12 -12.79,-11.45 -2.26,-1.65 -4.65,-3.2 -6.51,-5.33 -1.94,-2.34 -3.33,-5 -4.2,-7.93 -0.71,-2.1 -1.45,-4.2 -2.2,-6.28z"
android:fillColor="@color/ic_launcher_background"/>
</group>
</vector>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
-11
View File
@@ -38,7 +38,6 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<!-- force compiling emojipicker on sdk<21; runtime checks are required then -->
<uses-sdk tools:overrideLibrary="androidx.emoji2.emojipicker"/>
@@ -93,7 +92,6 @@
<intent-filter>
<data android:scheme="mailto"/>
<action android:name="android.intent.action.SENDTO"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
</intent-filter>
@@ -392,15 +390,6 @@
android:name=".service.FetchForegroundService"
android:foregroundServiceType="dataSync" />
<service
android:name=".service.AudioPlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
</intent-filter>
</service>
<receiver android:name=".notifications.MarkReadReceiver"
android:enabled="true"
android:exported="false">
File diff suppressed because one or more lines are too long
+16 -16
View File
@@ -1188,10 +1188,6 @@ to exchange encryption setup information through QR-code scanning or “invite l
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
@@ -1376,10 +1372,12 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1455,7 +1453,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1466,9 +1464,12 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<h3 id="pqc">
@@ -1478,13 +1479,12 @@ This approach is specified in the <a href="https://datatracker.ietf.org/doc/draf
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<h3 id="how-can-i-manually-check-encryption-information">
+74 -66
View File
@@ -1045,31 +1045,31 @@ für die <a href="https://chatmail.at/clients">Chatmail-Clients</a> umfasst, von
</h3>
<p>Wir möchten Delta Chat mit deiner Hilfe verbessern.
Deshalb fragt Delta Chat für Android, ob du
anonyme Nutzungsstatistiken senden möchtest.</p>
<p>We would like to improve Delta Chat with your help,
which is why Delta Chat for Android asks whether you want
to send anonymous usage statistics.</p>
<p>Du kannst dies unter
<strong>Einstellungen → Erweitert → Statistik an Delta Chat Entwickler senden</strong> ein- und ausschalten.</p>
<p>You can turn it on and off at
<strong>Settings → Advanced → Send statistics to Delta Chats developers</strong>.</p>
<p>Wenn eingeschaltet,
werden wöchentlich Statistiken automatisch an einen Bot gesendet.</p>
<p>When you turn it on,
weekly statistics will be automatically sent to a bot.</p>
<p>Wir sind beispielsweise an folgenden Statistiken interessiert:</p>
<p>We are interested e.g. in statistics like:</p>
<ul>
<li>
<p>Wie viele Kontakte werden durch das persönliche Scannen eines QR-Codes hergestellt?</p>
<p>How many contacts are introduced by personally scanning a QR code?</p>
</li>
<li>
<p>Welche Versionen von Delta Chat werden verwendet?</p>
<p>Which versions of Delta Chat are being used?</p>
</li>
<li>
<p>Welche Fehler treten bei Benutzern auf?</p>
<p>What errors occur for users?</p>
</li>
</ul>
<p>Wir werden <em>keinerlei</em> personenbezogene Daten über dich sammeln.</p>
<p>We will <em>not</em> collect any personally identifiable information about you.</p>
<h3 id="ich-bin-an-technischen-details-interessiert-gibt-es-hierzu-weitere-infos">
@@ -1107,10 +1107,6 @@ zum Austausch von Verschlüsselungsinformationen durch Scannen von QR-Codes oder
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> wird verwendet, um automatisch eine Ende-zu-Ende-Verschlüsselung zwischen Kontakten und allen Mitgliedern einer Gruppe herzustellen.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, dessen vollständige Implementierung für 2026 geplant ist,
wir post-quantum-resistente Verschlüsselung und Forward Secrecy einführen.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Teilen eines Kontakts im Chat</a>
ermöglicht es den Empfängern, eine Ende-zu-Ende-Verschlüsselung mit dem Kontakt zu verwenden.</p>
@@ -1284,13 +1280,15 @@ selbst wenn die Nachricht nicht Ende-zu-Ende-verschlüsselt war.</p>
speichern Delta-Chat-Apps keine Metadaten über Kontakte oder Gruppen auf Servern. Auch nicht in verschlüsselter Form.
Stattdessen werden alle Gruppen-Metadaten durchgängig verschlüsselt und ausschließlich auf den Endgeräten der Nutzer gespeichert.</p>
<p>Server können daher nur das folgende sehen:</p>
<p>Servers can therefore only see:</p>
<ul>
<li>Absender- und Empfängeradressen, standardmäßig zufällig generiert</li>
<li>Größe der Nachricht</li>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>Alle anderen Metadaten zu Nachrichten, Kontakten und Gruppen befinden sich im Ende-zu-Ende-verschlüsselten Teil der Nachrichten.</p>
<h3 id="device-seizure">
@@ -1301,15 +1299,15 @@ Stattdessen werden alle Gruppen-Metadaten durchgängig verschlüsselt und aussch
</h3>
<p>Sowohl zum Schutz vor Servern, die Metadaten sammeln,
als auch als Schutz bei Beschlagnahmung von Geräten
empfehlen wir die Verwendung eines <a href="https://chatmail.at/relays">Chatmail-Relays</a>,
um Chat-Profile mit zufälligen Adressen für den Transport zu erstellen.
Beachte, dass Delta-Chat-Apps mehrere Profile unterstützen,
sodass du neben deinem „Hauptprofil” ganz einfach situationsspezifische Profile verwenden kannst,
mit der Gewissheit, dass alle Daten sowie alle Metadaten gelöscht werden.
Darüber hinaus können Chat-Kontakte, die kurzlebige Profile verwenden,
im Falle einer Beschlagnahmung des Geräts nicht ohne Weiteres identifiziert werden.</p>
<p>Both for protecting against metadata-collecting servers
as well as against the threat of device seizure
we recommend to use a <a href="https://chatmail.at/relays">chatmail relay</a>
to create chat profiles using random addresses for transport.
Note that Delta Chat apps on all platforms support multiple profiles
so you can easily use situation-specific profiles next to your “main” profile
with the knowledge that all their data, along with all metadata, will be deleted.
Moreover, if a device is seized then chat contacts using short-lived profiles
can not be identified easily.</p>
<h3 id="wer-sieht-meine-ip-adresse">
@@ -1352,10 +1350,11 @@ um seine Serverinfrastruktur darüber im Unklaren zu lassen, wer eine Nachricht
Dies ist besonders wichtig, weil der Signal-Server die Handynummer jedes Kontos kennt,
die in der Regel mit einer Passidentität verbunden ist.</p>
<p>Auch wenn <a href="https://chatmail.at/relays">Chatmail-Relays</a>
keine privaten Daten (einschließlich Telefonnummern) abfragen,
könnte es dennoch sinnvoll sein, Metadaten zwischen Adressen zu schützen.
Wir sehen keine größeren Probleme bei der Verwendung von zufälligen Wegwerfadressen für aber eine Umsetzung wurde noch nicht als priorisiert.</p>
<p>Even if <a href="https://chatmail.at/relays">chatmail relays</a>
do not ask for any private data (including no phone numbers),
it might still be worthwhile to protect relational metadata between addresses.
We dont foresee bigger problems in using random throw-away addresses for sealed sending
but an implementation has not been agreed as a priority yet.</p>
<h3 id="pfs">
@@ -1365,20 +1364,23 @@ Wir sehen keine größeren Probleme bei der Verwendung von zufälligen Wegwerfad
</h3>
<p>Noch nicht, aber es kommt mit <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Nein, noch nicht.</p>
<p>Delta Chat unterstützt derzeit keine Perfect Forward Secrecy (PFS).
Das bedeutet, dass, wenn Ihr privater Schlüssel offengelegt wird
und jemand Ihre früheren Nachrichten während der Übertragung gesammelt hat,
diese mit dem offengelegten Schlüssel entschlüsselt und gelesen werden können.
Beachten Sie, dass Forward Secrecy die Sicherheit nur erhöht, wenn du Nachrichten löschst.
Andernfalls kann jemand, der deinen Schlüssel erhält,
in der Regel auch alle deine nicht gelöschten Nachrichten abrufen
und muss zuvor gesammelte Nachrichten nicht einmal entschlüsseln.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
and someone has collected your prior in-transit messages,
they will be able to decrypt and read them using the leaked decryption key.
Note that Forward Secrecy only increases security if you delete messages.
Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, dessen vollständige Implementierung für 2026 geplant ist,
wird durch automatische Schlüsselrotation eine zuverlässige Löschung (Forward Secrecy) gewährleisten.
Dieser Ansatz ist im Entwurf <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> festgelegt.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<h3 id="pqc">
@@ -1388,13 +1390,11 @@ Dieser Ansatz ist im Entwurf <a href="https://datatracker.ietf.org/doc/draft-aut
</h3>
<p>Noch nicht, aber es kommt mit <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Nein, noch nicht.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, dessen vollständige Implementierung für 2026 geplant ist,
wird eine post-quantum-resistente Verschlüsselung zum Schutz vor Angriffen durch Quantencomputer bieten.
Delta Chat verwendet die Rust-OpenPGP-Bibliothek <a href="https://github.com/rpgp/rpgp">rPGP</a>,
die den neuesten <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Kryptografie OpenPGP Entfurf</a> unterstützt.
Die Implementierung ist in <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> festgelegt.</p>
<p>Delta Chat verwendet die Rust OpenPGP-Bibliothek <a href="https://github.com/rpgp/rpgp">rPGP</a>
die den neuesten <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP Entwurf</a> unterstützt.
Wir beabsichtigen, PQC-Unterstützung zum <a href="https://github.com/chatmail/core">chatmail core</a> hinzuzufügen, sobald der Entwurf bei der IETF in Zusammenarbeit mit anderen OpenPGP-Implementierern fertiggestellt ist.</p>
<h3 id="wie-kann-ich-die-verschlüsselung-manuell-überprüfen">
@@ -1420,11 +1420,12 @@ ist die Verbindung sicher.</p>
<p>Nein.</p>
<p>Delta Chat generiert sichere OpenPGP-Schlüssel gemäß der Autocrypt-Spezifikation 1.1.
Wir bieten Benutzern keine manuelle Schlüsselverwaltung an, noch empfehlen diese.
Wir wollen sicherstellen, dass sich Sicherheitsüberprüfungen auf einige wenige bewährte kryptografische Algorithmen konzentrieren können,
anstatt auf die gesamte Bandbreite der mit OpenPGP zulässigen Algorithmen.
Wenn Sie Ihren OpenPGP-Schlüssel extrahieren möchten, gibt es nur eine Methode für Experten: Sie müssen ihn in der SQLite-Tabelle „keypairs” des Backups nachschlagen.</p>
<p>Delta Chat generates secure OpenPGP keys according to the Autocrypt specification 1.1.
We do not recommend or offer users to perform manual key management.
We want to ensure that security audits can focus on a few proven cryptographic algorithms
instead of the full breadth of possible algorithms allowed with OpenPGP.
If you want to extract your OpenPGP key, there only is an expert method:
you need to look it up in the “keypairs” SQLite table of a profile backup tar-file.</p>
<h3 id="security-audits">
@@ -1534,24 +1535,31 @@ Wir nutzen vielmehr öffentliche Finanzierungsquellen, die bisher aus der EU und
<ul>
<li>
<p>2023 und 2024 wurden wir in das Next-Generation-Internet-Programm (NGI)
für unsere Arbeit an <a href="https://nlnet.nl/project/WebXDC-Push/">Webxdc-PUSH</a> aufgenommen,
zusammen mit Kooperationspartnern, die an
<a href="https://nlnet.nl/project/Webxdc-Evolve/">Webxdc-Evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">Webxdc-XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> und
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
Alle diese Projekte sind teilweise abgeschlossen oder sollen Anfang 2025 abgeschlossen werden.</p>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
</li>
<li>
<p>Im Jahr 2021 erhielten wir weitere EU-Mittel für zwei “Next-Generation-Internet”-Anträge, nämlich für <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - E-Mail-Provider-Portabilitätsverzeichnis</a> (~97K EUR) und <a href="https://nlnet.nl/project/EmailPorting/">AEAP - E-Mail-Adressportierung</a> (~90K EUR). Ziel sind bessere Unterstützung von Mehrfachkonten, verbesserten QR-Code-Kontakt- und -Gruppen-Setups sowie Netzwerkverbesserungen auf allen Plattformen.</p>
</li>
<li>
<p>Die <a href="https://nlnet.nl/">NLnet-Stiftung</a> bewilligte 2019/2020 46K EUR für die Fertigstellung von Rust-/Python-Bindungs und die Einrichtung eines Chat-Bot-Ökosystems.</p>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
</li>
<li>
<p>Der <a href="https://opentechfund.org">Open Technology Fund</a> hat Delta Chat erstmals 2018/2019 bezuschusst; mit dieser Förderung (~$200K) wurden hauptsächlich die Android-App verbessert sowie das Release der Desktop-App in einer Betaversion ermöglicht. Basierend auf Nutzererfahrungen im Menschenrechtskontext wurden zudem verschiedene Funktionen entwickelt, siehe unseren Bericht <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
Die zweite Förderung 2019/2020 (~$300K) half uns bei der Erstellung der iOS-Version, unsere Kernbibliothek in die Programmiersprache “Rust” zu konvertieren und neue Funktionen für alle Plattformen bereitzustellen.</p>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>Das EU-Projekt <a href="https://nextleap.eu">NEXTLEAP</a> finanzierte 2017 und 2018 die Entwicklung und Implementierung von “Verifizierten Gruppen” und “Setup Kontakt” und half auch bei der Integration der Ende-zu-Ende-Verschlüsselung durch <a href="https://autocrypt.org">Autocrypt</a>.</p>
+16 -16
View File
@@ -1188,10 +1188,6 @@ to exchange encryption setup information through QR-code scanning or “invite l
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
@@ -1376,10 +1372,12 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1455,7 +1453,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1466,9 +1464,12 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<h3 id="pqc">
@@ -1478,13 +1479,12 @@ This approach is specified in the <a href="https://datatracker.ietf.org/doc/draf
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<h3 id="how-can-i-manually-check-encryption-information">
+16 -16
View File
@@ -1184,10 +1184,6 @@ to exchange encryption setup information through QR-code scanning or “invite l
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
@@ -1369,10 +1365,12 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1448,7 +1446,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1459,9 +1457,12 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<h3 id="pqc">
@@ -1471,13 +1472,12 @@ This approach is specified in the <a href="https://datatracker.ietf.org/doc/draf
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<h3 id="how-can-i-manually-check-encryption-information">
+17 -13
View File
@@ -1175,9 +1175,6 @@ to exchange encryption setup information through QR-code scanning or “invite l
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, qui est prévu pour 2026, amènera un chiffrement avec résistance post-quantique ainsi que la confidentialité persistante (“forward secrecy”).</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
chat</a>
@@ -1361,10 +1358,12 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1419,7 +1418,7 @@ For example, tapping a link exposes IP Addresses to unknown parties and is the b
</h3>
<p>Non, pas encore.</p>
<p>No, not yet.</p>
<p>The Signal messenger introduced <a href="https://signal.org/blog/sealed-sender/">“Sealed Sender” in 2018</a>
to keep their server infrastructure ignorant of who is sending a message to a set of recipients.
@@ -1440,7 +1439,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>Pas encore mais cela arrive avec <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1451,8 +1450,12 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, prévu pour 2026, permettra la suppression fiable (forward secrecy) grâce à une rotation automatique des clefs.
Cette approche est détaillée dans le brouillon du <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">certificat Autocrypt v2 OpenPGP</a>.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<h3 id="pqc">
@@ -1462,11 +1465,12 @@ Cette approche est détaillée dans le brouillon du <a href="https://datatracke
</h3>
<p>Pas encore mais cela arrive avec <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, prévu pour 2026, amènera un chiffrement avec résistance post-quantique pour protéger contre les attaques effectuées par des ordinateurs quantiques.
Delta Chat utilise la librairie Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a> qui supporte les dernières <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
Limplémentation est détaillée dans le brouillon du <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">certificat Autocrypt v2 OpenPGP</a>.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<h3 id="how-can-i-manually-check-encryption-information">
+16 -16
View File
@@ -1188,10 +1188,6 @@ to exchange encryption setup information through QR-code scanning or “invite l
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
@@ -1376,10 +1372,12 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1455,7 +1453,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1466,9 +1464,12 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<h3 id="pqc">
@@ -1478,13 +1479,12 @@ This approach is specified in the <a href="https://datatracker.ietf.org/doc/draf
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<h3 id="how-can-i-manually-check-encryption-information">
+29 -31
View File
@@ -1173,10 +1173,6 @@ per scambiare informazioni sulla configurazione della crittografia tramite la sc
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> viene utilizzato per stabilire
automaticamente la crittografia end-to-end tra i contatti e tutti i membri di una chat di gruppo.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, la cui piena implementazione è prevista per il 2026,
introdurrà una crittografia post-quantistica resistente e una segretezza avanzata.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Condivisione di un contatto con una
@@ -1440,7 +1436,7 @@ ma unimplementazione non è stata ancora concordata come priorità.</p>
</h3>
<p>Non ancora, ma arriverà con <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, non ancora.</p>
<p>Delta Chat al momento non supporta la tecnologia Perfect Forward Secrecy (PFS).
Ciò significa che se la tua chiave di decrittazione privata viene divulgata
@@ -1451,9 +1447,12 @@ In caso contrario, chi ottiene le tue chiavi di decrittazione
in genere è in grado di ottenere anche tutti i tuoi messaggi non eliminati
e non ha nemmeno bisogno di decifrare i messaggi raccolti in precedenza.</p>
<p><a href="https://autocrypt2.org">autocrypt v2</a>, la cui piena implementazione è prevista per il 2026,
garantirà uneliminazione affidabile (segretezza in avanti) tramite rotazione automatica delle chiavi.
Questo approccio è specificato nella bozza dei <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">certificati OpenPGP di Autocrypt v2</a>.</p>
<p>Abbiamo progettato un approccio Forward Secrecy che ha superato
lesame iniziale di alcuni crittografi ed esperti di implementazione
ma è in attesa di una stesura più formale
per accertarne laffidabilità nella messaggistica federata e nellutilizzo su più dispositivi,
prima di poter essere implementato in <a href="https://github.com/chatmail/core">chatmail core</a>,
che lo renderebbe disponibile in tutti i <a href="https://chatmail.at/clients">clients di chatmail</a>.</p>
<h3 id="pqc">
@@ -1463,13 +1462,12 @@ Questo approccio è specificato nella bozza dei <a href="https://datatracker.iet
</h3>
<p>Non ancora, ma arriverà con <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, non ancora.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, la cui piena implementazione è prevista per il 2026,
offrirà una crittografia post-quantistica resistente per proteggere dagli attacchi ai computer quantistici.
Delta Chat utilizza la libreria Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a>
<p>Delta Chat utilizza la libreria Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a>
che supporta lultima <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">bozza IETF Post-Quantum-Cryptography OpenPGP</a>.
Limplementazione è specificata nella bozza dei <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Certificati OpenPGP Autocrypt v2</a>.</p>
Il nostro obiettivo è aggiungere il supporto PQC nel <a href="https://github.com/chatmail/core">core di chatmail</a> dopo che la bozza sarà stata finalizzata dallIETF
in collaborazione con altri implementatori di OpenPGP.</p>
<h3 id="come-posso-controllare-manualmente-le-informazioni-di-crittografia">
@@ -1649,31 +1647,31 @@ ordinate cronologicamente:</p>
<ul>
<li>
<p>Nel 2023 e nel 2024 siamo stati accettati nel programma Next Generation Internet (NGI)
per il nostro lavoro in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
insieme ai partner di collaborazione che lavorano su
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> e
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
Tutti questi progetti sono parzialmente completati o saranno completati allinizio del 2025.</p>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
</li>
<li>
<p>Nel 2021 abbiamo ricevuto ulteriori finanziamenti dallUE per due proposte di Next-Generation-Internet, ovvero per <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - directory di portabilità dei provider di posta elettronica</a> (~97.000 EUR) e <a href="https://nlnet.nl/project/EmailPorting/">AEAP - portabilità degli indirizzi email</a> (~90.000 EUR), che hanno portato a un migliore supporto multi-profilo, a un miglioramento delle impostazioni di contatto e di gruppo tramite codice QR e a numerosi miglioramenti di rete su tutte le piattaforme.</p>
</li>
<li>
<p>La <a href="https://nlnet.nl/">fondazione NLnet</a> ha concesso nel 2019/2020 46.000 EUR per
completando i collegamenti Rust/Python e avviando un ecosistema Chat-bot.</p>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
</li>
<li>
<p>L<a href="https://opentechfund.org">Open Technology Fund</a> ci ha dato una
prima sovvenzione 2018/2019 (~$200K) durante la quale abbiamo notevolmente migliorato lapp Android
e ha rilasciato una prima versione beta dellapp desktop, e che inoltre
ancorato i nostri sviluppi delle funzionalità nella ricerca sulla UX nei contesti dei diritti umani,
vedete il nostro <a href="https://delta.chat/en/2019-07-19-uxreport">Rapporto Needfinding e UX</a> conclusivo.
La seconda sovvenzione 2019/2020 (~$300K) ci ha aiutato a farlo
rilasciare nelle versioni Delta/iOS, per convertire la nostra libreria principale in Rust, e
per fornire nuove funzionalità per tutte le piattaforme.</p>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>Il progetto UE <a href="https://nextleap.eu">NEXTLEAP</a> ha finanziato la ricerca
+16 -16
View File
@@ -1182,10 +1182,6 @@ to exchange encryption setup information through QR-code scanning or “invite l
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
@@ -1370,10 +1366,12 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1449,7 +1447,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1460,9 +1458,12 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<h3 id="pqc">
@@ -1472,13 +1473,12 @@ This approach is specified in the <a href="https://datatracker.ietf.org/doc/draf
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<h3 id="how-can-i-manually-check-encryption-information">
+28 -21
View File
@@ -1102,10 +1102,6 @@ weekly statistics will be automatically sent to a bot.</p>
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> służy do automatycznego ustanawiania szyfrowania typu end-to-end między kontaktami a wszystkimi członkami czatu grupowego.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Udostępnienie kontaktu na czacie</a> umożliwia odbiorcom korzystanie z szyfrowania typu end-to-end z tym kontaktem.</p>
</li>
@@ -1240,10 +1236,12 @@ even if the message was not end-to-end encrypted.</p>
<p>Servers can therefore only see:</p>
<ul>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>Wszystkie pozostałe metadane dotyczące wiadomości, kontaktów i grup znajdują się w zaszyfrowanej metodą end-to-end części wiadomości.</p>
<h3 id="device-seizure">
@@ -1316,13 +1314,11 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Nie, jeszcze nie.</p>
<p>Delta Chat obecnie nie obsługuje mechanizmu Perfect Forward Secrecy (PFS). Oznacza to, że jeśli twój prywatny klucz deszyfrujący zostanie ujawniony, a ktoś zdobędzie twoje wcześniejsze wiadomości w trakcie transmisji, będzie mógł je odszyfrować i odczytać za pomocą ujawnionego klucza deszyfrującego. Należy pamiętać, że mechanizm Forward Secrecy zwiększa bezpieczeństwo tylko w przypadku usuwania wiadomości. W przeciwnym razie osoba, która uzyska twoje klucze deszyfrujące, zazwyczaj będzie mogła uzyskać dostęp do wszystkich nieusuniętych wiadomości i nie będzie musiała odszyfrowywać żadnych wcześniej zebranych wiadomości.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>Opracowaliśmy metodę Forward Secrecy, która przeszła wstępną analizę niektórych kryptografów i ekspertów ds. wdrożeń, ale oczekuje na bardziej formalne opracowanie, które potwierdzi jej niezawodne działanie w federacyjnym przesyłaniu wiadomości i w przypadku korzystania z wielu urządzeń, zanim zostanie zaimplementowana w <a href="https://github.com/chatmail/core">rdzeniu chatmail</a>, co uczyniłoby ją dostępną we wszystkich <a href="https://chatmail.at/clients">klientach chatmail</a>.</p>
<h3 id="pqc">
@@ -1332,13 +1328,9 @@ This approach is specified in the <a href="https://datatracker.ietf.org/doc/draf
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Nie, jeszcze nie.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>Delta Chat korzysta z biblioteki Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a>, która obsługuje najnowszy <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">projekt OpenPGP IETF Post-Quantum-Cryptography</a>. Planujemy dodać obsługę PQC do <a href="https://github.com/chatmail/core">rdzenia chatmail</a> po sfinalizowaniu projektu w IETF we współpracy z innymi implementatorami OpenPGP.</p>
<h3 id="jak-mogę-ręcznie-sprawdzić-informacje-o-szyfrowaniu">
@@ -1471,17 +1463,32 @@ Raczej korzystamy z publicznych źródeł finansowania, jak dotąd pochodzących
<ul>
<li>
<p>W latach 2023 i 2024 zostaliśmy przyjęci do programu Next Generation Internet (NGI) za naszą pracę w <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>, wraz z partnerami współpracującymi pracującymi nad <a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>, <a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>, <a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> i <a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>. Wszystkie te projekty są częściowo ukończone lub zostaną ukończone na początku 2025 r.</p>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
</li>
<li>
<p>W 2021 r. otrzymaliśmy kolejne dofinansowanie z UE na dwie propozycje dotyczące Internetu nowej generacji, a mianowicie na <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD katalog przenośności dostawcy poczty e-mail</a> ( ~97 tys. EUR) i <a href="https://nlnet.nl/project/EmailPorting/">AEAP przenoszenie adresu e-mail</a> (~90 tys. EUR), co zaowocowało lepszą obsługą wielu kont, ulepszonymi kontaktami i ustawieniami grup za pomocą kodów QR oraz wieloma ulepszeniami sieciowymi na wszystkich platformach.</p>
<p>In 2021 we received further EU funding for two Next-Generation-Internet
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
</li>
<li>
<p><a href="https://nlnet.nl/">Fundacja NLnet</a> przekazała w latach 2019/2020 kwotę 46 tys. EUR na wykonanie wiązań Rust/Python i uruchomienie ekosystemu Chat-bot.</p>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
</li>
<li>
<p><a href="https://opentechfund.org">Open Technology Fund</a> przyznał nam pierwszy grant w 2018/2019 (~200 000 $), dzięki któremu znacznie ulepszyliśmy aplikację na Androida i wydaliśmy pierwszą wersję beta aplikacji na komputery stacjonarne, a także ugruntował rozwój naszych funkcji w badaniach UX w kontekście praw człowieka, zobacz nasz końcowy raport <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX</a>.
Druga dotacja w 2019/2020 (~300 000 4) pomogła nam wydać wersje Delta/iOS, przekonwertować naszą podstawową bibliotekę na Rust i zapewnić nowe funkcje dla wszystkich platform.</p>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>Unijny projekt <a href="https://nextleap.eu">NEXTLEAP</a> sfinansował badania i wdrożenie zweryfikowanych grup i ustawień protokołów kontaktowych w latach 2017 i 2018, a także pomógł zintegrować szyfrowanie end-to-end poprzez <a href="https://autocrypt.org">Autocrypt</a>.</p>
+16 -16
View File
@@ -1183,10 +1183,6 @@ to exchange encryption setup information through QR-code scanning or “invite l
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
@@ -1371,10 +1367,12 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1450,7 +1448,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1461,9 +1459,12 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<h3 id="pqc">
@@ -1473,13 +1474,12 @@ This approach is specified in the <a href="https://datatracker.ietf.org/doc/draf
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<h3 id="how-can-i-manually-check-encryption-information">
+31 -33
View File
@@ -1181,10 +1181,6 @@ Chatmail использует INBOX по умолчанию для ретран
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> используется для автоматической
настройки сквозного шифрования между контактами и всеми членами группового чата.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, полное внедрение которого запланировано на 2026 год,
обеспечит поддержку постквантового шифрования и прямой секретности.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Обмен контактом в
@@ -1431,7 +1427,7 @@ Delta Chat вместо этого использует реализацию Ope
</h3>
<p>Нет, пока нет.</p>
<p>Нет, еще нет.</p>
<p>Мессенджер Signal внедрил функцию <a href="https://signal.org/blog/sealed-sender/">“Sealed Sender” (Засекреченный отправитель) в 2018 году</a>,
чтобы их серверная инфраструктура не имела информации о том, кто отправляет сообщение группе получателей.
@@ -1452,7 +1448,7 @@ Delta Chat вместо этого использует реализацию Ope
</h3>
<p>Пока нет, но это будет реализовано в <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Нет, еще нет.</p>
<p>На данный момент, Delta Chat не поддерживает Perfect Forward Secrecy (PFS) (Совершенную прямую секретность).
Это означает, что если ваш приватный ключ дешифрования будет скомпрометирован,
@@ -1463,9 +1459,12 @@ Delta Chat вместо этого использует реализацию Ope
также может получить все ваши не удалённые сообщения
и ему даже не нужно расшифровывать какие-либо ранее собранные сообщения.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, полное внедрение которого запланировано на 2026 год,
обеспечит надёжное удаление (прямую секретность) за счёт автоматической ротации ключей.
Этот подход описан в черновике спецификации <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a>.</p>
<p>Мы разработали подход к Forward Secrecy (Прямой секретности), который прошёл
первичную проверку некоторыми криптографами и экспертами по реализации
но требует более формального описания
чтобы убедиться, что он надёжно работает в федеративном обмене сообщениями и при использовании нескольких устройств,
прежде чем он может быть внедрён в <a href="https://github.com/chatmail/core">ядро chatmail</a>,
что сделает его доступным во всех <a href="https://chatmail.at/clients">клиентах clients</a>.</p>
<h3 id="pqc">
@@ -1475,13 +1474,12 @@ Delta Chat вместо этого использует реализацию Ope
</h3>
<p>Пока нет, но эта возможность появится в <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Нет, еще нет.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, полное внедрение которого запланировано на 2026 год,
обеспечит поддержку постквантового шифрования для защиты от атак с использованием квантовых компьютеров.
Delta Chat использует Rust-библиотеку OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a>
которая поддерживает актуальный черновик IETF <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP</a>.
Особенности реализации описаны в черновике спецификации <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a>.</p>
<p>Delta Chat использует библиотеку OpenPGP на Rust <a href="https://github.com/rpgp/rpgp">rPGP</a>,
которая поддерживает последний <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">черновик IETF Post-Quantum-Cryptography OpenPGP</a>.
Мы планируем добавить поддержку PQC в <a href="https://github.com/chatmail/core">ядро chatmail</a> после того, как черновик будет окончательно утвержден в IETF
в сотрудничестве с другими разработчиками OpenPGP.</p>
<h3 id="как-можно-вручную-проверить-информацию-о-шифровании">
@@ -1660,32 +1658,32 @@ Google Play Store, F-Droid, Huawei App Gallery, iOS и macOS App Store, Microsof
<ul>
<li>
<p>В 2023 и 2024 годах мы были приняты в программу Next Generation Internet (NGI)
за нашу работу над <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
в сотрудничестве с партнерами, работающими над
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> и
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
Все эти проекты частично завершены или будут завершены в начале 2025 года.</p>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
</li>
<li>
<p>В 2021 г. мы получили дополнительное финансирование из ЕС для двух Next-Generation-Internet
целей, а именно для <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - e-mail provider portability directory</a> (~97 тыс. евро) и <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90 тыс. евро). Это привело к улучшению поддержки нескольких профилей, улучшению настройки контактов и групп с помощью QR-кода и многим улучшениям в сетевом взаимодействии на всех платформах.</p>
</li>
<li>
<p>Фонд <a href="https://nlnet.nl/">NLnet Foundation</a> выделил в 2019/2020 году 46 тысяч евро на
доработку связки Rust/Python и создание экосистемы чат-ботов..</p>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
</li>
<li>
<p>Фонд <a href="https://opentechfund.org">Open Technology Fund</a> предоставил нам
первый грант в 2018/2019 году (~$200 тыс.), благодаря которому мы существенно улучшили приложение для Android
и выпустили первую бета-версию приложения для настольных систем, а также провели
исследования в области UX в контексте прав человека,
см. наш заключительный отчет <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
Второй грант, полученный в 2019/2020 году (~$300 тыс.), помог нам
выпустить версии Delta/iOS, перевести наш основной код на Rust и
предоставить новые функции для всех платформ.</p>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>Проект ЕС <a href="https://nextleap.eu">NEXTLEAP</a> финансировал исследование
+16 -16
View File
@@ -1188,10 +1188,6 @@ to exchange encryption setup information through QR-code scanning or “invite l
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
@@ -1376,10 +1372,12 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1455,7 +1453,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1466,9 +1464,12 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<h3 id="pqc">
@@ -1478,13 +1479,12 @@ This approach is specified in the <a href="https://datatracker.ietf.org/doc/draf
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<h3 id="how-can-i-manually-check-encryption-information">
+32 -35
View File
@@ -1190,10 +1190,6 @@ to exchange encryption setup information through QR-code scanning or “invite l
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
@@ -1378,10 +1374,12 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1457,7 +1455,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1468,9 +1466,12 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<h3 id="pqc">
@@ -1480,13 +1481,12 @@ This approach is specified in the <a href="https://datatracker.ietf.org/doc/draf
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<h3 id="how-can-i-manually-check-encryption-information">
@@ -1675,31 +1675,28 @@ along with collaboration partners working on
All of these projects are partially completed or to be completed in early 2025.</p>
</li>
<li>
<p> 2021-n morëm financime të mëtejshme nga BE për dy propozime që shtrihen në
“Internetin e Brezit Tjetër”, konkretisht për <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - e-mail provider portability directory</a> (~97K euro) dhe <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K euro) që sollën mbulim më të mirë për përdorues me shumë
llogari, përmirësim të gjërave për kontakte me kod QR dhe grupe, si dhe mjaft
përmirësime në punën në rrjet për krejt platformat.</p>
<p>In 2021 we received further EU funding for two Next-Generation-Internet
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
</li>
<li>
<p><a href="https://nlnet.nl/">Fondacioni NLnet</a> dhuroi 46K euro gjatë 2019/2020 r
plotësimin e <em>Rust/Python bindings</em> dhe për ti dhënë udhë një ekosistemi
Chat-bot.</p>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
</li>
<li>
<p><a href="https://opentechfund.org">Open Technology Fund</a> na dha grantin e parë
për 2018/2019 (~200 mijë dollarë) me të cilin përmirësuam ndjeshëm aplikacionin
për Android dhe hodhëm në qarkullim një version të parë beta aplikacioni për Desktop,
si dhe i afroi më tepër zhvillimet tona për veçori me kërkime UX në kontekste të drejtash të njeriut,
shihni <a href="https://delta.chat/en/2019-07-19-uxreport">raportin tonë përfundimtar “Needfinding and UX</a>.
Granti i dytë për 2019/2020 (~$300K) na ndihmoi të hedhim në qarkullim
versione Delta/iOS, për të shndërruar bibliotekën tonë bazë në Rust, si dhe
për të sjellë veçori të reja për krejt platformat.</p>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>Projekti <a href="https://nextleap.eu">NEXTLEAP</a> i BE-së financoi kërkimin
për dhe sendërtimin e grupeve të verifikuara dhe protokolleve të
ujdisjes së kontakteve më 2017-n dhe 2018-n dhe ndihmoi gjithashtu
të integrohet Fshehtëzim Skaj-më-Skaj përmes <a href="https://autocrypt.org">Autocrypt</a>.</p>
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
and implementation of verified groups and setup contact protocols
in 2017 and 2018 and also helped to integrate end-to-end Encryption
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>Ndonjëherë marrim dhurime unike nga individë privatë.
+33 -28
View File
@@ -140,10 +140,17 @@
<ul>
<li>
<p>Якщо ви перебуваєте <strong>віч-на-віч</strong> зі своїм другом або родиною, торкніться піктограми <strong>QR-код</strong> на головному екрані <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> на головному екрані. Попросіть вашого партнера по чату <strong>сканувати</strong> QR-зображення за допомогою їхнього застосунку Delta Chat.</p>
<p>If you are <strong>face to face</strong> with your friend or family,
tap the <strong>QR Code</strong> icon <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" />
on the main screen.<br />
Ask your chat partner to <strong>scan</strong> the QR image
with their Delta Chat app.</p>
</li>
<li>
<p>Для <strong>віддаленого</strong> налаштування контакту, на тому ж самому екрані, натисніть “Копіювати” або “Поділитися” і відправте <strong>запрошувальне посилання</strong> через інший приватний чат.</p>
<p>For a <strong>remote</strong> contact setup,
from the same screen,
click “Copy” or “Share” and send the <strong>invite link</strong>
through another private chat.</p>
</li>
</ul>
@@ -1088,10 +1095,6 @@ to send anonymous usage statistics.</p>
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> використовується для автоматичного встановлення наскрізного шифрування між контактами і всіма учасниками групового чату.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Поширення контакту в чаті</a> дозволяє отримувачам використовувати наскрізне шифрування з контактом.</p>
</li>
@@ -1295,13 +1298,11 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Ні, поки ще ні.</p>
<p>Delta Chat наразі не підтримує ідеальну пряму секретність (Perfect Forward Secrecy, PFS). Це означає, що якщо ваш приватний ключ для розшифрування буде скомпрометовано, а хтось заздалегідь зібрав ваші повідомлення під час передачі, він зможе розшифрувати та прочитати їх, використовуючи зламаний ключ. Зверніть увагу, що пряма секретність підвищує рівень безпеки лише в тому разі, якщо ви видаляєте повідомлення. Інакше, якщо хтось отримує доступ до ваших ключів розшифрування, він зазвичай також має доступ до всіх ваших невидалених повідомлень і навіть не потребує розшифровувати заздалегідь перехоплені дані.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>Ми розробили підхід Forward Secrecy, який витримав початкову експертизу від деяких криптографів та експертів з реалізації але чекає на більш офіційний звіт щоб переконатися, що він надійно працює в об’єднаних системах обміну повідомленнями та при використанні декількох пристроїв, перш ніж його можна буде реалізувати в <a href="https://github.com/chatmail/core">ядрі чату</a>, що зробить його доступним у всіх <a href="https://chatmail.at/clients">клієнтах чату</a>.</p>
<h3 id="pqc">
@@ -1311,13 +1312,9 @@ This approach is specified in the <a href="https://datatracker.ietf.org/doc/draf
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Ні, поки ще ні.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>Delta Chat використовує бібліотеку Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a> яка підтримує останню версію <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>. Ми плануємо додати підтримку PQC у <a href="https://github.com/chatmail/core">chatmail core</a> після того, як проект буде завершено у IETF у співпраці з іншими розробниками OpenPGP.</p>
<h3 id="як-я-можу-вручну-перевірити-інформацію-про-шифрування">
@@ -1448,24 +1445,32 @@ Google Play Store, F-Droid, Huawei App Gallery, iOS and macOS App Store, Microso
<ul>
<li>
<p>У 2023 та 2024 роках нас прийняли до програми Next Generation Internet (NGI) за нашу роботу над <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>, а також у співпраці з партнерами, які працюють над <a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>, <a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>, <a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> та <a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>. Усі ці проєкти частково завершені або будуть завершені на початку 2025 року.</p>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
</li>
<li>
<p>У 2021 році ми отримали подальше фінансування від ЄС на дві пропозиції щодо Інтернету наступного покоління а саме на <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - каталог перенесення провайдерів електронної пошти</a> (~97 тис. євро) та <a href="https://nlnet.nl/project/EmailPorting/">AEAP - перенесення адрес електронної пошти</a> (~90 тис. євро), що дозволило нам покращити багатопрофільну підтримку, вдосконалити налаштування контактів та груп за допомогою QR-коду та багато інших мережевих покращень на всіх платформах.</p>
<p>In 2021 we received further EU funding for two Next-Generation-Internet
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
</li>
<li>
<p>Фонд <a href="https://nlnet.nl/">NLnet</a> виділив у 2019/2020 роках 46 тисяч євро на
завершення прив’язок Rust/Python та запуск екосистеми чат-ботів.</p>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
</li>
<li>
<p><a href="https://opentechfund.org">Open Technology Fund</a> надав нам два гранти.
Перший грант 2018/2019 року (~$200K), допоміг значно покращили додаток для Android
і випустили першу бета-версію додатка для ПК, і який до того ж
закріпив наші розробки функцій у дослідженнях UX у контексті прав людини,
дивіться наш підсумковий звіт <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
Другий грант 2019/2020 року (~$300K) допоміг нам
випустити Delta/iOS версію, конвертувати нашу основному бібліотеку на Rust,
і додати нові функції для всіх платформ.</p>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>Проект ЄС <a href="https://nextleap.eu">NEXTLEAP</a> фінансував дослідження та впровадження верифікованих груп і протоколів встановлення контактів у 2017 та 2018 роках, а також допоміг інтегрувати наскрізне шифрування через <a href="https://autocrypt.org">Autocrypt</a>.</p>
+36 -26
View File
@@ -1161,10 +1161,6 @@ weekly statistics will be automatically sent to a bot.</p>
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
用于在联系人和群聊的所有成员之间自动建立端到端加密。</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">将联系人分享到聊天中
@@ -1346,10 +1342,12 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1425,7 +1423,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1436,9 +1434,12 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<h3 id="pqc">
@@ -1448,13 +1449,12 @@ This approach is specified in the <a href="https://datatracker.ietf.org/doc/draf
</h3>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>No, not yet.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<h3 id="how-can-i-manually-check-encryption-information">
@@ -1624,22 +1624,32 @@ Google Play Store, F-Droid, Huawei App Gallery, iOS and macOS App Store, Microso
<ul>
<li>
<p> 2023 年和 2024 年,我们的 <a href="https://nlnet.nl/project/WebXDC-Push/">WebXDC PUSH</a> 工作已在下一代互联网 (NGI) 中获得认可,
并与致力于
<a href="https://nlnet.nl/project/Webxdc-Evolve/">WebXDC evolve</a>
<a href="https://nlnet.nl/project/WebXDC-XMPP/">WebXDC XMPP</a>
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a>
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a> 的合作伙伴合作。
所有这些项目都已部分完成或将在 2025 年初完成。</p>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
</li>
<li>
<p> 2021 年,我们从两项下一代互联网提案收到了欧盟的进一步资助,即 <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - 电子邮件提供商可移植性目录</a>(约 9.7 万欧元)和 <a href="https://nlnet.nl/project/EmailPorting/">AEAP - 电子邮件地址移植</a>(约 9 万欧元)。这带来了更好的多账户支持,改进的二维码联系人和群组设置,和所有平台上的多处网络改进。</p>
<p>In 2021 we received further EU funding for two Next-Generation-Internet
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
</li>
<li>
<p><a href="https://nlnet.nl/">NLnet 基金会</a> 2019/2020 年拨款 4.6 万欧元,用于完成 Rust/Python 绑定并建立聊天机器人生态系统。</p>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
</li>
<li>
<p><a href="https://opentechfund.org">开放技术基金</a> 2018/2019 年提供的第一笔赠款(约 20 万美元)期间,我们显著改善了安卓应用,发布了第一个桌面测试版,并根据人权方面的用户体验研究进行了功能开发,请参阅我们的结论<a href="https://delta.chat/en/2019-07-19-uxreport">《需求发现与用户体验报告》</a>。2019/2020 年的第二笔赠款(约 30 万美元)对发布 Delta/iOS 版本,将核心库转换到 Rust ,以及为所有平台开发新功能提供了帮助。</p>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p><a href="https://nextleap.eu">NEXTLEAP</a>欧盟项目资助了以下研究和实施工作:在 2017 年和 2018 年实施的验证组和设置联系协议和通过 <a href="https://autocrypt.org">Autocrypt</a>整合了端到端加密。</p>
+4 -31
View File
@@ -682,8 +682,7 @@ public class Rpc {
* Set group name.
* <p>
* If the group is already _promoted_ (any message was sent to the group),
* or if this is a brodacast channel,
* all members are informed by a special status message that is sent automatically by this function.
* all group members are informed by a special status message that is sent automatically by this function.
* <p>
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*/
@@ -691,37 +690,11 @@ public class Rpc {
transport.call("set_chat_name", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(newName));
}
/**
* Set group or broadcast channel description.
* <p>
* If the group is already _promoted_ (any message was sent to the group),
* or if this is a brodacast channel,
* all members are informed by a special status message that is sent automatically by this function.
* <p>
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
* <p>
* See also [`Self::get_chat_description`] / `getChatDescription()`.
*/
public void setChatDescription(Integer accountId, Integer chatId, String description) throws RpcException {
transport.call("set_chat_description", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(description));
}
/**
* Load the chat description from the database.
* <p>
* UIs show this in the profile page of the chat,
* it is settable by [`Self::set_chat_description`] / `setChatDescription()`.
*/
public String getChatDescription(Integer accountId, Integer chatId) throws RpcException {
return transport.callForResult(new TypeReference<String>(){}, "get_chat_description", mapper.valueToTree(accountId), mapper.valueToTree(chatId));
}
/**
* Set group profile image.
* <p>
* If the group is already _promoted_ (any message was sent to the group),
* or if this is a brodacast channel,
* all members are informed by a special status message that is sent automatically by this function.
* all group members are informed by a special status message that is sent automatically by this function.
* <p>
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
* <p>
@@ -1342,8 +1315,8 @@ public class Rpc {
}
/** Starts an outgoing call. */
public Integer placeOutgoingCall(Integer accountId, Integer chatId, String placeCallInfo, Boolean hasVideo) throws RpcException {
return transport.callForResult(new TypeReference<Integer>(){}, "place_outgoing_call", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(placeCallInfo), mapper.valueToTree(hasVideo));
public Integer placeOutgoingCall(Integer accountId, Integer chatId, String placeCallInfo) throws RpcException {
return transport.callForResult(new TypeReference<Integer>(){}, "place_outgoing_call", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(placeCallInfo));
}
/** Accepts an incoming call. */
@@ -2,7 +2,7 @@
package chat.delta.rpc.types;
public class CallInfo {
/** True if the call is started as a video call. */
/** True if SDP offer has a video. */
public Boolean hasVideo;
/**
* SDP offer.
@@ -4,7 +4,6 @@ package chat.delta.rpc.types;
public enum SystemMessageType {
Unknown,
GroupNameChanged,
GroupDescriptionChanged,
GroupImageChanged,
MemberAddedToGroup,
MemberRemovedFromGroup,
@@ -2,8 +2,8 @@ package com.b44t.messenger;
public class DcAccounts {
public DcAccounts(String dir, DcEventChannel channel) {
accountsCPtr = createAccountsCPtr(dir, channel);
public DcAccounts(String dir) {
accountsCPtr = createAccountsCPtr(dir);
if (accountsCPtr == 0) throw new RuntimeException("createAccountsCPtr() returned null pointer");
}
@@ -46,7 +46,7 @@ public class DcAccounts {
// working with raw c-data
private long accountsCPtr; // CAVE: the name is referenced in the JNI
private native long createAccountsCPtr (String dir, DcEventChannel channel);
private native long createAccountsCPtr (String dir);
private native void unrefAccountsCPtr ();
private native long getEventEmitterCPtr ();
private native long getJsonrpcInstanceCPtr ();
@@ -1,27 +0,0 @@
package com.b44t.messenger;
public class DcEventChannel {
public DcEventChannel() {
eventChannelCPtr = createEventChannelCPtr();
}
@Override
protected void finalize() throws Throwable {
super.finalize();
if (eventChannelCPtr != 0) {
unrefEventChannelCPtr();
eventChannelCPtr = 0;
}
}
public DcEventEmitter getEventEmitter() {
return new DcEventEmitter(getEventEmitterCPtr());
}
// working with raw c-data
private long eventChannelCPtr; // CAVE: the name is referenced in the JNI
private native long createEventChannelCPtr ();
private native void unrefEventChannelCPtr ();
private native long getEventEmitterCPtr ();
}
@@ -1,24 +1,16 @@
package org.thoughtcrime.securesms;
import android.content.ComponentName;
import android.os.Bundle;
import android.util.Log;
import android.view.MenuItem;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.session.MediaController;
import androidx.media3.session.SessionCommand;
import androidx.media3.session.SessionToken;
import androidx.viewpager.widget.ViewPager;
import com.b44t.messenger.DcChat;
@@ -26,13 +18,11 @@ import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcMsg;
import com.google.android.material.tabs.TabLayout;
import com.google.common.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.service.AudioPlaybackService;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.ArrayList;
@@ -40,7 +30,6 @@ import java.util.ArrayList;
public class AllMediaActivity extends PassphraseRequiredActionBarActivity
implements DcEventCenter.DcEventDelegate
{
private static final String TAG = AllMediaActivity.class.getSimpleName();
public static final String CHAT_ID_EXTRA = "chat_id";
public static final String CONTACT_ID_EXTRA = "contact_id";
@@ -68,10 +57,6 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
private TabLayout tabLayout;
private ViewPager viewPager;
private @Nullable MediaController mediaController;
private ListenableFuture<MediaController> mediaControllerFuture;
private AudioPlaybackViewModel playbackViewModel;
@Override
protected void onPreCreate() {
dynamicTheme = new DynamicNoActionBarTheme();
@@ -106,19 +91,11 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
DcEventCenter eventCenter = DcHelper.getEventCenter(this);
eventCenter.addObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this);
eventCenter.addObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class);
initializeMediaController();
}
@Override
public void onDestroy() {
DcHelper.getEventCenter(this).removeObservers(this);
if (mediaController != null) {
MediaController.releaseFuture(mediaControllerFuture);
mediaController = null;
playbackViewModel.setMediaController(null);
}
super.onDestroy();
}
@@ -147,40 +124,6 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
this.tabLayout = ViewUtil.findById(this, R.id.tab_layout);
}
private void initializeMediaController() {
SessionToken sessionToken = new SessionToken(this,
new ComponentName(this, AudioPlaybackService.class));
mediaControllerFuture = new MediaController.Builder(this, sessionToken)
.buildAsync();
mediaControllerFuture.addListener(() -> {
try {
mediaController = mediaControllerFuture.get();
addActivityContext(
this.getIntent().getExtras(),
this.getClass().getName()
);
playbackViewModel.setMediaController(mediaController);
} catch (Exception e) {
Log.e(TAG, "Error connecting to audio playback service", e);
}
}, ContextCompat.getMainExecutor(this));
}
private void addActivityContext(Bundle extras, String activityClassName) {
if (mediaController == null) return;
Bundle commandArgs = new Bundle();
commandArgs.putString("activity_class", activityClassName);
if (extras != null) {
commandArgs.putAll(extras);
}
SessionCommand updateContextCommand =
new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY);
mediaController.sendCustomCommand(updateContextCommand, commandArgs);
}
private boolean isGlobalGallery() {
return contactId==0 && chatId==0;
}
@@ -11,10 +11,9 @@ import androidx.annotation.NonNull;
import com.b44t.messenger.DcMsg;
import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.WebxdcView;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.DocumentSlide;
@@ -32,8 +31,7 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
private final ItemClickListener itemClickListener;
private final Set<DcMsg> selected;
private BucketedThreadMedia media;
private AudioPlaybackViewModel playbackViewModel;
private BucketedThreadMedia media;
private static class ViewHolder extends StickyHeaderGridAdapter.ItemViewHolder {
private final DocumentView documentView;
@@ -73,10 +71,6 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
this.media = media;
}
public void setPlaybackViewModel(AudioPlaybackViewModel playbackViewModel) {
this.playbackViewModel = playbackViewModel;
}
@Override
public StickyHeaderGridAdapter.HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType) {
return new HeaderHolder(LayoutInflater.from(context).inflate(R.layout.contact_selection_list_divider, parent, false));
@@ -103,8 +97,7 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
viewHolder.webxdcView.setVisibility(View.GONE);
viewHolder.audioView.setVisibility(View.VISIBLE);
viewHolder.audioView.setPlaybackViewModel(playbackViewModel);
viewHolder.audioView.setAudio((AudioSlide)slide);
viewHolder.audioView.setAudio((AudioSlide)slide, dcMsg.getDuration());
viewHolder.audioView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg));
viewHolder.audioView.setOnLongClickListener(view -> { itemClickListener.onMediaLongClicked(dcMsg); return true; });
viewHolder.audioView.disablePlayer(!selected.isEmpty());
@@ -16,7 +16,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.lifecycle.ViewModelProvider;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.RecyclerView;
@@ -26,7 +25,6 @@ import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcMsg;
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader;
@@ -74,11 +72,9 @@ public class AllMediaDocumentsFragment
// add padding to avoid content hidden behind system bars
ViewUtil.applyWindowInsets(recyclerView, true, false, true, true);
AllMediaDocumentsAdapter adapter = new AllMediaDocumentsAdapter(getContext(),
new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()),
this);
this.recyclerView.setAdapter(adapter);
adapter.setPlaybackViewModel(new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class));
this.recyclerView.setAdapter(new AllMediaDocumentsAdapter(getContext(),
new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()),
this));
this.recyclerView.setLayoutManager(gridManager);
this.recyclerView.setHasFixedSize(true);
@@ -243,13 +239,12 @@ public class AllMediaDocumentsFragment
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) {
int itemId = menuItem.getItemId();
AudioPlaybackViewModel playbackViewModel = new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
if (itemId == R.id.details) {
handleDisplayDetails(getSelectedMessageRecord(getListAdapter().getSelectedMedia()));
mode.finish();
return true;
} else if (itemId == R.id.delete) {
handleDeleteMessages(chatId, getListAdapter().getSelectedMedia(), playbackViewModel::stopByIds, playbackViewModel::stopByIds);
handleDeleteMessages(chatId, getListAdapter().getSelectedMedia());
mode.finish();
return true;
} else if (itemId == R.id.share) {
@@ -23,7 +23,6 @@ import androidx.work.WorkManager;
import com.b44t.messenger.DcAccounts;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcEventChannel;
import com.b44t.messenger.DcEventEmitter;
import com.b44t.messenger.FFITransport;
@@ -165,8 +164,7 @@ public class ApplicationContext extends MultiDexApplication {
StringWriter stringWriter = new StringWriter();
throwable.printStackTrace(new PrintWriter(stringWriter, true));
String errorMsg = "Android " + Build.VERSION.RELEASE +":\n" + stringWriter.getBuffer().toString();
errorMsg += "\n" + LogViewFragment.grabLogcat();
String subject = "ArcaneChat " + BuildConfig.VERSION_NAME + "-" + BuildConfig.FLAVOR + " Crash Report";
String subject = "ArcaneChat " + BuildConfig.VERSION_NAME + " Crash Report";
Intent intent = new Intent(android.content.Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
@@ -197,29 +195,7 @@ public class ApplicationContext extends MultiDexApplication {
Util.runOnBackground(() -> {
synchronized (initLock) {
try {
DcEventChannel eventChannel = new DcEventChannel();
DcEventEmitter emitter = eventChannel.getEventEmitter();
eventCenter = new DcEventCenter(this);
new Thread(() -> {
Log.i(TAG, "Starting event loop");
while (true) {
DcEvent event = emitter.getNextEvent();
if (event == null) {
break;
}
if (isInitialized) {
eventCenter.handleEvent(event);
} else {
// not fully initialized, only handle logging events,
// ex. account migrations during DcAccounts initialization
eventCenter.handleLogging(event);
}
}
Log.i("DeltaChat", "shutting down event handler");
}, "eventThread").start();
dcAccounts = new DcAccounts(new File(getFilesDir(), "accounts").getAbsolutePath(), eventChannel);
dcAccounts = new DcAccounts(new File(getFilesDir(), "accounts").getAbsolutePath());
Log.i(TAG, "DcAccounts created");
rpc = new Rpc(new FFITransport(dcAccounts.getJsonrpcInstance()));
Log.i(TAG, "Rpc created");
@@ -259,12 +235,28 @@ public class ApplicationContext extends MultiDexApplication {
}
dcContext = dcAccounts.getSelectedAccount();
notificationCenter = new NotificationCenter(this);
eventCenter = new DcEventCenter(this);
dcLocationManager = new DcLocationManager(this, dcContext);
// Mark as initialized before starting threads that depend on it
isInitialized = true;
initLock.notifyAll();
Log.i(TAG, "DcAccounts initialization complete");
new Thread(() -> {
Log.i(TAG, "Starting event loop");
DcEventEmitter emitter = dcAccounts.getEventEmitter();
Log.i(TAG, "DcEventEmitter obtained");
while (true) {
DcEvent event = emitter.getNextEvent();
if (event==null) {
break;
}
eventCenter.handleEvent(event);
}
Log.i("DeltaChat", "shutting down event handler");
}, "eventThread").start();
// set translations before starting I/O to avoid sending untranslated MDNs (issue #2288)
DcHelper.setStockTranslations(this);
@@ -50,11 +50,11 @@ public abstract class BaseConversationItem extends LinearLayout
this.rpc = DcHelper.getRpc(context);
}
protected void bindPartial(@NonNull DcMsg messageRecord,
@NonNull DcChat dcChat,
@NonNull Set<DcMsg> batchSelected,
boolean pulseHighlight,
@NonNull Recipient conversationRecipient)
protected void bind(@NonNull DcMsg messageRecord,
@NonNull DcChat dcChat,
@NonNull Set<DcMsg> batchSelected,
boolean pulseHighlight,
@NonNull Recipient conversationRecipient)
{
this.messageRecord = messageRecord;
this.dcChat = dcChat;
@@ -126,8 +126,6 @@ public abstract class BaseConversationItem extends LinearLayout
public void onClick(View v) {
if (!shouldInterceptClicks(messageRecord) && parent != null) {
// The click workaround on ConversationItem shall be revised.
// In fact, it is probably better rethinking accessibility approach for the items.
if (batchSelected.isEmpty() && Util.isTouchExplorationEnabled(context)) {
BaseConversationItem.this.onAccessibilityClick();
}
@@ -6,8 +6,6 @@ import androidx.annotation.Nullable;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -19,9 +17,7 @@ public interface BindableConversationItem extends Unbindable {
@NonNull GlideRequests glideRequests,
@NonNull Set<DcMsg> batchSelected,
@NonNull Recipient recipients,
boolean pulseHighlight,
@Nullable AudioPlaybackViewModel playbackViewModel,
AudioView.OnActionListener audioPlayPauseListener);
boolean pulseHighlight);
DcMsg getMessageRecord();
@@ -23,7 +23,9 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Activity container for selecting a list of contacts.
@@ -34,7 +36,9 @@ import java.util.List;
public class ContactMultiSelectionActivity extends ContactSelectionActivity {
public static final String CONTACTS_EXTRA = "contacts_extra";
public static final String DESELECTED_CONTACTS_EXTRA = "deselected_contacts_extra";
public static final String REMOVED_CONTACTS_EXTRA = "removed_contacts_extra";
private ArrayList<Integer> preselectedContacts;
@Override
protected void onCreate(Bundle icicle, boolean ready) {
@@ -45,6 +49,9 @@ public class ContactMultiSelectionActivity extends ContactSelectionActivity {
// it's a bit confusing having one "X" button on the left and one on the right -
// and the "clear search" button is not that important.
getToolbar().setUseClearButton(false);
// Store preselected contacts to track which ones were removed
preselectedContacts = getIntent().getIntegerArrayListExtra(ContactSelectionListFragment.PRESELECTED_CONTACTS);
}
@Override
@@ -72,9 +79,20 @@ public class ContactMultiSelectionActivity extends ContactSelectionActivity {
private void saveSelection() {
Intent resultIntent = getIntent();
List<Integer> selectedContacts = contactsFragment.getSelectedContacts();
List<Integer> deselectedContacts = contactsFragment.getDeselectedContacts();
resultIntent.putIntegerArrayListExtra(CONTACTS_EXTRA, new ArrayList<>(selectedContacts));
resultIntent.putIntegerArrayListExtra(DESELECTED_CONTACTS_EXTRA, new ArrayList<>(deselectedContacts));
// Calculate which contacts were removed (preselected but not in final selection)
if (preselectedContacts != null) {
Set<Integer> selectedSet = new HashSet<>(selectedContacts);
ArrayList<Integer> removedContacts = new ArrayList<>();
for (Integer preselectedId : preselectedContacts) {
if (!selectedSet.contains(preselectedId)) {
removedContacts.add(preselectedId);
}
}
resultIntent.putIntegerArrayListExtra(REMOVED_CONTACTS_EXTRA, removedContacts);
}
setResult(RESULT_OK, resultIntent);
}
}
@@ -58,7 +58,6 @@ import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -84,7 +83,6 @@ public class ContactSelectionListFragment extends Fragment
private DcContext dcContext;
private Set<Integer> selectedContacts;
private Set<Integer> deselectedContacts;
private OnContactSelectedListener onContactSelectedListener;
private String cursorFilter;
private RecyclerView recyclerView;
@@ -129,7 +127,6 @@ public class ContactSelectionListFragment extends Fragment
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.contact_list, menu);
menu.findItem(R.id.menu_delete_selected).setVisible(!isMulti());
updateActionModeState(actionMode);
return true;
}
@@ -229,15 +226,6 @@ public class ContactSelectionListFragment extends Fragment
return selected;
}
public @NonNull List<Integer> getDeselectedContacts() {
List<Integer> deselected = new LinkedList<>();
if (deselectedContacts != null) {
deselected.addAll(deselectedContacts);
}
return deselected;
}
private boolean isMulti() {
return getActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
}
@@ -253,7 +241,6 @@ public class ContactSelectionListFragment extends Fragment
isMulti(),
true);
selectedContacts = adapter.getSelectedContacts();
deselectedContacts = new HashSet<>();
ArrayList<Integer> preselectedContacts = getActivity().getIntent().getIntegerArrayListExtra(PRESELECTED_CONTACTS);
if(preselectedContacts!=null) {
selectedContacts.addAll(preselectedContacts);
@@ -322,14 +309,12 @@ public class ContactSelectionListFragment extends Fragment
}
selectedContacts.add(contactId);
deselectedContacts.remove(contactId);
contact.setChecked(true);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactSelected(contactId);
}
} else {
selectedContacts.remove(contactId);
deselectedContacts.add(contactId);
contact.setChecked(false);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactDeselected(contactId);
@@ -370,7 +355,6 @@ public class ContactSelectionListFragment extends Fragment
int contactId = data.getIntExtra(NewContactActivity.CONTACT_ID_EXTRA, 0);
if (contactId != 0) {
selectedContacts.add(contactId);
deselectedContacts.remove(contactId);
}
getLoaderManager().restartLoader(0, null, ContactSelectionListFragment.this);
}
@@ -25,7 +25,6 @@ import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
@@ -66,12 +65,7 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.core.view.WindowCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.session.MediaController;
import androidx.media3.session.SessionCommand;
import androidx.media3.session.SessionToken;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
@@ -82,7 +76,7 @@ import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.audio.AudioRecorder;
import org.thoughtcrime.securesms.calls.CallUtil;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.components.AttachmentTypeSelector;
import org.thoughtcrime.securesms.components.ComposeText;
@@ -92,8 +86,6 @@ import org.thoughtcrime.securesms.components.InputPanel;
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener;
import org.thoughtcrime.securesms.components.ScaleStableImageView;
import org.thoughtcrime.securesms.components.SendButton;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcEventCenter;
@@ -112,19 +104,19 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
import org.thoughtcrime.securesms.service.AudioPlaybackService;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.ShareUtil;
import org.thoughtcrime.securesms.util.SendRelayedMessageUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.ShareUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.thoughtcrime.securesms.util.guava.Optional;
import org.thoughtcrime.securesms.util.views.ProgressDialog;
import org.thoughtcrime.securesms.video.recode.VideoRecoder;
import org.thoughtcrime.securesms.calls.CallUtil;
import java.io.File;
import java.util.ArrayList;
@@ -146,13 +138,14 @@ import chat.delta.util.SettableFuture;
*/
@SuppressLint("StaticFieldLeak")
public class ConversationActivity extends PassphraseRequiredActionBarActivity
implements ConversationFragment.ConversationFragmentListener,
AttachmentManager.AttachmentListener,
SearchView.OnQueryTextListener,
DcEventCenter.DcEventDelegate,
OnKeyboardShownListener,
InputPanel.Listener,
InputPanel.MediaListener, AudioView.OnActionListener {
implements ConversationFragment.ConversationFragmentListener,
AttachmentManager.AttachmentListener,
SearchView.OnQueryTextListener,
DcEventCenter.DcEventDelegate,
OnKeyboardShownListener,
InputPanel.Listener,
InputPanel.MediaListener
{
private static final String TAG = ConversationActivity.class.getSimpleName();
public static final String ACCOUNT_ID_EXTRA = "account_id";
@@ -192,9 +185,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private MediaKeyboard emojiPicker;
protected HidingLinearLayout quickAttachmentToggle;
private InputPanel inputPanel;
private @Nullable MediaController mediaController;
private com.google.common.util.concurrent.ListenableFuture<MediaController> mediaControllerFuture;
private AudioPlaybackViewModel playbackViewModel;
private ApplicationContext context;
private Recipient recipient;
@@ -227,10 +217,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
initializeActionBar();
initializeViews();
initializeResources();
playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class);
initializeMediaController();
initializeSecurity(false, isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
@@ -281,36 +267,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
private void initializeMediaController() {
SessionToken sessionToken = new SessionToken(this,
new ComponentName(this, AudioPlaybackService.class));
mediaControllerFuture = new MediaController.Builder(this, sessionToken)
.buildAsync();
mediaControllerFuture.addListener(() -> {
try {
mediaController = mediaControllerFuture.get();
playbackViewModel.setMediaController(mediaController);
} catch (Exception e) {
Log.e(TAG, "Error connecting to audio playback service", e);
}
}, ContextCompat.getMainExecutor(this));
}
private void addActivityContext(Bundle extras, String activityClassName) {
if (mediaController == null) return;
Bundle commandArgs = new Bundle();
commandArgs.putString("activity_class", activityClassName);
if (extras != null) {
commandArgs.putAll(extras);
}
SessionCommand updateContextCommand =
new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY);
mediaController.sendCustomCommand(updateContextCommand, commandArgs);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
@@ -381,6 +337,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
DcHelper.getNotificationCenter(this).clearVisibleChat();
if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
inputPanel.onPause();
AudioSlidePlayer.stopAll();
}
@Override
@@ -400,11 +357,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
protected void onDestroy() {
DcHelper.getEventCenter(this).removeObservers(this);
if (mediaController != null) {
MediaController.releaseFuture(mediaControllerFuture);
mediaController = null;
playbackViewModel.setMediaController(null);
}
super.onDestroy();
}
@@ -617,11 +569,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} else if (itemId == R.id.menu_show_map) {
WebxdcActivity.openMaps(this, chatId);
return true;
} else if (itemId == R.id.menu_start_audio_call) {
CallUtil.startCall(this, chatId, false);
return true;
} else if (itemId == R.id.menu_start_video_call) {
CallUtil.startCall(this, chatId, true);
} else if (itemId == R.id.menu_start_call) {
CallUtil.startCall(this, chatId);
return true;
} else if (itemId == R.id.menu_all_media) {
handleAllMedia();
@@ -682,8 +631,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
extras.putInt(ConversationListFragment.RELOAD_LIST, 1);
}
playbackViewModel.stopNonMessageAudioPlayback();
boolean archived = getIntent().getBooleanExtra(FROM_ARCHIVED_CHATS_EXTRA, false);
Intent intent = new Intent(this, (archived ? ConversationListArchiveActivity.class : ConversationListActivity.class));
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
@@ -1057,30 +1004,20 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
recipient = new Recipient(this, dcChat);
glideRequests = GlideApp.with(this);
setComposePanelVisibility(true);
setComposePanelVisibility();
initializeContactRequest();
}
private void setComposePanelVisibility(boolean isInitialization) {
private void setComposePanelVisibility() {
if (dcChat.canSend()) {
composePanel.setVisibility(View.VISIBLE);
attachmentManager.setHidden(false);
inputPanel.setSubjectVisible(!dcChat.isEncrypted());
// FIXME: disabled for now to avoid problems with chat scrolling and keyboard covering input bar
// ViewUtil.forceApplyWindowInsets(findViewById(R.id.root_layout), true, false, true, true);
// fragment.handleRemoveBottomInsets();
} else {
composePanel.setVisibility(View.GONE);
attachmentManager.setHidden(true);
hideSoftKeyboard();
inputPanel.setSubjectVisible(false);
// FIXME: disabled for now to avoid problems with chat scrolling and keyboard covering input bar
/*
if (isInitialization) {
ViewUtil.forceApplyWindowInsets(findViewById(R.id.root_layout), true, false, true, false);
fragment.handleAddBottomInsets();
}
*/
}
}
@@ -1117,11 +1054,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return new SettableFuture<>(false);
}
return attachmentManager.setMedia(glideRequests, uri, null, mediaType, 0, 0, chatId, playbackViewModel);
return attachmentManager.setMedia(glideRequests, uri, null, mediaType, 0, 0, chatId);
}
private ListenableFuture<Boolean> setMedia(DcMsg msg, @NonNull MediaType mediaType) {
return attachmentManager.setMedia(glideRequests, Uri.fromFile(new File(msg.getFile())), msg, mediaType, 0, 0, chatId, playbackViewModel);
return attachmentManager.setMedia(glideRequests, Uri.fromFile(new File(msg.getFile())), msg, mediaType, 0, 0, chatId);
}
private void addAttachmentContactInfo(int contactId) {
@@ -1173,10 +1110,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
inputPanel.clearSubject();
}
// Stop draft audio playback regardless, since it is unlikely
// we will need background playback for drafts
playbackViewModel.stopNonMessageAudioPlayback();
DcContext dcContext = DcHelper.getContext(context);
Util.runOnAnyBackgroundThread(() -> {
DcMsg msg = null;
@@ -1489,14 +1422,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
// Listeners
@Override
public void onPlayPauseButtonClicked(View view) {
addActivityContext(
this.getIntent().getExtras(),
this.getClass().getName()
);
}
private class AttachmentTypeListener implements AttachmentTypeSelector.AttachmentClickedListener {
@Override
public void onClick(int type) {
@@ -1665,7 +1590,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
dcChat = dcContext.getChat(chatId);
titleView.setTitle(glideRequests, dcChat);
initializeSecurity(isSecureText, isDefaultSms);
setComposePanelVisibility(false);
setComposePanelVisibility();
initializeContactRequest();
} else if ((eventId == DcContext.DC_EVENT_INCOMING_MSG
|| eventId == DcContext.DC_EVENT_MSG_READ)
@@ -34,8 +34,6 @@ import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -61,7 +59,6 @@ import java.util.Set;
* @author Moxie Marlinspike
*
*/
// FIXME: this breaks type checks, that is why there are so many casts.
public class ConversationAdapter <V extends View & BindableConversationItem>
extends RecyclerView.Adapter
implements StickyHeaderDecoration.StickyHeaderAdapter<HeaderViewHolder>
@@ -100,8 +97,6 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
private long pulseHighlightingSince = -1;
private int lastSeenPosition = -1;
private long lastSeen = -1;
private AudioPlaybackViewModel playbackViewModel;
private AudioView.OnActionListener audioPlayPauseListener;
protected static class ViewHolder extends RecyclerView.ViewHolder {
public <V extends View & BindableConversationItem> ViewHolder(final @NonNull V itemView) {
@@ -175,14 +170,6 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
return fromDb;
}
public void setPlaybackViewModel(AudioPlaybackViewModel playbackViewModel) {
this.playbackViewModel = playbackViewModel;
}
public void setAudioPlayPauseListener(AudioView.OnActionListener audioPlayPauseListener) {
this.audioPlayPauseListener = audioPlayPauseListener;
}
/**
* Returns the position of the message with msgId in the chat list, counted from the top
*/
@@ -250,7 +237,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
long elapsed = now - pulseHighlightingSince;
boolean pulseHighlight = (positionCurrentlyPulseHighlighting == position && elapsed < PULSE_HIGHLIGHT_MILLIS);
holder.getItem().bind(getMsg(position), dcChat, glideRequests, batchSelected, recipient, pulseHighlight, playbackViewModel, audioPlayPauseListener);
holder.getItem().bind(getMsg(position), dcChat, glideRequests, batchSelected, recipient, pulseHighlight);
}
@Override
@@ -41,7 +41,6 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
@@ -53,7 +52,6 @@ import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.ConversationAdapter.ItemClickListener;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.reminder.DozeReminder;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
@@ -103,7 +101,6 @@ public class ConversationFragment extends MessageSelectorFragment
private StickyHeaderDecoration dateDecoration;
private View scrollToBottomButton;
private View floatingLocationButton;
private View bottomDivider;
private AddReactionView addReactionView;
private TextView noMessageTextView;
private Timer reloadTimer;
@@ -111,8 +108,6 @@ public class ConversationFragment extends MessageSelectorFragment
public boolean isPaused;
private Debouncer markseenDebouncer;
private Rpc rpc;
private boolean pendingAddBottomInsets;
private boolean pendingRemoveBottomInsets;
@Override
public void onCreate(Bundle icicle) {
@@ -146,7 +141,6 @@ public class ConversationFragment extends MessageSelectorFragment
floatingLocationButton = ViewUtil.findById(view, R.id.floating_location_button);
addReactionView = ViewUtil.findById(view, R.id.add_reaction_view);
noMessageTextView = ViewUtil.findById(view, R.id.no_messages_text_view);
bottomDivider = ViewUtil.findById(view, R.id.bottom_divider);
scrollToBottomButton.setOnClickListener(v -> scrollToBottom());
@@ -165,32 +159,18 @@ public class ConversationFragment extends MessageSelectorFragment
// with hardware layers, drawing may result in errors as "OpenGLRenderer: Path too large to be rendered into a texture"
list.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
if (pendingAddBottomInsets) {
bottomDivider.setVisibility(View.GONE);
ViewUtil.forceApplyWindowInsets(list, false, true, false, true);
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, true);
pendingAddBottomInsets = false;
}
if (pendingRemoveBottomInsets) {
bottomDivider.setVisibility(View.VISIBLE);
ViewUtil.forceApplyWindowInsets(list, false, true, false, false);
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, false);
pendingRemoveBottomInsets = false;
}
return view;
}
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
public void onActivityCreated(Bundle bundle) {
super.onActivityCreated(bundle);
initializeResources();
initializeListAdapter();
}
private void setNoMessageText() {
private void setNoMessageText() {
DcChat dcChat = getListAdapter().getChat();
if(dcChat.isMultiUser()){
if (dcChat.isInBroadcast() || dcChat.isOutBroadcast()) {
@@ -217,28 +197,6 @@ public class ConversationFragment extends MessageSelectorFragment
}
}
public void handleAddBottomInsets() {
if (bottomDivider != null) {
bottomDivider.setVisibility(View.GONE);
ViewUtil.forceApplyWindowInsets(list, false, true, false, true);
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, true);
pendingAddBottomInsets = false;
} else {
pendingAddBottomInsets = true;
}
}
public void handleRemoveBottomInsets() {
if (bottomDivider != null) {
bottomDivider.setVisibility(View.VISIBLE);
ViewUtil.forceApplyWindowInsets(list, false, true, false, false);
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, false);
pendingRemoveBottomInsets = false;
} else {
pendingRemoveBottomInsets = true;
}
}
@Override
public void onDestroy() {
DcHelper.getEventCenter(getContext()).removeObservers(this);
@@ -333,10 +291,6 @@ public class ConversationFragment extends MessageSelectorFragment
if (this.recipient != null && this.chatId != -1) {
ConversationAdapter adapter = new ConversationAdapter(getActivity(), this.recipient.getChat(), GlideApp.with(this), selectionClickListener, this.recipient);
list.setAdapter(adapter);
AudioPlaybackViewModel playbackViewModel =
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
adapter.setPlaybackViewModel(playbackViewModel);
adapter.setAudioPlayPauseListener(((ConversationActivity) requireActivity()));
if (dateDecoration != null) {
list.removeItemDecoration(dateDecoration);
@@ -453,14 +407,7 @@ public class ConversationFragment extends MessageSelectorFragment
}
public void handleClearChat() {
AudioPlaybackViewModel playbackViewModel =
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
handleDeleteMessages(
(int) chatId,
getListAdapter().getMessageIds(),
playbackViewModel::stopByIds,
playbackViewModel::stopByIds);
handleDeleteMessages((int) chatId, getListAdapter().getMessageIds());
}
private ConversationAdapter getListAdapter() {
@@ -944,7 +891,7 @@ public class ConversationFragment extends MessageSelectorFragment
@Override
public void onReactionClicked(DcMsg messageRecord) {
ReactionsDetailsFragment dialog = ReactionsDetailsFragment.newInstance(messageRecord.getId());
ReactionsDetailsFragment dialog = new ReactionsDetailsFragment(messageRecord.getId());
dialog.show(getActivity().getSupportFragmentManager(), null);
}
}
@@ -993,15 +940,12 @@ public class ConversationFragment extends MessageSelectorFragment
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
hideAddReactionView();
int itemId = item.getItemId();
AudioPlaybackViewModel playbackViewModel =
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
if (itemId == R.id.menu_context_copy) {
handleCopyMessage(getListAdapter().getSelectedItems());
actionMode.finish();
return true;
} else if (itemId == R.id.menu_context_delete_message) {
handleDeleteMessages((int) chatId, getListAdapter().getSelectedItems(), playbackViewModel::stopByIds, playbackViewModel::stopByIds);
handleDeleteMessages((int) chatId, getListAdapter().getSelectedItems());
return true;
} else if (itemId == R.id.menu_context_share) {
DcHelper.openForViewOrShare(getContext(), getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId(), Intent.ACTION_SEND);
@@ -41,7 +41,8 @@ import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.calls.CallUtil;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.BorderlessImageView;
import org.thoughtcrime.securesms.components.CallItemView;
@@ -51,8 +52,6 @@ import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.QuoteView;
import org.thoughtcrime.securesms.components.VcardView;
import org.thoughtcrime.securesms.components.WebxdcView;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.DocumentSlide;
@@ -72,6 +71,7 @@ import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub;
import org.thoughtcrime.securesms.calls.CallUtil;
import java.util.List;
import java.util.Set;
@@ -181,11 +181,9 @@ public class ConversationItem extends BaseConversationItem
@NonNull GlideRequests glideRequests,
@NonNull Set<DcMsg> batchSelected,
@NonNull Recipient recipients,
boolean pulseHighlight,
@Nullable AudioPlaybackViewModel playbackViewModel,
AudioView.OnActionListener audioPlayPauseListener)
boolean pulseHighlight)
{
bindPartial(messageRecord, dcChat, batchSelected, pulseHighlight, recipients);
bind(messageRecord, dcChat, batchSelected, pulseHighlight, recipients);
this.glideRequests = glideRequests;
this.showSender = ((dcChat.isMultiUser() || dcChat.isSelfTalk()) && !messageRecord.isOutgoing()) || messageRecord.getOverrideSenderName() != null;
@@ -206,7 +204,7 @@ public class ConversationItem extends BaseConversationItem
setGutterSizes(messageRecord, showSender);
setMessageShape(messageRecord);
setMediaAttributes(messageRecord, showSender, playbackViewModel, audioPlayPauseListener);
setMediaAttributes(messageRecord, showSender);
setBodyText(messageRecord);
setBubbleState(messageRecord);
setContactPhoto();
@@ -484,10 +482,24 @@ public class ConversationItem extends BaseConversationItem
}
private void setMediaAttributes(@NonNull DcMsg messageRecord,
boolean showSender,
AudioPlaybackViewModel playbackViewModel,
AudioView.OnActionListener audioPlayPauseListener)
boolean showSender)
{
class SetDurationListener implements AudioSlidePlayer.Listener {
@Override
public void onStart() {}
@Override
public void onStop() {}
@Override
public void onProgress(AudioSlide slide, double progress, long millis) {}
@Override
public void onReceivedDuration(int millis) {
messageRecord.lateFilingMediaSize(0,0, millis);
audioViewStub.get().setDuration(millis);
}
}
if (hasAudio(messageRecord)) {
audioViewStub.get().setVisibility(View.VISIBLE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
@@ -497,9 +509,15 @@ public class ConversationItem extends BaseConversationItem
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
if (callViewStub.resolved()) callViewStub.get().setVisibility(View.GONE);
audioViewStub.get().setPlaybackViewModel(playbackViewModel);
audioViewStub.get().setOnActionListener(audioPlayPauseListener);
audioViewStub.get().setAudio(new AudioSlide(context, messageRecord));
//noinspection ConstantConditions
int duration = messageRecord.getDuration();
if (duration == 0) {
AudioSlide audio = new AudioSlide(context, messageRecord);
AudioSlidePlayer audioSlidePlayer = AudioSlidePlayer.createFor(getContext(), audio, new SetDurationListener());
audioSlidePlayer.requestDuration();
}
audioViewStub.get().setAudio(new AudioSlide(context, messageRecord), duration);
audioViewStub.get().setOnClickListener(passthroughClickListener);
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
audioViewStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
@@ -982,9 +1000,9 @@ public class ConversationItem extends BaseConversationItem
int chatId = messageRecord.getChatId();
if (!messageRecord.isOutgoing() && callInfo.state instanceof CallState.Alerting) {
int callId = messageRecord.getId();
CallUtil.openCall(getContext(), accId, chatId, callId, callInfo.sdpOffer, callInfo.hasVideo);
CallUtil.openCall(getContext(), accId, chatId, callId, callInfo.sdpOffer);
} else {
CallUtil.startCall(getContext(), accId, chatId, callInfo.hasVideo);
CallUtil.startCall(getContext(), accId, chatId);
}
}
}
@@ -99,7 +99,6 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
public static final String CLEAR_NOTIFICATIONS = "clear_notifications";
public static final String ACCOUNT_ID_EXTRA = "account_id";
public static final String FROM_WELCOME = "from_welcome";
public static final String FROM_WELCOME_RAW_QR = "from_welcome_raw_qr";
private static final int REQUEST_CODE_CONFIRM_CREDENTIALS_DELETE_PROFILE = ScreenLockUtil.REQUEST_CODE_CONFIRM_CREDENTIALS+1;
private ConversationListFragment conversationListFragment;
@@ -194,13 +193,6 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
if (BuildConfig.DEBUG) checkNdkArchitecture();
DcHelper.maybeShowMigrationError(this);
String rawQrString = getIntent().getStringExtra(FROM_WELCOME_RAW_QR);
// Launch chat directly, if coming from onboarding with a join chat/group QR
if (rawQrString != null) {
QrCodeHandler qrCodeHandler = new QrCodeHandler(this);
qrCodeHandler.secureJoinByQr(rawQrString, SecurejoinSource.Scan, SecurejoinUiPath.Unknown);
}
}
/**
@@ -629,7 +621,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
refreshAvatar();
refreshUnreadIndicator();
refreshTitle();
conversationListFragment.loadChatlistAsync();
conversationListFragment.loadChatlist();
}
public void onDeleteProfile(int profileId) {
@@ -233,7 +233,7 @@ public class ConversationListFragment extends BaseConversationListFragment
private final Object loadChatlistLock = new Object();
private boolean inLoadChatlist;
private boolean needsAnotherLoad;
public void loadChatlistAsync() {
private void loadChatlistAsync() {
synchronized (loadChatlistLock) {
needsAnotherLoad = true;
if (inLoadChatlist) {
@@ -260,7 +260,7 @@ public class ConversationListFragment extends BaseConversationListFragment
});
}
private void loadChatlist() {
public void loadChatlist() {
int listflags = 0;
if (archive) {
listflags |= DcContext.DC_GCL_ARCHIVED_ONLY;
@@ -271,6 +271,12 @@ public class ConversationListItem extends RelativeLayout
} else {
deliveryStatusIndicator.setNone();
}
if (state == DcMsg.DC_STATE_OUT_FAILED) {
deliveryStatusIndicator.setTint(Color.RED);
} else {
deliveryStatusIndicator.resetTint();
}
}
int unreadCount = thread.getUnreadCount();
@@ -15,8 +15,6 @@ import com.b44t.messenger.DcMsg;
import org.json.JSONObject;
import org.thoughtcrime.securesms.components.DeliveryStatusView;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.JsonUtils;
@@ -63,11 +61,9 @@ public class ConversationUpdateItem extends BaseConversationItem
@NonNull GlideRequests glideRequests,
@NonNull Set<DcMsg> batchSelected,
@NonNull Recipient conversationRecipient,
boolean pulseUpdate,
@Nullable AudioPlaybackViewModel playbackViewModel,
AudioView.OnActionListener audioPlayPauseListener)
boolean pulseUpdate)
{
bindPartial(messageRecord, dcChat, batchSelected, pulseUpdate, conversationRecipient);
bind(messageRecord, dcChat, batchSelected, pulseUpdate, conversationRecipient);
setGenericInfoRecord(messageRecord);
}
@@ -123,6 +119,12 @@ public class ConversationUpdateItem extends BaseConversationItem
else if (messageRecord.isPreparing()) deliveryStatusView.setPreparing();
else if (messageRecord.isPending()) deliveryStatusView.setPending();
else deliveryStatusView.setNone();
if (messageRecord.isFailed()) {
deliveryStatusView.setTint(Color.RED);
} else {
deliveryStatusView.setTint(textColor);
}
}
@Override
@@ -6,7 +6,6 @@ import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -45,14 +44,12 @@ import java.io.File;
import java.util.ArrayList;
import java.util.Objects;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
implements ItemClickListener
{
private static final String TAG = GroupCreateActivity.class.getSimpleName();
public static final String EDIT_GROUP_CHAT_ID = "edit_group_chat_id";
public static final String CREATE_BROADCAST = "create_broadcast";
public static final String UNENCRYPTED = "unencrypted";
@@ -66,7 +63,6 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
private boolean unencrypted;
private boolean broadcast;
private EditText groupName;
private EditText chatDescription;
private ListView lv;
private ImageView avatar;
private Bitmap avatarBmp;
@@ -144,7 +140,6 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
lv = ViewUtil.findById(this, R.id.selected_contacts_list);
avatar = ViewUtil.findById(this, R.id.avatar);
groupName = ViewUtil.findById(this, R.id.group_name);
chatDescription = ViewUtil.findById(this, R.id.chat_description);
TextView chatHints = ViewUtil.findById(this, R.id.chat_hints);
// add padding to avoid content hidden behind system bars
@@ -183,7 +178,6 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
} else if (unencrypted) {
avatar.setVisibility(View.GONE);
groupName.setHint(R.string.subject);
findViewById(R.id.chat_description_container).setVisibility(View.GONE);
chatHints.setVisibility(View.GONE);
} else {
chatHints.setVisibility(View.GONE);
@@ -192,14 +186,6 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
if(isEdit()) {
groupName.setText(dcContext.getChat(groupChatId).getName());
lv.setVisibility(View.GONE);
Rpc rpc = DcHelper.getRpc(this);
try {
String description = rpc.getChatDescription(rpc.getSelectedAccountId(), groupChatId);
chatDescription.setText(description);
} catch (RpcException e) {
Log.e(TAG, "RPC error", e);
}
}
}
@@ -275,22 +261,22 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
}
private void createGroup(String groupName) {
Rpc rpc = DcHelper.getRpc(this);
int accId;
try {
accId = rpc.getSelectedAccountId();
if (broadcast) {
groupChatId = rpc.createBroadcast(accId, groupName);
} else if (unencrypted) {
groupChatId = rpc.createGroupChatUnencrypted(accId, groupName);
} else {
groupChatId = rpc.createGroupChat(accId, groupName, false);
if (broadcast) {
try {
groupChatId = DcHelper.getRpc(this).createBroadcast(dcContext.getAccountId(), groupName);
} catch (RpcException e) {
e.printStackTrace();
return;
}
rpc.setChatDescription(accId, groupChatId, getChatDescription());
} catch (RpcException e) {
Log.e(TAG, "RPC error", e);
return;
} else if (unencrypted) {
try {
groupChatId = DcHelper.getRpc(this).createGroupChatUnencrypted(dcContext.getAccountId(), groupName);
} catch (RpcException e) {
e.printStackTrace();
return;
}
} else {
groupChatId = dcContext.createGroupChat(groupName);
}
for (int contactId : getAdapter().getContacts()) {
@@ -321,14 +307,6 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
}
dcContext.setChatName(groupChatId, groupName);
Rpc rpc = DcHelper.getRpc(this);
String description = getChatDescription();
try {
rpc.setChatDescription(rpc.getSelectedAccountId(), groupChatId, description);
} catch (RpcException e) {
Log.e(TAG, "RPC error", e);
}
if (avatarChanged) AvatarHelper.setGroupAvatar(this, groupChatId, avatarBmp);
attachmentManager.cleanup();
@@ -353,10 +331,6 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
return ret;
}
private @Nullable String getChatDescription() {
return chatDescription.getText() != null ? chatDescription.getText().toString().trim() : "";
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
@@ -63,8 +63,6 @@ import java.io.File;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
@@ -81,7 +79,6 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
private ImageView avatar;
private EditText name;
private TextView invitationText;
private TextView privacyPolicyBtn;
private Button signUpBtn;
@@ -89,11 +86,7 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
private boolean imageLoaded;
private String providerHost;
private String providerQrData;
private String rawQrData;
private DcLot parsedQrData;
private boolean isDcLogin;
private boolean isContactInvitation;
private boolean isGroupInvitation;
private AttachmentManager attachmentManager;
private Bitmap avatarBmp;
@@ -103,8 +96,6 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
private DcContext dcContext;
private ExecutorService executor = Executors.newSingleThreadExecutor();
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
@@ -215,31 +206,17 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
private void setProviderFromQr(String rawQr) {
DcLot qrParsed = dcContext.checkQr(rawQr);
switch (qrParsed.getState()) {
case DcContext.DC_QR_LOGIN:
isDcLogin = true; // Intentional fall-through
case DcContext.DC_QR_ACCOUNT:
providerHost = qrParsed.getText1();
providerQrData = rawQr;
updateProvider();
break;
case DcContext.DC_QR_ASK_VERIFYCONTACT:
isContactInvitation = true;
rawQrData = rawQr;
parsedQrData = qrParsed;
updateProvider();
break;
case DcContext.DC_QR_ASK_VERIFYGROUP:
isGroupInvitation = true;
rawQrData = rawQr;
parsedQrData = qrParsed;
updateProvider();
break;
default:
new AlertDialog.Builder(this)
.setMessage(R.string.qraccount_qr_code_cannot_be_used)
.setPositiveButton(R.string.ok, null)
.show();
boolean isDcLogin = qrParsed.getState() == DcContext.DC_QR_LOGIN;
if (isDcLogin || qrParsed.getState() == DcContext.DC_QR_ACCOUNT) {
this.isDcLogin = isDcLogin;
providerHost = qrParsed.getText1();
providerQrData = rawQr;
updateProvider();
} else {
new AlertDialog.Builder(this)
.setMessage(R.string.qraccount_qr_code_cannot_be_used)
.setPositiveButton(R.string.ok, null)
.show();
}
}
@@ -283,7 +260,6 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
public void onDestroy() {
super.onDestroy();
DcHelper.getEventCenter(this).removeObservers(this);
executor.shutdown();
}
private void handleIntent() {
@@ -335,7 +311,6 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
private void initializeResources() {
this.avatar = findViewById(R.id.avatar);
this.name = findViewById(R.id.name_text);
this.invitationText = findViewById(R.id.invitation_label);
this.privacyPolicyBtn = findViewById(R.id.privacy_policy_button);
this.signUpBtn = findViewById(R.id.signup_button);
@@ -383,19 +358,6 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
privacyPolicyBtn.setText(TextUtil.markAsExternal(
getString(R.string.instant_onboarding_agree_instance, providerHost)));
}
if (parsedQrData != null) {
if (isContactInvitation) {
String name = dcContext.getContact(parsedQrData.getId()).getDisplayName();
invitationText.setText(this.getString(R.string.instant_onboarding_contact_info, name));
invitationText.setVisibility(View.VISIBLE);
} else if (isGroupInvitation) {
String groupName = parsedQrData.getText1();
invitationText.setText(this.getString(R.string.instant_onboarding_group_info, groupName));
invitationText.setVisibility(View.VISIBLE);
}
}
}
}
@@ -457,14 +419,11 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
Intent intent = new Intent(getApplicationContext(), ConversationListActivity.class);
intent.putExtra(ConversationListActivity.FROM_WELCOME, true);
if (isContactInvitation || isGroupInvitation) {
intent.putExtra(ConversationListActivity.FROM_WELCOME_RAW_QR, rawQrData);
}
startActivity(intent);
finishAffinity();
}
@SuppressLint("StaticFieldLeak")
private void createProfile() {
if (TextUtils.isEmpty(this.name.getText())) {
Toast.makeText(this, R.string.please_enter_name, Toast.LENGTH_LONG).show();
@@ -472,31 +431,37 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
}
final String name = this.name.getText().toString();
executor.execute(() -> {
Context context = InstantOnboardingActivity.this;
DcHelper.set(context, DcHelper.CONFIG_DISPLAY_NAME, name);
new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... params) {
Context context = InstantOnboardingActivity.this;
DcHelper.set(context, DcHelper.CONFIG_DISPLAY_NAME, name);
boolean result = true;
if (avatarChanged) {
try {
AvatarHelper.setSelfAvatar(InstantOnboardingActivity.this, avatarBmp);
Prefs.setProfileAvatarId(InstantOnboardingActivity.this, new SecureRandom().nextInt());
} catch (IOException e) {
Log.w(TAG, e);
result = false;
if (avatarChanged) {
try {
AvatarHelper.setSelfAvatar(InstantOnboardingActivity.this, avatarBmp);
Prefs.setProfileAvatarId(InstantOnboardingActivity.this, new SecureRandom().nextInt());
} catch (IOException e) {
Log.w(TAG, e);
return false;
}
}
return true;
}
boolean finalResult = result;
runOnUiThread(() -> {
if (finalResult) {
@Override
public void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (result) {
attachmentManager.cleanup();
startQrAccountCreation(providerQrData);
} else {
Toast.makeText(InstantOnboardingActivity.this, R.string.error, Toast.LENGTH_LONG).show();
}
});
});
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void startQrAccountCreation(String qrCode)
@@ -129,7 +129,7 @@ public class LogViewFragment extends Fragment {
return logFile;
}
public static String grabLogcat() {
private static String grabLogcat(LogViewFragment fragment) {
String command = "logcat -v threadtime -d -t 10000 *:I";
try {
final Process process = Runtime.getRuntime().exec(command);
@@ -165,7 +165,7 @@ public class LogViewFragment extends Fragment {
if (fragment == null) return null;
return "**This log may contain sensitive information. If you want to post it publicly you may examine and edit it beforehand.**\n\n" +
buildDescription(fragment) + "\n" + grabLogcat();
buildDescription(fragment) + "\n" + grabLogcat(fragment);
}
@Override
@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms;
import android.Manifest;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
@@ -13,7 +12,6 @@ import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.ActionMode;
import androidx.core.util.Consumer;
import androidx.fragment.app.Fragment;
import com.b44t.messenger.DcChat;
@@ -59,14 +57,10 @@ public abstract class MessageSelectorFragment
}
protected void handleDeleteMessages(int chatId, final Set<DcMsg> messageRecords) {
handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords), null, null);
handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords));
}
protected void handleDeleteMessages(int chatId, final Set<DcMsg> messageRecords, Consumer<int[]> deleteForMeListenerExtra, Consumer<int[]> deleteForAllListenerExtra) {
handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords), deleteForMeListenerExtra, deleteForAllListenerExtra);
}
protected void handleDeleteMessages(int chatId, final int[] messageIds, Consumer<int[]> deleteForMeListenerExtra, Consumer<int[]> deleteForAllListenerExtra) {
protected void handleDeleteMessages(int chatId, final int[] messageIds) {
DcContext dcContext = DcHelper.getContext(getContext());
DcChat dcChat = dcContext.getChat(chatId);
boolean canDeleteForAll = true;
@@ -85,24 +79,20 @@ public abstract class MessageSelectorFragment
String text = getActivity().getResources().getQuantityString(R.plurals.ask_delete_messages, messageIds.length, messageIds.length);
int positiveBtnLabel = dcChat.isSelfTalk() ? R.string.delete : R.string.delete_for_me;
DialogInterface.OnClickListener deleteForMeListener = (d, which) -> {
Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds));
if (actionMode != null) actionMode.finish();
if (deleteForMeListenerExtra != null) deleteForMeListenerExtra.accept(messageIds);
};
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity())
.setMessage(text)
.setCancelable(true)
.setNeutralButton(android.R.string.cancel, null)
.setPositiveButton(positiveBtnLabel, deleteForMeListener);
.setPositiveButton(positiveBtnLabel, (d, which) -> {
Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds));
if (actionMode != null) actionMode.finish();
});
if(canDeleteForAll) {
DialogInterface.OnClickListener deleteForAllListener = (d, which) -> {
builder.setNegativeButton(R.string.delete_for_everyone, (d, which) -> {
Util.runOnAnyBackgroundThread(() -> dcContext.sendDeleteRequest(messageIds));
if (actionMode != null) actionMode.finish();
if (deleteForAllListenerExtra != null) deleteForAllListenerExtra.accept(messageIds);
};
builder.setNegativeButton(R.string.delete_for_everyone, deleteForAllListener);
});
AlertDialog dialog = builder.show();
Util.redButton(dialog, AlertDialog.BUTTON_NEGATIVE);
Util.redPositiveButton(dialog);
@@ -22,7 +22,10 @@ import static org.thoughtcrime.securesms.util.ShareUtil.acquireRelayMessageConte
import static org.thoughtcrime.securesms.util.ShareUtil.isRelayingMessageContent;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
@@ -34,6 +37,7 @@ import com.google.zxing.integration.android.IntentResult;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.qr.QrActivity;
import org.thoughtcrime.securesms.qr.QrCodeHandler;
import org.thoughtcrime.securesms.util.MailtoUtil;
import chat.delta.rpc.types.SecurejoinSource;
import chat.delta.rpc.types.SecurejoinUiPath;
@@ -53,6 +57,53 @@ public class NewConversationActivity extends ContactSelectionActivity {
super.onCreate(bundle, ready);
assert getSupportActionBar() != null;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
handleIntent();
}
private void handleIntent() {
Intent intent = getIntent();
String action = intent.getAction();
if(Intent.ACTION_VIEW.equals(action) || Intent.ACTION_SENDTO.equals(action)) {
try {
Uri uri = intent.getData();
if(uri != null) {
String scheme = uri.getScheme();
if(MailtoUtil.isMailto(uri)) {
String textToShare = MailtoUtil.getText(uri);
String[] recipientsArray = MailtoUtil.getRecipients(uri);
if (recipientsArray.length >= 1) {
if (!textToShare.isEmpty()) {
getIntent().putExtra(TEXT_EXTRA, textToShare);
}
final String addr = recipientsArray[0];
final DcContext dcContext = DcHelper.getContext(this);
int contactId = dcContext.lookupContactIdByAddr(addr);
if (contactId == 0 && dcContext.mayBeValidAddr(addr)) {
contactId = dcContext.createContact(null, recipientsArray[0]);
}
if (contactId == 0) {
Toast.makeText(this, R.string.bad_email_address, Toast.LENGTH_LONG).show();
} else {
onContactSelected(contactId);
}
} else {
Intent shareIntent = new Intent(this, ShareActivity.class);
shareIntent.putExtra(Intent.EXTRA_TEXT, textToShare);
startActivity(shareIntent);
finish();
}
} else if(scheme != null && scheme.startsWith("http")) {
Intent shareIntent = new Intent(this, ShareActivity.class);
shareIntent.putExtra(Intent.EXTRA_TEXT, uri.toString());
startActivity(shareIntent);
finish();
}
}
}
catch(Exception e) {
Log.e(TAG, "start activity from external 'mailto:' link failed", e);
}
}
}
@Override
@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -31,13 +30,8 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class ProfileAdapter extends RecyclerView.Adapter
{
private static final String TAG = ProfileAdapter.class.getSimpleName();
public static final int ITEM_AVATAR = 10;
public static final int ITEM_DIVIDER = 20;
public static final int ITEM_SIGNATURE = 25;
@@ -201,7 +195,7 @@ public class ProfileAdapter extends RecyclerView.Adapter
}
else if(holder.itemView instanceof ProfileStatusItem) {
ProfileStatusItem item = (ProfileStatusItem) holder.itemView;
item.setOnLongClickListener(view -> {clickListener.onStatusLongClicked(dcContact == null); return true;});
item.setOnLongClickListener(view -> {clickListener.onStatusLongClicked(); return true;});
item.set(data.label);
}
else if(holder.itemView instanceof ProfileAvatarItem) {
@@ -236,7 +230,7 @@ public class ProfileAdapter extends RecyclerView.Adapter
public interface ItemClickListener {
void onSettingsClicked(int settingsId);
void onStatusLongClicked(boolean isMultiUser);
void onStatusLongClicked();
void onSharedChatClicked(int chatId);
void onMemberClicked(int contactId);
void onMemberLongClicked(int contactId);
@@ -284,21 +278,8 @@ public class ProfileAdapter extends RecyclerView.Adapter
itemData.add(new ItemData(ITEM_AVATAR, null, 0));
if (isSelfTalk) {
itemDataStatusText = context.getString(R.string.saved_messages_explain);
} else if (dcContact != null) {
itemDataStatusText = dcContact.getStatus();
} else if (dcChat != null && dcChat.isEncrypted()) {
// Load group or channel description
try {
Rpc rpc = DcHelper.getRpc(context);
itemDataStatusText = rpc.getChatDescription(rpc.getSelectedAccountId(), dcChat.getId());
} catch (RpcException e) {
Log.e(TAG, "RPC error", e);
}
}
if (!itemDataStatusText.isEmpty()) {
if (isSelfTalk || dcContact != null && !dcContact.getStatus().isEmpty()) {
itemDataStatusText = isSelfTalk ? context.getString(R.string.saved_messages_explain) : dcContact.getStatus();
itemData.add(new ItemData(ITEM_SIGNATURE, itemDataStatusText, 0));
} else {
itemData.add(new ItemData(ITEM_DIVIDER, null, 0));
@@ -140,10 +140,10 @@ public class ProfileFragment extends Fragment
}
@Override
public void onStatusLongClicked(boolean isMultiUser) {
public void onStatusLongClicked() {
Context context = requireContext();
new AlertDialog.Builder(context)
.setTitle(isMultiUser? R.string.chat_description : R.string.pref_default_status_label)
.setTitle(R.string.pref_default_status_label)
.setItems(new CharSequence[]{
context.getString(R.string.menu_copy_to_clipboard)
},
@@ -310,26 +310,21 @@ public class ProfileFragment extends Fragment
super.onActivityResult(requestCode, resultCode, data);
if (requestCode==REQUEST_CODE_PICK_CONTACT && resultCode==Activity.RESULT_OK && data!=null) {
List<Integer> selected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.CONTACTS_EXTRA);
List<Integer> deselected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.DESELECTED_CONTACTS_EXTRA);
List<Integer> removed = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.REMOVED_CONTACTS_EXTRA);
if(selected == null) return;
Util.runOnAnyBackgroundThread(() -> {
if (deselected != null) {
// Remove members that were deselected
int[] members = dcContext.getChatContacts(chatId);
for (int contactId : deselected) {
for (int memberId : members) {
if (memberId == contactId) {
dcContext.removeContactFromChat(chatId, memberId);
break;
}
}
// Add new members
for (Integer contactId : selected) {
if (contactId != null) {
dcContext.addContactToChat(chatId, contactId);
}
}
if (selected != null) {
// Add new members
for (Integer contactId : selected) {
// Remove members that were explicitly unchecked
if (removed != null) {
for (Integer contactId : removed) {
if (contactId != null) {
dcContext.addContactToChat(chatId, contactId);
dcContext.removeContactFromChat(chatId, contactId);
}
}
}
@@ -104,25 +104,26 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity implement
resolvedExtras = new ArrayList<>();
List<Uri> streamExtras = new ArrayList<>();
if (MailtoUtil.isMailto(getIntent().getData())) {
String[] extraEmail = getIntent().getStringArrayExtra(Intent.EXTRA_EMAIL);
if (extraEmail == null || extraEmail.length == 0) {
getIntent().putExtra(Intent.EXTRA_EMAIL, MailtoUtil.getRecipients(getIntent().getData()));
}
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
if (text == null || text.isEmpty()) {
getIntent().putExtra(Intent.EXTRA_TEXT, MailtoUtil.getText(getIntent().getData()));
}
}
if (Intent.ACTION_SEND.equals(getIntent().getAction()) &&
getIntent().getParcelableExtra(Intent.EXTRA_STREAM) != null) {
Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
streamExtras.add(uri);
} else if (getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM) != null) {
streamExtras = getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM);
} else if (getIntent().getData() != null) {
streamExtras.add(getIntent().getData());
} else {
Uri uri = getIntent().getData();
if (MailtoUtil.isMailto(uri)) {
String[] extraEmail = getIntent().getStringArrayExtra(Intent.EXTRA_EMAIL);
if (extraEmail == null || extraEmail.length == 0) {
getIntent().putExtra(Intent.EXTRA_EMAIL, MailtoUtil.getRecipients(uri));
}
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
if (text == null || text.isEmpty()) {
getIntent().putExtra(Intent.EXTRA_TEXT, MailtoUtil.getText(uri));
}
} else if (uri != null) {
streamExtras.add(uri);
}
}
if (needsFilePermission(streamExtras)) {
@@ -264,7 +265,6 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity implement
}
chatId = dcContext.createChatByContactId(contactId);
accId = dcContext.getAccountId();
}
Intent composeIntent;
if (accId != -1 && chatId != -1) {
@@ -77,7 +77,7 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
findViewById(R.id.status_bar_background).setBackgroundResource(R.drawable.search_toolbar_shadow);
} else {
// add padding to avoid content hidden behind system bars
ViewUtil.applyWindowInsets(findViewById(R.id.content_container), true, true, true, true, true, false);
ViewUtil.applyWindowInsets(findViewById(R.id.content_container));
}
webView.setWebViewClient(new WebViewClient() {
@@ -47,24 +47,21 @@ import chat.delta.rpc.RpcException;
public class AccountSelectionListFragment extends DialogFragment implements DcEventCenter.DcEventDelegate
{
private static final String TAG = AccountSelectionListFragment.class.getSimpleName();
private static final String ARG_SELECT_ONLY = "select_only";
private final ConversationListActivity activity;
private RecyclerView recyclerView;
private AccountSelectionListAdapter adapter;
private boolean selectOnly;
private final boolean selectOnly;
public static AccountSelectionListFragment newInstance(boolean selectOnly) {
AccountSelectionListFragment fragment = new AccountSelectionListFragment();
Bundle args = new Bundle();
args.putBoolean(ARG_SELECT_ONLY, selectOnly);
fragment.setArguments(args);
return fragment;
public AccountSelectionListFragment(ConversationListActivity activity, boolean selectOnly) {
super();
this.activity = activity;
this.selectOnly = selectOnly;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
selectOnly = getArguments() != null && getArguments().getBoolean(ARG_SELECT_ONLY, false);
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity())
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setTitle(R.string.switch_account)
.setNegativeButton(R.string.cancel, null);
if (!selectOnly) {
@@ -182,7 +179,6 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv
}
private void onSetTag(int accountId) {
ConversationListActivity activity = (ConversationListActivity)requireActivity();
AccountSelectionListFragment.this.dismiss();
DcContext dcContext = DcHelper.getAccounts(activity).getAccount(accountId);
@@ -206,7 +202,6 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv
private void onDeleteProfile(int accountId) {
AccountSelectionListFragment.this.dismiss();
ConversationListActivity activity = (ConversationListActivity)requireActivity();
DcAccounts accounts = DcHelper.getAccounts(activity);
Rpc rpc = DcHelper.getRpc(activity);
@@ -259,7 +254,6 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv
@Override
public void onItemClick(AccountSelectionListItem contact) {
AccountSelectionListFragment.this.dismiss();
ConversationListActivity activity = (ConversationListActivity)requireActivity();
int accountId = contact.getAccountId();
if (accountId == DC_CONTACT_ID_ADD_ACCOUNT) {
AccountManager.getInstance().switchAccountAndStartActivity(activity, 0);
@@ -0,0 +1,356 @@
package org.thoughtcrime.securesms.audio;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.util.Pair;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.guava.Optional;
import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
import java.io.IOException;
import java.lang.ref.WeakReference;
public class AudioSlidePlayer {
private static final String TAG = AudioSlidePlayer.class.getSimpleName();
private static @NonNull Optional<AudioSlidePlayer> playing = Optional.absent();
private final @NonNull Context context;
private final @NonNull AudioSlide slide;
private final @NonNull Handler progressEventHandler;
private @NonNull WeakReference<Listener> listener;
private @Nullable SimpleExoPlayer mediaPlayer;
private @Nullable SimpleExoPlayer durationCalculator;
public synchronized static AudioSlidePlayer createFor(@NonNull Context context,
@NonNull AudioSlide slide,
@NonNull Listener listener)
{
if (playing.isPresent() && playing.get().getAudioSlide().equals(slide)) {
playing.get().setListener(listener);
return playing.get();
} else {
return new AudioSlidePlayer(context, slide, listener);
}
}
private AudioSlidePlayer(@NonNull Context context,
@NonNull AudioSlide slide,
@NonNull Listener listener)
{
this.context = context;
this.slide = slide;
this.listener = new WeakReference<>(listener);
this.progressEventHandler = new ProgressEventHandler(this);
}
public void requestDuration() {
try {
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).build();
durationCalculator = new SimpleExoPlayer.Builder(context, new DefaultRenderersFactory(context))
.setTrackSelector(new DefaultTrackSelector(context))
.setLoadControl(loadControl)
.build();
durationCalculator.setPlayWhenReady(false);
durationCalculator.addListener(new Player.Listener() {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == Player.STATE_READY) {
Util.runOnMain(() -> {
synchronized (AudioSlidePlayer.this) {
if (durationCalculator == null) return;
Log.d(TAG, "request duration " + durationCalculator.getDuration());
getListener().onReceivedDuration(Long.valueOf(durationCalculator.getDuration()).intValue());
durationCalculator.release();
durationCalculator.removeListener(this);
durationCalculator = null;
}
});
}
}
});
durationCalculator.prepare(createMediaSource(slide.getUri()));
} catch (Exception e) {
Log.w(TAG, e);
getListener().onReceivedDuration(0);
}
}
public void play(final double progress) throws IOException {
play(progress, false);
}
private void play(final double progress, boolean earpiece) throws IOException {
if (this.mediaPlayer != null) {
return;
}
if (slide.getUri() == null) {
throw new IOException("Slide has no URI!");
}
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).build();
this.mediaPlayer = new SimpleExoPlayer.Builder(context, new DefaultRenderersFactory(context))
.setTrackSelector(new DefaultTrackSelector(context))
.setLoadControl(loadControl)
.build();
mediaPlayer.prepare(createMediaSource(slide.getUri()));
mediaPlayer.setPlayWhenReady(true);
mediaPlayer.setAudioAttributes(new AudioAttributes.Builder()
.setContentType(earpiece ? C.AUDIO_CONTENT_TYPE_SPEECH : C.AUDIO_CONTENT_TYPE_MUSIC)
.setUsage(earpiece ? C.USAGE_VOICE_COMMUNICATION : C.USAGE_MEDIA)
.build(), false);
mediaPlayer.addListener(new Player.Listener() {
boolean started = false;
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
Log.d(TAG, "onPlayerStateChanged(" + playWhenReady + ", " + playbackState + ")");
switch (playbackState) {
case Player.STATE_READY:
Log.i(TAG, "onPrepared() " + mediaPlayer.getBufferedPercentage() + "% buffered");
synchronized (AudioSlidePlayer.this) {
if (mediaPlayer == null) return;
Log.d(TAG, "DURATION: " + mediaPlayer.getDuration());
if (started) {
Log.d(TAG, "Already started. Ignoring.");
return;
}
started = true;
if (progress > 0) {
mediaPlayer.seekTo((long) (mediaPlayer.getDuration() * progress));
}
setPlaying(AudioSlidePlayer.this);
}
keepScreenOn(true);
notifyOnStart();
progressEventHandler.sendEmptyMessage(0);
break;
case Player.STATE_ENDED:
Log.i(TAG, "onComplete");
synchronized (AudioSlidePlayer.this) {
getListener().onReceivedDuration(Long.valueOf(mediaPlayer.getDuration()).intValue());
mediaPlayer.release();
mediaPlayer = null;
}
keepScreenOn(false);
notifyOnStop();
progressEventHandler.removeMessages(0);
}
}
@Override
public void onPlayerError(PlaybackException error) {
Log.w(TAG, "MediaPlayer Error: " + error);
synchronized (AudioSlidePlayer.this) {
mediaPlayer.release();
mediaPlayer = null;
}
notifyOnStop();
progressEventHandler.removeMessages(0);
// Failed to play media file, maybe another app can handle it
int msgId = getAudioSlide().getDcMsgId();
DcHelper.openForViewOrShare(context, msgId, Intent.ACTION_VIEW);
}
});
}
private MediaSource createMediaSource(@NonNull Uri uri) {
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(defaultDataSourceFactory);
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
return new ProgressiveMediaSource.Factory(attachmentDataSourceFactory, extractorsFactory)
.createMediaSource(MediaItem.fromUri(uri));
}
public synchronized void stop() {
Log.i(TAG, "Stop called!");
keepScreenOn(false);
removePlaying(this);
if (this.mediaPlayer != null) {
this.mediaPlayer.stop();
this.mediaPlayer.release();
}
this.mediaPlayer = null;
}
public static void stopAll() {
if (playing.isPresent()) {
synchronized (AudioSlidePlayer.class) {
if (playing.isPresent()) {
playing.get().stop();
}
}
}
}
public void setListener(@NonNull Listener listener) {
this.listener = new WeakReference<>(listener);
if (this.mediaPlayer != null && this.mediaPlayer.getPlaybackState() == Player.STATE_READY) {
notifyOnStart();
}
}
public @NonNull AudioSlide getAudioSlide() {
return slide;
}
private Pair<Double, Integer> getProgress() {
if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) {
return new Pair<>(0D, 0);
} else {
return new Pair<>((double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration(),
(int) mediaPlayer.getCurrentPosition());
}
}
private void notifyOnStart() {
Util.runOnMain(new Runnable() {
@Override
public void run() {
getListener().onStart();
}
});
}
private void notifyOnStop() {
Util.runOnMain(new Runnable() {
@Override
public void run() {
getListener().onStop();
}
});
}
private void notifyOnProgress(final double progress, final long millis) {
Util.runOnMain(new Runnable() {
@Override
public void run() {
getListener().onProgress(slide, progress, millis);
}
});
}
private @NonNull Listener getListener() {
Listener listener = this.listener.get();
if (listener != null) return listener;
else return new Listener() {
@Override
public void onStart() {}
@Override
public void onStop() {}
@Override
public void onProgress(AudioSlide slide, double progress, long millis) {}
@Override
public void onReceivedDuration(int millis) {}
};
}
public void keepScreenOn(boolean keepOn) {
if (context instanceof Activity) {
if (keepOn) {
((Activity) context).getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} else {
((Activity) context).getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}
}
private synchronized static void setPlaying(@NonNull AudioSlidePlayer player) {
if (playing.isPresent() && playing.get() != player) {
playing.get().notifyOnStop();
playing.get().stop();
}
playing = Optional.of(player);
}
private synchronized static void removePlaying(@NonNull AudioSlidePlayer player) {
if (playing.isPresent() && playing.get() == player) {
playing = Optional.absent();
}
}
public interface Listener {
void onStart();
void onStop();
void onProgress(AudioSlide slide, double progress, long millis);
void onReceivedDuration(int millis);
}
private static class ProgressEventHandler extends Handler {
private final WeakReference<AudioSlidePlayer> playerReference;
private ProgressEventHandler(@NonNull AudioSlidePlayer player) {
this.playerReference = new WeakReference<>(player);
}
@Override
public void handleMessage(@NonNull Message msg) {
AudioSlidePlayer player = playerReference.get();
if (player == null || player.mediaPlayer == null || !isPlayerActive(player.mediaPlayer)) {
return;
}
Pair<Double, Integer> progress = player.getProgress();
player.notifyOnProgress(progress.first, progress.second);
sendEmptyMessageDelayed(0, 50);
}
private boolean isPlayerActive(@NonNull SimpleExoPlayer player) {
return player.getPlaybackState() == Player.STATE_READY || player.getPlaybackState() == Player.STATE_BUFFERING;
}
}
}
@@ -43,14 +43,12 @@ public class CallActivity extends WebViewActivity implements DcEventCenter.DcEve
public static final String EXTRA_CHAT_ID = "chat_id";
public static final String EXTRA_CALL_ID = "call_id";
public static final String EXTRA_HASH = "hash";
public static final String EXTRA_HAS_VIDEO = "has_video";
private DcContext dcContext;
private Rpc rpc;
private int accId;
private int chatId;
private int callId;
private boolean hasVideo;
private boolean ended = false;
@SuppressLint("SetJavaScriptEnabled")
@@ -60,9 +58,7 @@ public class CallActivity extends WebViewActivity implements DcEventCenter.DcEve
Bundle bundle = getIntent().getExtras();
assert bundle != null;
hasVideo = bundle.getBoolean(EXTRA_HAS_VIDEO, true);
String hash = bundle.getString(EXTRA_HASH, "");
String query = hasVideo? "" : "?noOutgoingVideoInitially";
accId = bundle.getInt(EXTRA_ACCOUNT_ID, -1);
chatId = bundle.getInt(EXTRA_CHAT_ID, 0);
callId = bundle.getInt(EXTRA_CALL_ID, 0);
@@ -98,7 +94,7 @@ public class CallActivity extends WebViewActivity implements DcEventCenter.DcEve
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_camera_denied))
.onAllGranted(() -> {
String url = "file:///android_asset/calls/index.html";
webView.loadUrl(url + query + hash);
webView.loadUrl(url + hash);
}).onAnyDenied(this::finish)
.execute();
}
@@ -172,7 +168,7 @@ public class CallActivity extends WebViewActivity implements DcEventCenter.DcEve
@JavascriptInterface
public void startCall(String payload) {
try {
callId = rpc.placeOutgoingCall(accId, chatId, payload, hasVideo);
callId = rpc.placeOutgoingCall(accId, chatId, payload);
} catch (RpcException e) {
Log.e(TAG, "Error", e);
}
@@ -18,29 +18,28 @@ import java.nio.charset.StandardCharsets;
public class CallUtil {
private static final String TAG = CallUtil.class.getSimpleName();
public static void startCall(Activity activity, int chatId, boolean hasVideo) {
public static void startCall(Activity activity, int chatId) {
Permissions.with(activity)
.request(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
.ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_camera_denied))
.onAllGranted(() -> {
int accId = DcHelper.getContext(activity).getAccountId();
startCall(activity, accId, chatId, hasVideo);
startCall(activity, accId, chatId);
})
.execute();
}
public static void startCall(Context context, int accId, int chatId, boolean hasVideo) {
public static void startCall(Context context, int accId, int chatId) {
Intent intent = new Intent(context, CallActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(CallActivity.EXTRA_ACCOUNT_ID, accId);
intent.putExtra(CallActivity.EXTRA_CHAT_ID, chatId);
intent.putExtra(CallActivity.EXTRA_HAS_VIDEO, hasVideo);
intent.putExtra(CallActivity.EXTRA_HASH, "#startCall");
context.startActivity(intent);
}
public static void openCall(Context context, int accId, int chatId, int callId, String payload, boolean hasVideo) {
public static void openCall(Context context, int accId, int chatId, int callId, String payload) {
String base64 = Base64.encodeToString(payload.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP);
String hash = "";
try {
@@ -54,7 +53,6 @@ public class CallUtil {
intent.putExtra(CallActivity.EXTRA_ACCOUNT_ID, accId);
intent.putExtra(CallActivity.EXTRA_CHAT_ID, chatId);
intent.putExtra(CallActivity.EXTRA_CALL_ID, callId);
intent.putExtra(CallActivity.EXTRA_HAS_VIDEO, hasVideo);
intent.putExtra(CallActivity.EXTRA_HASH, hash);
context.startActivity(intent);
}
@@ -0,0 +1,301 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.util.DateUtils;
import java.io.IOException;
public class AudioView extends FrameLayout implements AudioSlidePlayer.Listener {
private static final String TAG = AudioView.class.getSimpleName();
private final @NonNull AnimatingToggle controlToggle;
private final @NonNull ImageView playButton;
private final @NonNull ImageView pauseButton;
private final @NonNull SeekBar seekBar;
private final @NonNull TextView timestamp;
private final @NonNull TextView title;
private final @NonNull View mask;
private @Nullable AudioSlidePlayer audioSlidePlayer;
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
private int backwardsCounter;
public AudioView(Context context) {
this(context, null);
}
public AudioView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AudioView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.audio_view, this);
this.controlToggle = (AnimatingToggle) findViewById(R.id.control_toggle);
this.playButton = (ImageView) findViewById(R.id.play);
this.pauseButton = (ImageView) findViewById(R.id.pause);
this.seekBar = (SeekBar) findViewById(R.id.seek);
this.timestamp = (TextView) findViewById(R.id.timestamp);
this.title = (TextView) findViewById(R.id.title);
this.mask = findViewById(R.id.interception_mask);
this.timestamp.setText("00:00");
this.playButton.setOnClickListener(new PlayClickedListener());
this.pauseButton.setOnClickListener(new PauseClickedListener());
this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());
this.playButton.setImageDrawable(context.getDrawable(R.drawable.play_icon));
this.pauseButton.setImageDrawable(context.getDrawable(R.drawable.pause_icon));
this.playButton.setBackground(context.getDrawable(R.drawable.ic_circle_fill_white_48dp));
this.pauseButton.setBackground(context.getDrawable(R.drawable.ic_circle_fill_white_48dp));
setTint(getContext().getResources().getColor(R.color.audio_icon));
}
public void setAudio(final @NonNull AudioSlide audio, int duration)
{
controlToggle.displayQuick(playButton);
seekBar.setEnabled(true);
seekBar.setProgress(0);
audioSlidePlayer = AudioSlidePlayer.createFor(getContext(), audio, this);
timestamp.setText(DateUtils.getFormatedDuration(duration));
if(audio.asAttachment().isVoiceNote() || !audio.getFileName().isPresent()) {
title.setVisibility(View.GONE);
}
else {
title.setText(audio.getFileName().get());
title.setVisibility(View.VISIBLE);
}
}
@Override
public void setOnClickListener(OnClickListener listener) {
super.setOnClickListener(listener);
this.mask.setOnClickListener(listener);
}
@Override
public void setOnLongClickListener(OnLongClickListener listener) {
super.setOnLongClickListener(listener);
this.mask.setOnLongClickListener(listener);
this.playButton.setOnLongClickListener(listener);
this.pauseButton.setOnLongClickListener(listener);
}
public void togglePlay() {
if (this.playButton.getVisibility() == View.VISIBLE) {
playButton.performClick();
} else {
pauseButton.performClick();
}
}
public String getDescription() {
String desc;
if (this.title.getVisibility() == View.VISIBLE) {
desc = getContext().getString(R.string.audio);
} else {
desc = getContext().getString(R.string.voice_message);
}
desc += "\n" + this.timestamp.getText();
if (title.getVisibility() == View.VISIBLE) {
desc += "\n" + this.title.getText();
}
return desc;
}
public void setDuration(int duration) {
if (getProgress()==0)
this.timestamp.setText(DateUtils.getFormatedDuration(duration));
}
public void cleanup() {
if (this.audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
this.audioSlidePlayer.stop();
}
}
@Override
public void onReceivedDuration(int millis) {
this.timestamp.setText(DateUtils.getFormatedDuration(millis));
}
@Override
public void onStart() {
if (this.pauseButton.getVisibility() != View.VISIBLE) {
togglePlayToPause();
}
}
@Override
public void onStop() {
if (this.playButton.getVisibility() != View.VISIBLE) {
togglePauseToPlay();
}
if (seekBar.getProgress() + 5 >= seekBar.getMax()) {
backwardsCounter = 4;
onProgress(audioSlidePlayer.getAudioSlide(), 0.0, -1);
}
}
public void disablePlayer(boolean disable) {
this.mask.setVisibility(disable? View.VISIBLE : View.GONE);
}
@Override
public void onProgress(AudioSlide slide, double progress, long millis) {
if (!audioSlidePlayer.getAudioSlide().equals(slide)) {
return;
}
int seekProgress = (int) Math.floor(progress * this.seekBar.getMax());
if (seekProgress > seekBar.getProgress() || backwardsCounter > 3) {
backwardsCounter = 0;
this.seekBar.setProgress(seekProgress);
if (millis != -1) {
this.timestamp.setText(DateUtils.getFormatedDuration(millis));
}
} else {
backwardsCounter++;
}
}
public void setTint(int foregroundTint) {
this.playButton.setBackgroundTintList(ColorStateList.valueOf(foregroundTint));
this.pauseButton.setBackgroundTintList(ColorStateList.valueOf(foregroundTint));
this.seekBar.getProgressDrawable().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
this.seekBar.getThumb().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
}
public void getSeekBarGlobalVisibleRect(@NonNull Rect rect) {
seekBar.getGlobalVisibleRect(rect);
}
private double getProgress() {
if (this.seekBar.getProgress() <= 0 || this.seekBar.getMax() <= 0) {
return 0;
} else {
return (double)this.seekBar.getProgress() / (double)this.seekBar.getMax();
}
}
private void togglePlayToPause() {
controlToggle.displayQuick(pauseButton);
AnimatedVectorDrawable playToPauseDrawable = (AnimatedVectorDrawable) getContext().getDrawable(R.drawable.play_to_pause_animation);
pauseButton.setImageDrawable(playToPauseDrawable);
playToPauseDrawable.start();
}
private void togglePauseToPlay() {
controlToggle.displayQuick(playButton);
AnimatedVectorDrawable pauseToPlayDrawable = (AnimatedVectorDrawable) getContext().getDrawable(R.drawable.pause_to_play_animation);
playButton.setImageDrawable(pauseToPlayDrawable);
pauseToPlayDrawable.start();
}
private class PlayClickedListener implements View.OnClickListener {
@Override
public void onClick(View v) {
try {
Log.w(TAG, "playbutton onClick");
if (audioSlidePlayer != null) {
if (Build.VERSION.SDK_INT >= 26) {
if (audioFocusChangeListener == null) {
audioFocusChangeListener = focusChange -> {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
pauseButton.performClick();
}
};
}
AudioAttributes playbackAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build();
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(playbackAttributes)
.setAcceptsDelayedFocusGain(false)
.setWillPauseWhenDucked(false)
.setOnAudioFocusChangeListener(audioFocusChangeListener)
.build();
AudioManager audioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
audioManager.requestAudioFocus(focusRequest);
}
togglePlayToPause();
audioSlidePlayer.play(getProgress());
}
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
private class PauseClickedListener implements View.OnClickListener {
@Override
public void onClick(View v) {
Log.w(TAG, "pausebutton onClick");
if (audioSlidePlayer != null) {
togglePauseToPlay();
audioSlidePlayer.stop();
}
}
}
private class SeekBarModifiedListener implements SeekBar.OnSeekBarChangeListener {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}
@Override
public synchronized void onStartTrackingTouch(SeekBar seekBar) {
if (audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
audioSlidePlayer.stop();
}
}
@Override
public synchronized void onStopTrackingTouch(SeekBar seekBar) {
try {
if (audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
audioSlidePlayer.play(getProgress());
}
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
}
@@ -67,14 +67,10 @@ public class CallItemView extends FrameLayout {
title.setText(R.string.canceled_call);
} else if (callInfo.state instanceof CallState.Declined) {
title.setText(R.string.declined_call);
} else if (callInfo.hasVideo) {
title.setText(isOutgoing? R.string.outgoing_video_call : R.string.incoming_video_call);
} else {
title.setText(isOutgoing? R.string.outgoing_audio_call : R.string.incoming_audio_call);
title.setText(isOutgoing? R.string.outgoing_call : R.string.incoming_call);
}
icon.setImageResource(callInfo.hasVideo? R.drawable.ic_videocam_white_24dp : R.drawable.baseline_call_24);
int[] attrs;
if (isOutgoing) {
attrs = new int[]{
@@ -151,6 +151,12 @@ public class ConversationItemFooter extends LinearLayout {
else if (messageRecord.isDelivered()) deliveryStatusView.setSent();
else if (messageRecord.isPreparing()) deliveryStatusView.setPreparing();
else deliveryStatusView.setPending();
if (messageRecord.isFailed()) {
deliveryStatusView.setTint(Color.RED);
} else {
deliveryStatusView.setTint(textColor); // Reset the color to the standard color (because the footer is re-used in a RecyclerView)
}
}
public String getDescription() {
@@ -11,9 +11,6 @@ import android.widget.ImageView;
import org.thoughtcrime.securesms.R;
import java.util.ArrayList;
import java.util.List;
public class RemovableEditableMediaView extends FrameLayout {
private final @NonNull ImageView remove;
@@ -22,7 +19,6 @@ public class RemovableEditableMediaView extends FrameLayout {
private final int removeSize;
private @Nullable View current;
private final List<OnClickListener> removeClickListeners = new ArrayList<>();
public RemovableEditableMediaView(Context context) {
this(context, null);
@@ -76,22 +72,8 @@ public class RemovableEditableMediaView extends FrameLayout {
return current;
}
public void addRemoveClickListener(View.OnClickListener listener) {
removeClickListeners.add(listener);
updateRemoveClickListener();
}
public void removeRemoveClickListener(View.OnClickListener listener) {
removeClickListeners.remove(listener);
updateRemoveClickListener();
}
private void updateRemoveClickListener() {
this.remove.setOnClickListener(v -> {
for (OnClickListener listener : removeClickListeners) {
listener.onClick(v);
}
});
public void setRemoveClickListener(View.OnClickListener listener) {
this.remove.setOnClickListener(listener);
}
public void setEditClickListener(View.OnClickListener listener) {
@@ -1,58 +0,0 @@
package org.thoughtcrime.securesms.components.audioplay;
import android.net.Uri;
import androidx.annotation.Nullable;
public class AudioPlaybackState {
private final int msgId;
private final @Nullable Uri audioUri;
private final PlaybackStatus status;
private final long currentPosition;
private final long duration;
public enum PlaybackStatus {
IDLE,
LOADING,
PLAYING,
PAUSED,
ERROR
}
public AudioPlaybackState(int msgId,
@Nullable Uri audioUri,
PlaybackStatus status,
long currentPosition,
long duration) {
this.msgId = msgId;
this.audioUri = audioUri;
this.status = status;
this.currentPosition = currentPosition;
this.duration = duration;
}
public static AudioPlaybackState idle() {
return new AudioPlaybackState(0, null, PlaybackStatus.IDLE, 0, 0);
}
public int getMsgId() {
return msgId;
}
@Nullable
public Uri getAudioUri() {
return audioUri;
}
public PlaybackStatus getStatus() {
return status;
}
public long getCurrentPosition() {
return currentPosition;
}
public long getDuration() {
return duration;
}
}
@@ -1,318 +0,0 @@
package org.thoughtcrime.securesms.components.audioplay;
import android.content.Context;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.session.MediaController;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AudioPlaybackViewModel extends ViewModel {
private static final String TAG = AudioPlaybackViewModel.class.getSimpleName();
private static final int NON_MESSAGE_AUDIO_MSG_ID = 0; // Audios not attached to a message doesn't have message id.
private final MutableLiveData<AudioPlaybackState> playbackState;
private final MutableLiveData<Map<Integer, Long>> durations = new MutableLiveData<>(new HashMap<>());
private final Set<Integer> extractionInProgress = new HashSet<>();
private final ExecutorService extractionExecutor = Executors.newFixedThreadPool(2);
private @Nullable MediaController mediaController;
private final Handler handler;
private boolean isUserSeeking = false;
public AudioPlaybackViewModel() {
playbackState = new MutableLiveData<>(AudioPlaybackState.idle());
handler = new Handler(Looper.getMainLooper());
}
public LiveData<AudioPlaybackState> getPlaybackState() {
return playbackState;
}
public void setMediaController(@Nullable MediaController controller) {
this.mediaController = controller;
if (mediaController != null && mediaController.isPlaying()) {
startUpdateProgress();
}
updateCurrentState(true);
setupPlayerListener();
}
// Public methods
public void loadAudioAndPlay(int msgId, Uri audioUri) {
if (mediaController == null) return;
// Set media item if we have a different audio.
if (isDifferentAudio(msgId, audioUri)) {
updateState(msgId, audioUri, AudioPlaybackState.PlaybackStatus.LOADING, 0, 0);
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(String.valueOf(msgId))
.setUri(audioUri)
.build();
mediaController.setMediaItem(mediaItem);
mediaController.prepare();
}
play(msgId, audioUri);
}
private boolean isSameAudio(int msgId, Uri audioUri) {
return !isDifferentAudio(msgId, audioUri);
}
private boolean isDifferentAudio(int msgId, Uri audioUri) {
AudioPlaybackState currentState = playbackState.getValue();
return currentState != null && (
msgId != currentState.getMsgId() ||
currentState.getAudioUri() == null ||
currentState.getAudioUri() != null && !currentState.getAudioUri().equals(audioUri));
}
public LiveData<Map<Integer, Long>> getDurations() {
return durations;
}
public void ensureDurationLoaded(Context context, int msgId, Uri audioUri) {
// Check cache
Map<Integer, Long> currentDurations = durations.getValue();
if (currentDurations != null && currentDurations.containsKey(msgId)) {
return;
}
// Check extracting
synchronized (extractionInProgress) {
if (extractionInProgress.contains(msgId)) {
return;
}
extractionInProgress.add(msgId);
}
// Extract in background
extractionExecutor.execute(() -> {
long duration = extractDurationFromAudio(context, audioUri);
handler.post(() -> {
Map<Integer, Long> updatedDurations = new HashMap<>(durations.getValue());
updatedDurations.put(msgId, duration);
durations.setValue(updatedDurations);
});
synchronized (extractionInProgress) {
extractionInProgress.remove(msgId);
}
});
}
private long extractDurationFromAudio(Context context, Uri audioUri) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
retriever.setDataSource(context, audioUri);
String durationStr = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_DURATION
);
return durationStr != null ? Long.parseLong(durationStr) : 0;
} catch (Exception e) {
return 0;
} finally {
try {
retriever.release();
} catch (Exception ignored) {}
}
}
public void pause(int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
mediaController.pause();
}
}
public void play(int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
mediaController.play();
}
}
public void seekTo(long position, int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
mediaController.seekTo(position);
}
}
public void stop(int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
mediaController.stop();
stopUpdateProgress();
playbackState.setValue(AudioPlaybackState.idle());
}
}
public void stopNonMessageAudioPlayback() {
stopByIds(NON_MESSAGE_AUDIO_MSG_ID);
}
// A special method for deleting message, where we only use message Ids
public void stopByIds(int... msgIds) {
AudioPlaybackState currentState = playbackState.getValue();
if (mediaController != null && currentState != null) {
for (int msgId : msgIds) {
if (msgId == currentState.getMsgId()) {
mediaController.stop();
stopUpdateProgress();
playbackState.setValue(AudioPlaybackState.idle());
}
}
}
}
public void setUserSeeking(boolean isUserSeeking) {
this.isUserSeeking = isUserSeeking;
}
// Private methods
private void setupPlayerListener() {
if (mediaController == null) return;
mediaController.addListener(new Player.Listener() {
@Override
public void onEvents(Player player, Player.Events events) {
if (events.containsAny(Player.EVENT_IS_PLAYING_CHANGED)) {
if (player.isPlaying()) {
startUpdateProgress();
} else {
stopUpdateProgress();
}
updateCurrentState(false);
}
if (events.containsAny(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
if (player.getPlaybackState() == Player.STATE_READY) {
updateCurrentState(false);
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
// This is to prevent automatically playing after the audio
// has been play to the end once, then user dragged the seek bar again
mediaController.setPlayWhenReady(false);
}
}
if (events.containsAny(Player.EVENT_PLAYER_ERROR)) {
updateCurrentAudioState(AudioPlaybackState.PlaybackStatus.ERROR, 0, 0);
}
}
});
}
private void updateCurrentState(boolean queryPlaying) {
if (mediaController == null) return;
AudioPlaybackState.PlaybackStatus status;
if (mediaController.isPlaying()) {
status = AudioPlaybackState.PlaybackStatus.PLAYING;
} else if (mediaController.getPlaybackState() == Player.STATE_READY
|| mediaController.getPlaybackState() == Player.STATE_ENDED) {
status = AudioPlaybackState.PlaybackStatus.PAUSED;
} else {
status = AudioPlaybackState.PlaybackStatus.IDLE;
}
Uri currentUri = null;
int currentMsgId = 0;
if (playbackState.getValue() != null) {
currentMsgId = playbackState.getValue().getMsgId();
currentUri = playbackState.getValue().getAudioUri();
}
if (queryPlaying || playbackState.getValue() == null) {
MediaItem item = mediaController.getCurrentMediaItem();
if (item != null) {
try {
currentMsgId = Integer.parseInt(item.mediaId);
} catch (NumberFormatException e) {
Log.w(TAG, "Invalid integer", e);
}
if (item.localConfiguration != null) {
currentUri = item.localConfiguration.uri;
}
}
}
updateState(
currentMsgId,
currentUri,
status,
mediaController.getCurrentPosition(),
mediaController.getDuration());
}
private void updateState(int msgId,
Uri audioUri,
AudioPlaybackState.PlaybackStatus status,
long position,
long duration) {
// Sanitize longs
if (position < 0 || position > Integer.MAX_VALUE) {
position = 0;
}
if (duration < 0 || duration > Integer.MAX_VALUE) {
duration = 0;
}
playbackState.setValue(new AudioPlaybackState(
msgId, audioUri, status, position, duration
));
}
private void updateCurrentAudioState(AudioPlaybackState.PlaybackStatus status,
long position,
long duration) {
AudioPlaybackState current = playbackState.getValue();
if (current != null) {
updateState(current.getMsgId(), current.getAudioUri(), status, position, duration);
}
}
// Progress tracking
private final Runnable progressRunnable = new Runnable() {
@Override
public void run() {
if (mediaController != null && mediaController.isPlaying() && !isUserSeeking) {
updateCurrentAudioState(AudioPlaybackState.PlaybackStatus.PLAYING,
mediaController.getCurrentPosition(),
mediaController.getDuration());
handler.postDelayed(this, 100);
}
}
};
private void startUpdateProgress() {
stopUpdateProgress();
handler.post(progressRunnable);
}
private void stopUpdateProgress() {
handler.removeCallbacks(progressRunnable);
}
@Override
protected void onCleared() {
stopUpdateProgress();
extractionExecutor.shutdown();
super.onCleared();
}
}
@@ -1,367 +0,0 @@
package org.thoughtcrime.securesms.components.audioplay;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.lifecycle.Observer;
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.util.DateUtils;
import java.util.Map;
public class AudioView extends FrameLayout {
private static final String TAG = AudioView.class.getSimpleName();
private final @NonNull ImageView playPauseButton;
private final AnimatedVectorDrawableCompat playToPauseDrawable;
private final AnimatedVectorDrawableCompat pauseToPlayDrawable;
private final Drawable playDrawable;
private final Drawable pauseDrawable;
private final Animatable2Compat.AnimationCallback animationCallback;
private final @NonNull SeekBar seekBar;
private final @NonNull TextView timestamp;
private final @NonNull TextView title;
private final @NonNull View mask;
private OnActionListener listener;
private int msgId = -1;
private Uri audioUri;
private int progress;
private int duration;
private AudioPlaybackViewModel viewModel;
private final Observer<AudioPlaybackState> stateObserver = this::onPlaybackStateChanged;
private final Observer<Map<Integer, Long>> durationObserver = this::onDurationsChanged;
private boolean isPlaying;
public AudioView(Context context) {
this(context, null);
}
public AudioView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AudioView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.audio_view, this);
this.playPauseButton = findViewById(R.id.play_pause);
this.seekBar = findViewById(R.id.seek);
this.timestamp = findViewById(R.id.timestamp);
this.title = findViewById(R.id.title);
this.mask = findViewById(R.id.interception_mask);
updateTimestampsAndSeekBar();
// Load drawables once
this.playToPauseDrawable = AnimatedVectorDrawableCompat.create(
getContext(), R.drawable.play_to_pause_animation);
this.pauseToPlayDrawable = AnimatedVectorDrawableCompat.create(
getContext(), R.drawable.pause_to_play_animation);
this.playDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.play_icon);
this.pauseDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.pause_icon);
this.animationCallback = new Animatable2Compat.AnimationCallback() {
@Override
public void onAnimationEnd(Drawable drawable) {
Drawable endState = isPlaying ? pauseDrawable : playDrawable;
playPauseButton.setImageDrawable(endState);
}
};
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
setupControls();
}
private void setupControls() {
// Set up observer in a very specific case when the view is detached and then re-attached,
// but binding from adapter has not happened yet
if (viewModel != null) {
viewModel.getPlaybackState().removeObserver(stateObserver);
viewModel.getPlaybackState().observeForever(stateObserver);
viewModel.getDurations().removeObserver(durationObserver);
viewModel.getDurations().observeForever(durationObserver);
}
playPauseButton.setOnClickListener(v -> {
Log.w(TAG, "playPauseButton onClick");
if (viewModel == null || audioUri == null) return;
AudioPlaybackState state = viewModel.getPlaybackState().getValue();
if (state != null && msgId == state.getMsgId() && audioUri.equals(state.getAudioUri())) {
// Same audio
if (state.getStatus() == AudioPlaybackState.PlaybackStatus.PLAYING) {
viewModel.pause(msgId, audioUri);
} else {
viewModel.play(msgId, audioUri);
}
} else {
// Different audio
// Note: they can be the same *physical* file, but in different messages
viewModel.loadAudioAndPlay(msgId, audioUri);
}
if (listener != null) {
listener.onPlayPauseButtonClicked(v);
}
});
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
AudioView.this.progress = progress;
updateTimestampsAndSeekBar();
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
viewModel.setUserSeeking(true);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
viewModel.setUserSeeking(false);
viewModel.seekTo(seekBar.getProgress(), msgId, audioUri);
}
});
if (playToPauseDrawable != null) {
playToPauseDrawable.registerAnimationCallback(animationCallback);
}
if (pauseToPlayDrawable != null) {
pauseToPlayDrawable.registerAnimationCallback(animationCallback);
}
}
@Override
protected void onDetachedFromWindow() {
if (viewModel != null) {
viewModel.getPlaybackState().removeObserver(stateObserver);
viewModel.getDurations().removeObserver(durationObserver);
}
if (playToPauseDrawable != null) {
playToPauseDrawable.clearAnimationCallbacks();
}
if (pauseToPlayDrawable != null) {
pauseToPlayDrawable.clearAnimationCallbacks();
}
super.onDetachedFromWindow();
}
public void setPlaybackViewModel(AudioPlaybackViewModel viewModel) {
if (this.viewModel != null) {
this.viewModel.getPlaybackState().removeObserver(stateObserver);
this.viewModel.getDurations().removeObserver(durationObserver);
}
// ViewModel is used directly for simplicity, since there is no reuse yet
this.viewModel = viewModel;
if (viewModel != null) {
viewModel.getPlaybackState().observeForever(stateObserver);
viewModel.getDurations().observeForever(durationObserver);
}
}
public void setAudio(final @NonNull AudioSlide audio)
{
msgId = audio.getDcMsgId();
audioUri = audio.getUri();
playPauseButton.setImageDrawable(playDrawable);
seekBar.setEnabled(true);
// Get duration
Map<Integer, Long> durations = viewModel.getDurations().getValue();
if (durations != null && durations.containsKey(msgId)) {
this.duration = Math.toIntExact(durations.get(msgId));
updateTimestampsAndSeekBar();
} else {
viewModel.ensureDurationLoaded(getContext(), msgId, audioUri);
}
if(audio.asAttachment().isVoiceNote() || !audio.getFileName().isPresent()) {
title.setVisibility(View.GONE);
}
else {
title.setText(audio.getFileName().get());
title.setVisibility(View.VISIBLE);
}
}
@Override
public void setOnClickListener(OnClickListener listener) {
super.setOnClickListener(listener);
this.mask.setOnClickListener(listener);
}
@Override
public void setOnLongClickListener(OnLongClickListener listener) {
super.setOnLongClickListener(listener);
this.mask.setOnLongClickListener(listener);
this.playPauseButton.setOnLongClickListener(listener);
}
public int getMsgId() {
return msgId;
}
public Uri getAudioUri() {
return audioUri;
}
public interface OnActionListener {
void onPlayPauseButtonClicked(View view);
}
public void setOnActionListener(OnActionListener listener) {
this.listener = listener;
}
public void togglePlay() {
playPauseButton.performClick();
}
public String getDescription() {
String desc;
if (this.title.getVisibility() == View.VISIBLE) {
desc = getContext().getString(R.string.audio);
} else {
desc = getContext().getString(R.string.voice_message);
}
desc += "\n" + this.timestamp.getText();
if (title.getVisibility() == View.VISIBLE) {
desc += "\n" + this.title.getText();
}
return desc;
}
private void updateProgress(AudioPlaybackState state) {
int duration = Math.toIntExact(state.getDuration());
int position = Math.toIntExact(state.getCurrentPosition());
if (duration > 0) {
this.progress = position;
this.duration = duration;
updateTimestampsAndSeekBar();
}
}
public void disablePlayer(boolean disable) {
this.mask.setVisibility(disable? View.VISIBLE : View.GONE);
}
public void getSeekBarGlobalVisibleRect(@NonNull Rect rect) {
seekBar.getGlobalVisibleRect(rect);
}
private void togglePlayPause(boolean expectedPlaying) {
isPlaying = expectedPlaying;
Drawable expectedDrawable = expectedPlaying ? pauseDrawable : playDrawable;
boolean isAnimating = false;
Drawable currentDrawable = playPauseButton.getDrawable();
if (currentDrawable instanceof AnimatedVectorDrawableCompat) {
isAnimating = ((AnimatedVectorDrawableCompat) currentDrawable).isRunning();
}
if (!isAnimating && playPauseButton.getDrawable() != expectedDrawable) {
AnimatedVectorDrawableCompat animDrawable = expectedPlaying ? playToPauseDrawable : pauseToPlayDrawable;
String contentDescription = getContext().getString(
expectedPlaying ? R.string.menu_pause : R.string.menu_play);
if (animDrawable != null) {
playPauseButton.setImageDrawable(animDrawable);
playPauseButton.setContentDescription(contentDescription);
animDrawable.start();
}
}
}
private void onPlaybackStateChanged(AudioPlaybackState state) {
if (audioUri == null || state == null) return;
// Check if this state is about this message
boolean isThisMessage = msgId == state.getMsgId() && audioUri.equals(state.getAudioUri());
if (isThisMessage) {
updateUIForPlaybackState(state);
} else {
togglePlayPause(false);
// Also clear progress to avoid confusion
this.progress = 0;
updateTimestampsAndSeekBar();
}
}
private void updateUIForPlaybackState(AudioPlaybackState state) {
switch (state.getStatus()) {
case PLAYING:
togglePlayPause(true);
updateProgress(state);
break;
case PAUSED:
togglePlayPause(false);
updateProgress(state);
break;
case LOADING:
case ERROR:
// No special handling yet
break;
}
}
private void onDurationsChanged(Map<Integer, Long> durations) {
AudioPlaybackState state = viewModel.getPlaybackState().getValue();
// When there is no playback happening, msgId can be -1 and audioUri is null
if (state != null &&
msgId >= 0 && msgId == state.getMsgId() &&
audioUri != null && audioUri.equals(state.getAudioUri())) {
return; // Is playing this message
}
Long duration = durations.get(msgId);
if (duration != null && seekBar.getMax() <= 100) {
this.duration = Math.toIntExact(duration);
updateTimestampsAndSeekBar();
seekBar.setMax(this.duration);
}
}
private void updateTimestampsAndSeekBar() {
String progressText = DateUtils.getFormatedDuration(progress);
String durationText = DateUtils.getFormatedDuration(duration);
timestamp.setText(String.format("%s / %s", progressText, durationText));
seekBar.setProgress(progress);
seekBar.setMax(duration);
}
}
@@ -138,7 +138,7 @@ public class AccountManager {
// ui
public void showSwitchAccountMenu(ConversationListActivity activity, boolean selectOnly) {
AccountSelectionListFragment dialog = AccountSelectionListFragment.newInstance(selectOnly);
AccountSelectionListFragment dialog = new AccountSelectionListFragment(activity, selectOnly);
dialog.show(((FragmentActivity) activity).getSupportFragmentManager(), null);
}
@@ -164,23 +164,6 @@ public class DcEventCenter {
});
}
public void handleLogging(@NonNull DcEvent event) {
final String logPrefix = "[accId="+event.getAccountId()+ "] ";
switch (event.getId()) {
case DcContext.DC_EVENT_INFO:
Log.i("DeltaChat", logPrefix + event.getData2Str());
break;
case DcContext.DC_EVENT_WARNING:
Log.w("DeltaChat", logPrefix + event.getData2Str());
break;
case DcContext.DC_EVENT_ERROR:
Log.e("DeltaChat", logPrefix + event.getData2Str());
break;
}
}
public long handleEvent(@NonNull DcEvent event) {
int accountId = event.getAccountId();
int id = event.getId();
@@ -222,7 +205,20 @@ public class DcEventCenter {
return 0;
}
handleLogging(event);
final String logPrefix = "[accId="+accountId + "] ";
switch (id) {
case DcContext.DC_EVENT_INFO:
Log.i("DeltaChat", logPrefix + event.getData2Str());
break;
case DcContext.DC_EVENT_WARNING:
Log.w("DeltaChat", logPrefix + event.getData2Str());
break;
case DcContext.DC_EVENT_ERROR:
Log.e("DeltaChat", logPrefix + event.getData2Str());
break;
}
if (accountId != context.getDcContext().getAccountId()) {
return 0;
@@ -132,7 +132,7 @@ public class DcHelper {
dcContext.setStockTranslation(68, context.getString(R.string.device_talk));
dcContext.setStockTranslation(69, context.getString(R.string.saved_messages));
dcContext.setStockTranslation(70, context.getString(R.string.device_talk_explain));
dcContext.setStockTranslation(71, context.getString(R.string.device_welcome_message, "https://i.delta.chat/#0A45953086F0C166D3BAF1D4BB2025496E4C2704&x=3KvvQZfzU4t-9u5s0PF3USGp&i=X-QDZ681F6Plz_uBu47CKdg4&s=IwE4LraLDcdPBW597wB7DBnI&a=arcanechat%40arcanechat.me&n=ArcaneChat&g=ArcaneChat+Community"));
dcContext.setStockTranslation(71, context.getString(R.string.device_welcome_message, "https://i.delta.chat/#0A45953086F0C166D3BAF1D4BB2025496E4C2704&x=MVPi07rQBEmHO4FRb3brpwDe&j=n8mkKqu42WAKKUCx1bQOVh23&s=RxuXoa0vhvTs0QLsWM45Ues0&a=adb%40arcanechat.me&n=adb&b=ArcaneChat+Channel"));
dcContext.setStockTranslation(73, context.getString(R.string.systemmsg_subject_for_new_contact));
dcContext.setStockTranslation(74, context.getString(R.string.systemmsg_failed_sending_to));
dcContext.setStockTranslation(84, context.getString(R.string.configuration_failed_with_error));
@@ -210,6 +210,8 @@ public class DcHelper {
dcContext.setStockTranslation(178, context.getString(R.string.member_x_removed));
dcContext.setStockTranslation(190, context.getString(R.string.secure_join_wait));
dcContext.setStockTranslation(193, context.getString(R.string.donate_device_msg));
dcContext.setStockTranslation(194, context.getString(R.string.outgoing_call));
dcContext.setStockTranslation(195, context.getString(R.string.incoming_call));
dcContext.setStockTranslation(196, context.getString(R.string.declined_call));
dcContext.setStockTranslation(197, context.getString(R.string.canceled_call));
dcContext.setStockTranslation(198, context.getString(R.string.missed_call));
@@ -221,12 +223,6 @@ public class DcHelper {
dcContext.setStockTranslation(220, context.getString(R.string.proxy_enabled));
dcContext.setStockTranslation(221, context.getString(R.string.proxy_enabled_hint));
dcContext.setStockTranslation(230, context.getString(R.string.chat_unencrypted_explanation));
dcContext.setStockTranslation(232, context.getString(R.string.outgoing_audio_call));
dcContext.setStockTranslation(233, context.getString(R.string.outgoing_video_call));
dcContext.setStockTranslation(234, context.getString(R.string.incoming_audio_call));
dcContext.setStockTranslation(235, context.getString(R.string.incoming_video_call));
dcContext.setStockTranslation(240, context.getString(R.string.chat_description_changed_by_you));
dcContext.setStockTranslation(241, context.getString(R.string.chat_description_changed_by_other));
}
public static File getImexDir() {
@@ -49,13 +49,13 @@ import org.thoughtcrime.securesms.WebxdcActivity;
import org.thoughtcrime.securesms.WebxdcStoreActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.components.VcardView;
import org.thoughtcrime.securesms.components.WebxdcView;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.geolocation.DcLocationManager;
@@ -121,7 +121,7 @@ public class AttachmentManager {
//this.mapView = ViewUtil.findById(root, R.id.attachment_location);
this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view);
removableMediaView.addRemoveClickListener(new RemoveButtonListener());
removableMediaView.setRemoveClickListener(new RemoveButtonListener());
removableMediaView.setEditClickListener(new EditButtonListener());
thumbnail.setOnClickListener(new ThumbnailClickListener());
}
@@ -152,6 +152,8 @@ public class AttachmentManager {
markGarbage(getSlideUri());
slide = Optional.absent();
audioView.cleanup();
}
}
@@ -233,8 +235,7 @@ public class AttachmentManager {
@NonNull final MediaType mediaType,
final int width,
final int height,
final int chatId,
AudioPlaybackViewModel playbackViewModel)
final int chatId)
{
inflateStub();
@@ -284,12 +285,26 @@ public class AttachmentManager {
setAttachmentPresent(true);
if (slide.hasAudio()) {
audioView.setPlaybackViewModel(playbackViewModel);
audioView.setAudio((AudioSlide) slide);
class SetDurationListener implements AudioSlidePlayer.Listener {
@Override
public void onStart() {}
@Override
public void onStop() {}
@Override
public void onProgress(AudioSlide slide, double progress, long millis) {}
@Override
public void onReceivedDuration(int millis) {
((AudioView) removableMediaView.getCurrent()).setDuration(millis);
}
}
AudioSlidePlayer audioSlidePlayer = AudioSlidePlayer.createFor(context, (AudioSlide) slide, new SetDurationListener());
audioSlidePlayer.requestDuration();
audioView.setAudio((AudioSlide) slide, 0);
removableMediaView.display(audioView, false);
removableMediaView.addRemoveClickListener(v -> {
playbackViewModel.stop(audioView.getMsgId(), audioView.getAudioUri());
});
result.set(true);
} else if (slide.isVcard()) {
vcardView.setVcard(glideRequests, (VcardSlide)slide, DcHelper.getRpc(context));
@@ -61,8 +61,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import chat.delta.rpc.RpcException;
public class NotificationCenter {
private static final String TAG = NotificationCenter.class.getSimpleName();
@NonNull private final ApplicationContext context;
@@ -166,7 +164,7 @@ public class NotificationCenter {
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | IntentUtils.FLAG_MUTABLE());
}
public PendingIntent getOpenCallIntent(ChatData chatData, int callId, String payload, boolean autoAccept, boolean hasVideo) {
public PendingIntent getOpenCallIntent(ChatData chatData, int callId, String payload, boolean autoAccept) {
final Intent chatIntent = new Intent(context, ConversationActivity.class)
.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, chatData.accountId)
.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatData.chatId)
@@ -186,7 +184,6 @@ public class NotificationCenter {
intent.putExtra(CallActivity.EXTRA_CHAT_ID, chatData.chatId);
intent.putExtra(CallActivity.EXTRA_CALL_ID, callId);
intent.putExtra(CallActivity.EXTRA_HASH, hash);
intent.putExtra(CallActivity.EXTRA_HAS_VIDEO, hasVideo);
intent.setPackage(context.getPackageName());
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(chatIntent)
@@ -430,13 +427,6 @@ public class NotificationCenter {
Util.runOnAnyBackgroundThread(() -> {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
DcContext dcContext = context.getDcAccounts().getAccount(accId);
boolean hasVideo;
try {
hasVideo = context.getRpc().callInfo(accId, callId).hasVideo;
} catch (RpcException e) {
Log.e(TAG, "Rpc.callInfo() failed", e);
hasVideo = false;
}
int chatId = dcContext.getMsg(callId).getChatId();
DcChat dcChat = dcContext.getChat(chatId);
String name = dcChat.getName();
@@ -444,7 +434,7 @@ public class NotificationCenter {
String notificationChannel = getCallNotificationChannel(notificationManager, chatData, name);
PendingIntent declineCallIntent = getDeclineCallIntent(chatData, callId);
PendingIntent openCallIntent = getOpenCallIntent(chatData, callId, payload, false, hasVideo);
PendingIntent openCallIntent = getOpenCallIntent(chatData, callId, payload, false);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, notificationChannel)
.setSmallIcon(R.drawable.icon_notification)
@@ -469,7 +459,7 @@ public class NotificationCenter {
new NotificationCompat.Action.Builder(
R.drawable.ic_videocam_white_24dp,
context.getString(R.string.answer_call),
getOpenCallIntent(chatData, callId, payload, true, hasVideo)).build());
getOpenCallIntent(chatData, callId, payload, true)).build());
Bitmap bitmap = getAvatar(dcChat);
if (bitmap != null) {
@@ -1,35 +0,0 @@
package org.thoughtcrime.securesms.qr;
import android.app.Activity;
import com.journeyapps.barcodescanner.BarcodeResult;
import com.journeyapps.barcodescanner.CaptureManager;
import com.journeyapps.barcodescanner.DecoratedBarcodeView;
public class CustomCaptureManager extends CaptureManager {
private OnResultInterceptor interceptor;
public CustomCaptureManager(Activity activity, DecoratedBarcodeView barcodeView) {
super(activity, barcodeView);
}
public void setResultInterceptor(OnResultInterceptor interceptor) {
this.interceptor = interceptor;
}
@Override
protected void returnResult(BarcodeResult rawResult) {
if (interceptor != null) {
interceptor.onResult(rawResult, () -> {
super.returnResult(rawResult);
});
} else {
super.returnResult(rawResult);
}
}
public interface OnResultInterceptor {
void onResult(BarcodeResult result, Runnable finishCallback);
}
}
@@ -325,29 +325,25 @@ public class QrCodeHandler {
}
builder.setMessage(msg);
builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
secureJoinByQr(qrRawString, source, uipath);
try {
int newChatId = DcHelper.getRpc(activity).secureJoinWithUxInfo(dcContext.getAccountId(), qrRawString, source, uipath);
if (newChatId == 0) throw new Exception("Securejoin failed to create a chat");
Intent intent = new Intent(activity, ConversationActivity.class);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, newChatId);
activity.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
AlertDialog.Builder builder1 = new AlertDialog.Builder(activity);
builder1.setMessage(e.getMessage());
builder1.setPositiveButton(android.R.string.ok, null);
builder1.create().show();
}
});
builder.setNegativeButton(android.R.string.cancel, null);
}
public void secureJoinByQr(String qrRawString, SecurejoinSource source, SecurejoinUiPath uipath) {
try {
int newChatId = DcHelper.getRpc(activity).secureJoinWithUxInfo(dcContext.getAccountId(), qrRawString, source, uipath);
if (newChatId == 0) throw new Exception("Securejoin failed to create a chat");
Intent intent = new Intent(activity, ConversationActivity.class);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, newChatId);
activity.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
AlertDialog.Builder builder1 = new AlertDialog.Builder(activity);
builder1.setMessage(e.getMessage());
builder1.setPositiveButton(android.R.string.ok, null);
builder1.create().show();
}
}
public void addRelay(String qrData) {
public void addRelay(String qrData) {
ProgressDialog progressDialog = new ProgressDialog(activity);
progressDialog.setMessage(activity.getResources().getString(R.string.one_moment));
progressDialog.setCanceledOnTouchOutside(false);
@@ -10,12 +10,8 @@ import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcLot;
import com.journeyapps.barcodescanner.CaptureManager;
import com.journeyapps.barcodescanner.CompoundBarcodeView;
import org.thoughtcrime.securesms.BaseActionBarActivity;
@@ -30,12 +26,10 @@ public class RegistrationQrActivity extends BaseActionBarActivity {
public static final String ADD_AS_SECOND_DEVICE_EXTRA = "add_as_second_device";
public static final String QRDATA_EXTRA = "qrdata";
private CustomCaptureManager capture;
private CaptureManager capture;
private CompoundBarcodeView barcodeScannerView;
private DcContext dcContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -72,8 +66,6 @@ public class RegistrationQrActivity extends BaseActionBarActivity {
.onAnyResult(this::handleQrScanWithPermissions)
.onAnyDenied(this::handleQrScanWithDeniedPermission)
.execute();
dcContext = DcHelper.getContext(this);
}
private void handleQrScanWithPermissions() {
@@ -97,70 +89,23 @@ public class RegistrationQrActivity extends BaseActionBarActivity {
DcHelper.openHelp(this, "#multiclient");
return true;
} else if (itemId == R.id.menu_paste) {
String rawQr = Util.getTextFromClipboard(this);
Runnable okCallback = () -> {
Intent intent = new Intent();
intent.putExtra(QRDATA_EXTRA, rawQr);
setResult(Activity.RESULT_OK, intent);
finish();
};
showConfirmDialog(rawQr, okCallback, null);
Intent intent = new Intent();
intent.putExtra(QRDATA_EXTRA, Util.getTextFromClipboard(this));
setResult(Activity.RESULT_OK, intent);
finish();
return true;
}
return false;
}
private void showConfirmDialog(String rawQr, @NonNull Runnable okCallback, @Nullable Runnable cancelCallback) {
DcLot qrParsed = dcContext.checkQr(rawQr);
String dialogMsg = "";
if (qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYCONTACT) {
String name = dcContext.getContact(qrParsed.getId()).getDisplayName();
dialogMsg = getString(R.string.instant_onboarding_confirm_contact, name);
} else if (qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYGROUP) {
String groupName = qrParsed.getText1();
dialogMsg = getString(R.string.instant_onboarding_confirm_group, groupName);
}
if (qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYCONTACT
|| qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYGROUP) {
AlertDialog confirmDialog = new AlertDialog.Builder(this)
.setMessage(dialogMsg)
.setPositiveButton("OK", (dialog, which) -> {
okCallback.run();
})
.setNegativeButton("Cancel", (dialog, which) -> {
if (cancelCallback != null) {
cancelCallback.run();
}
})
.show();
} else {
okCallback.run();
}
}
@Override
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
private void init(CompoundBarcodeView barcodeScannerView, Intent intent, Bundle savedInstanceState) {
capture = new CustomCaptureManager(this, barcodeScannerView);
capture.setResultInterceptor((result, finishCallback) -> {
String rawQr = result.getText();
showConfirmDialog(rawQr, finishCallback, () -> {
barcodeScannerView.resume();
capture.decode();
});
});
capture = new CaptureManager(this, barcodeScannerView);
capture.initializeFromIntent(intent, savedInstanceState);
capture.decode();
}
@@ -36,24 +36,19 @@ import chat.delta.rpc.types.Reactions;
public class ReactionsDetailsFragment extends DialogFragment implements DcEventCenter.DcEventDelegate {
private static final String TAG = ReactionsDetailsFragment.class.getSimpleName();
private static final String ARG_MSG_ID = "msg_id";
private RecyclerView recyclerView;
private ReactionRecipientsAdapter adapter;
private int msgId;
private final int msgId;
public static ReactionsDetailsFragment newInstance(int msgId) {
ReactionsDetailsFragment fragment = new ReactionsDetailsFragment();
Bundle args = new Bundle();
args.putInt(ARG_MSG_ID, msgId);
fragment.setArguments(args);
return fragment;
public ReactionsDetailsFragment(int msgId) {
super();
this.msgId = msgId;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
msgId = getArguments() != null? getArguments().getInt(ARG_MSG_ID, 0) : 0;
adapter = new ReactionRecipientsAdapter(requireActivity(), GlideApp.with(requireActivity()), new ListClickListener());
LayoutInflater inflater = requireActivity().getLayoutInflater();
@@ -3,9 +3,7 @@ package org.thoughtcrime.securesms.relay;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.ContextMenu;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
@@ -49,9 +47,6 @@ public class RelayListActivity extends BaseActionBarActivity
/** QR provided via Intent extras needs to be saved to pass it to QrCodeHandler when authorization finishes */
private String qrData = null;
/** Relay selected for context menu via onRelayLongClick() */
private EnteredLoginParam contextMenuRelay = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -148,54 +143,14 @@ public class RelayListActivity extends BaseActionBarActivity
}
@Override
public void onRelayLongClick(View view, EnteredLoginParam relay) {
contextMenuRelay = relay;
registerForContextMenu(view);
openContextMenu(view);
unregisterForContextMenu(view);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.relay_item_context, menu);
boolean nonNullAddr = contextMenuRelay != null && contextMenuRelay.addr != null;
boolean isMain = nonNullAddr && contextMenuRelay.addr.equals(adapter.getMainRelay());
menu.findItem(R.id.menu_delete_relay).setVisible(!isMain);
}
@Override
public void onContextMenuClosed(android.view.Menu menu) {
super.onContextMenuClosed(menu);
contextMenuRelay = null;
}
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
if (contextMenuRelay == null) return super.onContextItemSelected(item);
int itemId = item.getItemId();
if (itemId == R.id.menu_edit_relay) {
onRelayEdit(contextMenuRelay);
contextMenuRelay = null;
return true;
} else if (itemId == R.id.menu_delete_relay) {
onRelayDelete(contextMenuRelay);
contextMenuRelay = null;
return true;
}
return super.onContextItemSelected(item);
}
private void onRelayEdit(EnteredLoginParam relay) {
public void onRelayEdit(EnteredLoginParam relay) {
Intent intent = new Intent(this, EditRelayActivity.class);
intent.putExtra(EditRelayActivity.EXTRA_ADDR, relay.addr);
startActivity(intent);
}
private void onRelayDelete(EnteredLoginParam relay) {
@Override
public void onRelayDelete(EnteredLoginParam relay) {
new AlertDialog.Builder(this)
.setTitle(R.string.remove_transport)
.setMessage(getString(R.string.confirm_remove_transport, relay.addr))
@@ -25,7 +25,8 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
public interface OnRelayClickListener {
void onRelayClick(EnteredLoginParam relay);
void onRelayLongClick(View view, EnteredLoginParam relay);
void onRelayEdit(EnteredLoginParam relay);
void onRelayDelete(EnteredLoginParam relay);
}
public RelayListAdapter(OnRelayClickListener listener) {
@@ -66,12 +67,16 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
private final TextView titleText;
private final TextView subtitleText;
private final ImageView mainIndicator;
private final ImageView editButton;
private final ImageView deleteButton;
public RelayViewHolder(@NonNull View itemView) {
super(itemView);
titleText = itemView.findViewById(R.id.title);
subtitleText = itemView.findViewById(R.id.subtitle);
mainIndicator = itemView.findViewById(R.id.main_indicator);
editButton = itemView.findViewById(R.id.edit_button);
deleteButton = itemView.findViewById(R.id.delete_button);
}
public void bind(EnteredLoginParam relay, boolean isMain, OnRelayClickListener listener) {
@@ -79,6 +84,7 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
titleText.setText(parts.length == 2? parts[1] : parts[0]);
subtitleText.setText(parts.length == 2? parts[0] : "");
mainIndicator.setVisibility(isMain ? View.VISIBLE : View.INVISIBLE);
deleteButton.setVisibility(isMain ? View.GONE : View.VISIBLE);
itemView.setOnClickListener(v -> {
if (listener != null) {
@@ -86,11 +92,16 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
}
});
itemView.setOnLongClickListener(v -> {
editButton.setOnClickListener(v -> {
if (listener != null) {
listener.onRelayLongClick(v, relay);
listener.onRelayEdit(relay);
}
});
deleteButton.setOnClickListener(v -> {
if (listener != null) {
listener.onRelayDelete(relay);
}
return true;
});
}
}
@@ -1,140 +0,0 @@
package org.thoughtcrime.securesms.service;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.session.MediaSession;
import androidx.media3.session.MediaSessionService;
import androidx.media3.session.SessionCommand;
import androidx.media3.session.SessionCommands;
import androidx.media3.session.SessionResult;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.ConversationListActivity;
public class AudioPlaybackService extends MediaSessionService {
private static final String TAG = AudioPlaybackService.class.getSimpleName();
private ExoPlayer player;
private MediaSession session;
@Override
public void onCreate() {
super.onCreate();
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA) // USAGE_VOICE_COMMUNICATION is for VoIP calls
.setContentType(C.AUDIO_CONTENT_TYPE_SPEECH)
.build();
player = new ExoPlayer.Builder(this)
.setAudioAttributes(audioAttributes, true)
.setHandleAudioBecomingNoisy(true)
.build();
// This is for click on the notification to go back to app
Intent intent = new Intent(this, ConversationListActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent initialIntent = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
session = new MediaSession.Builder(this, player)
.setSessionActivity(initialIntent)
.setCallback(new MediaSession.Callback() {
@OptIn(markerClass = UnstableApi.class)
@Override
public MediaSession.ConnectionResult onConnect(
MediaSession session,
MediaSession.ControllerInfo controller
) {
SessionCommands sessionCommands = MediaSession
.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
.add(new SessionCommand("UPDATE_ACTIVITY_CONTEXT", new Bundle()))
.build();
return new MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailableSessionCommands(sessionCommands)
.build();
}
@NonNull
@Override
public ListenableFuture<SessionResult> onCustomCommand(
MediaSession session,
MediaSession.ControllerInfo controller,
SessionCommand customCommand,
Bundle args
) {
if ("UPDATE_ACTIVITY_CONTEXT".equals(customCommand.customAction)) {
updateSessionActivity(args);
}
return Futures.immediateFuture(
new SessionResult(SessionResult.RESULT_SUCCESS));
}
})
.build();
}
@OptIn(markerClass = UnstableApi.class)
private void updateSessionActivity(Bundle args) {
try {
// Put all the original extras back into the intent
if (args != null && !args.isEmpty()) {
String activityClassName = args.getString("activity_class");
args.remove("activity_class");
if (activityClassName != null) {
Class<?> activityClass = Class.forName(activityClassName);
Intent intent = new Intent(this, activityClass);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtras(args);
PendingIntent pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
session.setSessionActivity(pendingIntent);
}
}
} catch (ClassNotFoundException e) {
Log.e(TAG, "Activity class not found", e);
}
}
@Nullable
@Override
public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
return session;
}
@Override
public void onDestroy() {
if (session != null) {
session.release();
session = null;
}
if (player != null) {
player.release();
player = null;
}
super.onDestroy();
}
}
@@ -323,15 +323,7 @@ public class ViewUtil {
* @param view The view to apply insets to
*/
public static void applyWindowInsetsAsMargin(@NonNull View view) {
applyWindowInsetsAsMargin(view, true, true, true, true, false);
}
public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
applyWindowInsetsAsMargin(view, left, top, right, bottom, false);
}
public static void forceApplyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
applyWindowInsetsAsMargin(view, left, top, right, bottom, true);
applyWindowInsetsAsMargin(view, true, true, true, true);
}
/**
@@ -345,9 +337,8 @@ public class ViewUtil {
* @param top Whether to apply top inset
* @param right Whether to apply right inset
* @param bottom Whether to apply bottom inset
* @param forceDispatch Whether to force application of insets
*/
public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom, boolean forceDispatch) {
public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
// Only enable on API 30+ where WindowInsets APIs work correctly
if (!isEdgeToEdgeSupported()) return;
@@ -380,10 +371,10 @@ public class ViewUtil {
ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) layoutParams;
marginParams.leftMargin = left ? baseMarginLeft + insets.left : baseMarginLeft;
marginParams.topMargin = top ? baseMarginTop + insets.top : baseMarginTop;
marginParams.rightMargin = right ? baseMarginRight + insets.right : baseMarginRight;
marginParams.bottomMargin = bottom ? baseMarginBottom + insets.bottom : baseMarginBottom;
marginParams.leftMargin = baseMarginLeft + insets.left;
marginParams.topMargin = baseMarginTop + insets.top;
marginParams.rightMargin = baseMarginRight + insets.right;
marginParams.bottomMargin = baseMarginBottom + insets.bottom;
v.setLayoutParams(marginParams);
}
@@ -392,54 +383,18 @@ public class ViewUtil {
// Request the initial insets to be dispatched if the view is attached
if (view.isAttachedToWindow()) {
if (forceDispatch) {
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view);
if (insets != null) {
ViewCompat.dispatchApplyWindowInsets(view, insets);
}
} else {
ViewCompat.requestApplyInsets(view);
}
ViewCompat.requestApplyInsets(view);
}
}
/**
* Apply window insets to a view by adding padding to avoid drawing elements behind system bars.
* Convenience method that applies insets to all sides.
* IME insets are propagated to child views.
*
* @param view The view to apply insets to
*/
public static void applyWindowInsets(@NonNull View view) {
applyWindowInsets(view, true, true, true, true, false, false);
}
/**
* Apply window insets to a view by adding padding to avoid drawing elements behind system bars.
*
* IME insets are propagated to child views.
*
* @param view The view to apply insets to
* @param left Whether to apply left inset
* @param top Whether to apply top inset
* @param right Whether to apply right inset
* @param bottom Whether to apply bottom inset
*/
public static void applyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
applyWindowInsets(view, left, top, right, bottom, false, false);
}
/**
* Force applying window insets to a view by adding padding to avoid drawing elements behind system bars.
*
* @param view The view to apply insets to
* @param left Whether to apply left inset
* @param top Whether to apply top inset
* @param right Whether to apply right inset
* @param bottom Whether to apply bottom inset
*/
public static void forceApplyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
applyWindowInsets(view, left, top, right, bottom, false, true);
applyWindowInsets(view, true, true, true, true);
}
/**
@@ -453,10 +408,8 @@ public class ViewUtil {
* @param top Whether to apply top inset
* @param right Whether to apply right inset
* @param bottom Whether to apply bottom inset
* @param consumeImeInsets Whether to consume IME insets so they don't propagate to child views
* @param forceDispatch Force application of Insets, regardless if system think it shall dispatch
*/
public static void applyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom, boolean consumeImeInsets, boolean forceDispatch) {
public static void applyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
// Only enable on API 30+ where WindowInsets APIs work correctly
if (!isEdgeToEdgeSupported()) return;
@@ -489,24 +442,12 @@ public class ViewUtil {
bottom ? basePaddingBottom + insets.bottom : basePaddingBottom
);
if (consumeImeInsets) {
windowInsets = new WindowInsetsCompat.Builder(windowInsets)
.setInsets(WindowInsetsCompat.Type.ime(), Insets.NONE)
.build();
}
return windowInsets;
});
// Request the initial insets to be dispatched if the view is attached
if (view.isAttachedToWindow()) {
if (forceDispatch) {
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view);
if (insets != null) {
ViewCompat.dispatchApplyWindowInsets(view, insets);
}
} else {
ViewCompat.requestApplyInsets(view);
}
ViewCompat.requestApplyInsets(view);
}
}
@@ -37,7 +37,7 @@ import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.ui.StyledPlayerView;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
@@ -49,7 +49,7 @@ import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
public class VideoPlayer extends FrameLayout {
@Nullable private final StyledPlayerView exoView;
@Nullable private final PlayerView exoView;
@Nullable private SimpleExoPlayer exoPlayer;
@Nullable private Window window;
+37 -19
View File
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context="org.thoughtcrime.securesms.components.AudioView">
<LinearLayout android:id="@+id/audio_widget_container"
android:orientation="vertical"
@@ -10,19 +12,39 @@
android:layout_height="fill_parent"
android:orientation="horizontal">
<ImageView android:id="@+id/play_pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:focusable="true"
android:gravity="center_vertical"
android:clickable="true"
android:visibility="visible"
android:background="@drawable/ic_circle_fill_white_48dp"
android:backgroundTint="@color/audio_icon"
android:src="@drawable/play_icon"
android:scaleType="centerInside"
android:contentDescription="@string/menu_play"/>
<org.thoughtcrime.securesms.components.AnimatingToggle
android:id="@+id/control_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center">
<ImageView android:id="@+id/play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:clickable="true"
android:visibility="gone"
android:background="@drawable/circle_touch_highlight_background"
android:src="@drawable/ic_play_circle_fill_white_48dp"
android:scaleType="centerInside"
tools:visibility="gone"
android:contentDescription="@string/menu_play"/>
<ImageView android:id="@+id/pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:clickable="true"
android:visibility="gone"
android:background="@drawable/circle_touch_highlight_background"
android:src="@drawable/ic_pause_circle_fill_white_48dp"
android:scaleType="centerInside"
android:contentDescription="@string/menu_pause"/>
</org.thoughtcrime.securesms.components.AnimatingToggle>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="fill_parent"
@@ -32,11 +54,7 @@
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:layout_gravity="center_vertical"
android:progressTint="@color/audio_icon"
android:progressTintMode="src_in"
android:thumbTint="@color/audio_icon"
android:thumbTintMode="src_in"/>
android:layout_gravity="center_vertical"/>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content"
+1
View File
@@ -44,6 +44,7 @@
android:layout_width="30dp"
android:layout_height="30dp"
android:contentDescription="@null"
android:src="@drawable/ic_videocam_white_24dp"
/>
</LinearLayout>
@@ -37,7 +37,7 @@
app:minHeight="100dp"
app:maxHeight="300dp" />
<org.thoughtcrime.securesms.components.audioplay.AudioView
<org.thoughtcrime.securesms.components.AudioView
android:id="@+id/attachment_audio"
android:layout_width="230dp"
android:layout_height="wrap_content"
@@ -27,7 +27,6 @@
/>
<View
android:id="@+id/bottom_divider"
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_gravity="bottom"
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.audioplay.AudioView
<org.thoughtcrime.securesms.components.AudioView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="210dp"
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.audioplay.AudioView
<org.thoughtcrime.securesms.components.AudioView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="210dp"
android:layout_height="wrap_content"
@@ -39,24 +39,6 @@
</androidx.appcompat.widget.AppCompatEditText>
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/chat_description_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/chat_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top|start"
android:hint="@string/chat_description"
android:inputType="textMultiLine"
android:maxLines="3" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/chat_hints"
android:layout_width="match_parent"
@@ -47,17 +47,6 @@
</LinearLayout>
<TextView
android:id="@+id/invitation_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:gravity="start"
android:visibility="gone"
android:textColor="@color/gray50" />
<TextView
android:id="@+id/information_label"
android:layout_width="match_parent"
@@ -17,7 +17,7 @@
android:layout_weight="1"
android:visibility="gone"/>
<org.thoughtcrime.securesms.components.audioplay.AudioView
<org.thoughtcrime.securesms.components.AudioView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/audio_view"
+26
View File
@@ -59,4 +59,30 @@
</LinearLayout>
<ImageView
android:id="@+id/delete_button"
android:layout_width="48dp"
android:layout_height="72dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:clickable="true"
android:focusable="true"
android:contentDescription="@string/remove_transport"
android:background="@drawable/touch_highlight_background"
android:src="@drawable/ic_delete_white_24dp"
app:tint="?attr/conversation_list_item_date_color" />
<ImageView
android:id="@+id/edit_button"
android:layout_width="48dp"
android:layout_height="72dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:clickable="true"
android:focusable="true"
android:contentDescription="@string/edit_transport"
android:background="@drawable/touch_highlight_background"
android:src="@drawable/ic_create_white_24dp"
app:tint="?attr/conversation_list_item_date_color" />
</LinearLayout>
+1 -1
View File
@@ -4,7 +4,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.exoplayer2.ui.StyledPlayerView
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
+3 -16
View File
@@ -26,22 +26,9 @@
app:showAsAction="always"/>
<item android:id="@+id/menu_start_call"
android:title="@string/start_call"
app:showAsAction="always"
android:icon="@drawable/baseline_call_24">
<menu>
<item android:id="@+id/menu_start_audio_call"
android:title="@string/start_audio_call"
android:icon="@drawable/baseline_call_24"
app:iconTint="?attr/menu_icon_tint"
/>
<item android:id="@+id/menu_start_video_call"
android:title="@string/start_video_call"
android:icon="@drawable/ic_videocam_white_24dp"
app:iconTint="?attr/menu_icon_tint"
/>
</menu>
</item>
android:title="@string/start_call"
android:icon="@drawable/ic_videocam_white_24dp"
app:showAsAction="always" />
<item android:id="@+id/menu_all_media"
android:title="@string/apps_and_media"
-11
View File
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="@string/edit_transport"
android:id="@+id/menu_edit_relay"/>
<item android:title="@string/remove_transport"
android:id="@+id/menu_delete_relay"/>
</menu>
-3
View File
@@ -21,7 +21,6 @@
<string name="back">العودة</string>
<string name="close">إغلاق</string>
<string name="forward">إعادة توجيه</string>
<!-- Verb "to mute". used to mute a chat, so that it is silenced e.g. does not make noise or cause notifications -->
<string name="mute">حجب المكرفون.</string>
<string name="ephemeral_messages">الرسائل المُختفية</string>
<string name="save">حفظ</string>
@@ -56,7 +55,6 @@
<string name="image">صورة</string>
<string name="gif">صورة GIF</string>
<string name="images">الصور</string>
<!-- a noun, used for "Music" and other "Audio" files -->
<string name="audio">الصوت</string>
<string name="voice_message">رسالة صوتية</string>
<string name="video">فيديو</string>
@@ -165,7 +163,6 @@
<string name="pref_your_name">اسمك</string>
<!-- Label of the Bio/Signature/Status/Motto field -->
<string name="pref_default_status_label">التوقيع</string>
<!-- Option name for changing behaviour of the "Enter key" to sending out a message directly (instead of adding new line). If in doubt, please refer to a similar option in other messengers. -->
<string name="pref_enter_sends">مفتاح الدخول يقوم بالإرسا</string>
<string name="pref_vibrate">إهتز</string>
<string name="pref_screen_security">تأمين الشاشة</string>
-3
View File
@@ -26,7 +26,6 @@
<string name="archive">Arxivləşmə</string>
<!-- Verb "to unarchive", as in "remove a chat from the archive", opposite of the previous string -->
<string name="unarchive">Arxivdən çıxarma</string>
<!-- Verb "to mute". used to mute a chat, so that it is silenced e.g. does not make noise or cause notifications -->
<string name="mute">Səssiz</string>
<string name="ephemeral_messages">Yox olan mesajlar</string>
<string name="save">Saxla</string>
@@ -88,7 +87,6 @@
<string name="image">Şəkil</string>
<string name="gif">Gif</string>
<string name="images">Şəkillər</string>
<!-- a noun, used for "Music" and other "Audio" files -->
<string name="audio">Audio</string>
<string name="voice_message">Səsli mesaj</string>
<string name="forwarded_message">Köçürülmüş mesaj</string>
@@ -280,7 +278,6 @@
<string name="pref_your_name">Adın</string>
<!-- Label of the Bio/Signature/Status/Motto field -->
<string name="pref_default_status_label">Yazını mətni</string>
<!-- Option name for changing behaviour of the "Enter key" to sending out a message directly (instead of adding new line). If in doubt, please refer to a similar option in other messengers. -->
<string name="pref_enter_sends">Enter açarı ilə göndərmək</string>
<string name="pref_enter_sends_explain">Enter düyməsini basaraq mətn mesajları göndərəcək</string>
<string name="pref_outgoing_media_quality">Göndərilən medianın keyfiyyatı</string>
-3
View File
@@ -40,7 +40,6 @@
<string name="archive">Архивиране</string>
<!-- Verb "to unarchive", as in "remove a chat from the archive", opposite of the previous string -->
<string name="unarchive">Деархивиране</string>
<!-- Verb "to mute". used to mute a chat, so that it is silenced e.g. does not make noise or cause notifications -->
<string name="mute">Изключване на звука</string>
<string name="ephemeral_messages">Изчезващи съобщения</string>
<string name="ephemeral_messages_hint">Приложимо за всички членове на този чат, ако използват Delta Chat; в противен случай те пак могат да копират, записват и препращат съобщения или да използват други клиенти за електронна поща.</string>
@@ -147,7 +146,6 @@
<string name="add_stickers_instructions">За да добавите стикери, натиснете върху \"Отваряне на папка Стикери\", създайте подпапка за Вашия пакет от стикери и дърпайте файлове със стикери и изображения тук</string>
<string name="open_sticker_folder">Отваряне на папка Стикери</string>
<string name="images">Изображения</string>
<!-- a noun, used for "Music" and other "Audio" files -->
<string name="audio">Аудио</string>
<string name="voice_message">Гласово съобщение</string>
<string name="forwarded">Препратено</string>
@@ -561,7 +559,6 @@
<string name="pref_your_name">Вашето име</string>
<!-- Label of the Bio/Signature/Status/Motto field -->
<string name="pref_default_status_label">Текст на подписа</string>
<!-- Option name for changing behaviour of the "Enter key" to sending out a message directly (instead of adding new line). If in doubt, please refer to a similar option in other messengers. -->
<string name="pref_enter_sends">Клавишът Enter изпраща</string>
<string name="pref_enter_sends_explain">Натискането на клавиша Enter ще изпраща текстовите съобщения</string>
<string name="pref_outgoing_media_quality">Качество на изходящата медия</string>
-4
View File
@@ -51,9 +51,7 @@
<string name="archive">بایگۊوی</string>
<!-- Verb "to unarchive", as in "remove a chat from the archive", opposite of the previous string -->
<string name="unarchive">و در زیڌن ز بایگۊوی</string>
<!-- Verb "to mute". used to mute a chat, so that it is silenced e.g. does not make noise or cause notifications -->
<string name="mute">بؽ دونگ</string>
<!-- Adjective "smth is muted". used to describe a muted or silenced chat, that e.g. does not make noise or cause notifications -->
<string name="muted">بؽ دونگ</string>
<string name="ephemeral_messages">پیوما بؽڌارکۊنی</string>
<string name="ephemeral_messages_hint">یو ری پوی منتورا ای گوفت وو لوفت، ٱر دلتا چت ن و کار بگرن ائمال ابۊ. ولی هنی ترن پیوما ن لف گیری، زفت وو فوروارد کونن.</string>
@@ -165,7 +163,6 @@
<string name="add_stickers_instructions">سی ٱووردن برچسب ری «گۊشیڌن دوبلگه برچسب» بزنین، ی لم دوبلگه ای سی کتن برچسب خوتووݩ وورکل کۊنین وو برچسبا ن و اوچو بکشین</string>
<string name="open_sticker_folder">گۊشیڌن دوبلگه استیکر</string>
<string name="images">شؽواتا</string>
<!-- a noun, used for "Music" and other "Audio" files -->
<string name="audio">دونگ</string>
<string name="voice_message">پیوم دونگ</string>
<string name="forwarded">فوروارد وابیڌه</string>
@@ -202,7 +199,6 @@
<string name="games">بازی یل</string>
<string name="tools">ٱوزار</string>
<string name="app_size">هندا</string>
<!-- Used as a headline for a date. E.g. in the details of a mini app in the app picker -->
<string name="app_date_published">تیجنیڌن</string>
<string name="add_to_chat">ٱووردن و گوفت وو لوفت</string>
<!-- short for "Browse through the App Picker/Store/Catalogue"; could also be translated as "Discover" or "Search" -->
+2 -39
View File
@@ -51,9 +51,7 @@
<string name="archive">Arxiva</string>
<!-- Verb "to unarchive", as in "remove a chat from the archive", opposite of the previous string -->
<string name="unarchive">Treu de l\'arxiu</string>
<!-- Verb "to mute". used to mute a chat, so that it is silenced e.g. does not make noise or cause notifications -->
<string name="mute">Silencia</string>
<!-- Adjective "smth is muted". used to describe a muted or silenced chat, that e.g. does not make noise or cause notifications -->
<string name="muted">Silenciat</string>
<string name="ephemeral_messages">Missatges efímers</string>
<string name="ephemeral_messages_hint">Aquestes opcions s\'apliquen a tots els integrants del xat que utiltizen Delta Chat. Tot i així, poden copiar, desar o reenviar missatges o utilitzar altres clients de correu.</string>
@@ -181,7 +179,6 @@
<string name="add_stickers_instructions">Per a afegir adhesius: premeu \"Obre la carpeta d\'adhesius\", creeu una subcarpeta per a la vostra col·lecció d\'adhesius i arrossegueu-hi la imatge i els fitxers d\'adhesius.</string>
<string name="open_sticker_folder">Obre la carpeta d\'adhesius</string>
<string name="images">Imatges</string>
<!-- a noun, used for "Music" and other "Audio" files -->
<string name="audio">Àudio</string>
<string name="voice_message">Missatge de veu</string>
<string name="forwarded">Reenviat</string>
@@ -218,7 +215,6 @@
<string name="games">Jocs</string>
<string name="tools">Eines</string>
<string name="app_size">Mida</string>
<!-- Used as a headline for a date. E.g. in the details of a mini app in the app picker -->
<string name="app_date_published">Publicat</string>
<string name="add_to_chat">Afegeix al xat</string>
<!-- short for "Browse through the App Picker/Store/Catalogue"; could also be translated as "Discover" or "Search" -->
@@ -384,20 +380,12 @@
<!-- the action "to call someone", used as a tooltip for the "phone" icon. not: "the call" -->
<string name="start_call">Truca</string>
<string name="start_audio_call">Trucada de veu</string>
<string name="start_video_call">Videotrucada</string>
<!-- the action "to answer" or to "accept" or to "pick up" a call. not: "the answer" -->
<string name="answer_call">Respon</string>
<!-- the action "to decline" a call, not: "the decline" -->
<string name="end_call">Rebutja</string>
<!-- deprecated, use outgoing_audio_call or outgoing_video_call instead -->
<string name="outgoing_call">Trucada sortint</string>
<string name="outgoing_audio_call">Trucada de veu sortint</string>
<string name="outgoing_video_call">Videotrucada sortint</string>
<!-- deprecated, use incoming_audio_call or incoming_video_call instead -->
<string name="incoming_call">Trucada entrant</string>
<string name="incoming_audio_call">Trucada de veu entrant</string>
<string name="incoming_video_call">Videotrucada entrant</string>
<string name="declined_call">Trucada rebutjada</string>
<string name="canceled_call">Trucada cancel·lada</string>
<string name="missed_call">Trucada perduda</string>
@@ -638,7 +626,6 @@
<string name="welcome_chat_over_email">Xat segur i descentralitzat</string>
<string name="scan_invitation_code">Escaneja el Codi d\'Invitació</string>
<string name="login_title">Identifiqueu-vos</string>
<string name="login_advanced_hint">Aquest inici de sessió és per a usuaris avançats:\n\n• No feu servir una adreça que utilitzeu a una altra app.\n\n• Les adreces de correu tradicional admeten xats sense xifratge d\'extrem a extrem, indicades amb una icona de correu.</string>
<string name="login_inbox">Safata d\'entrada</string>
<string name="login_imap_login">Nom d\'identificació IMAP</string>
<string name="login_imap_server">Servidor IMAP</string>
@@ -658,13 +645,11 @@
<string name="proxy_add_url_hint">Introduïu l\'enllaç del servidor intermediari aquí</string>
<string name="proxy_invalid">Servidor intermediari no compatible o invàlid</string>
<string name="proxy_list_header">Proxies desats</string>
<string name="proxy_delete">Esborra el servidor intermediari</string>
<string name="proxy_delete">Esborra el proxy</string>
<string name="proxy_delete_explain">Esteu segur de voler esborrar «%1$s»?</string>
<string name="proxy_use_proxy_confirm">Voleu utilitzar el servidor intermediari \"%1$s\"?</string>
<string name="proxy_use_proxy_confirm">Voleu utilitzar el proxy \"%1$s\"?</string>
<string name="proxy_share_explain">Els vostres amics poden afegir aquest servidor intermediari escanejant el codi QR.</string>
<string name="proxy_share_link">Comparteix l\'enllaç</string>
<string name="proxy_enabled">Servidor intermediari activat</string>
<string name="proxy_enabled_hint">Esteu fent servir un servidor intermediari. Si teniu problemes per a connectar-vos, proveu un servidor intermediari diferent.</string>
<string name="login_certificate_checks">El certificat coincideix</string>
<string name="login_error_mail">Si us plau poseu una adreça d\'email vàlida</string>
<string name="login_error_server">Si us plau poseu un servidor / adreça IP vàlida</string>
@@ -717,7 +702,6 @@
<string name="pref_your_name">El vostre nom</string>
<!-- Label of the Bio/Signature/Status/Motto field -->
<string name="pref_default_status_label">Signatura</string>
<!-- Option name for changing behaviour of the "Enter key" to sending out a message directly (instead of adding new line). If in doubt, please refer to a similar option in other messengers. -->
<string name="pref_enter_sends">La tecla «Retorn» envia</string>
<string name="pref_enter_sends_explain">Els missatges s\'enviaran prement la tecla «Retorn»</string>
<string name="pref_outgoing_media_quality">Qualitat del contingut multimèdia sortint</string>
@@ -768,7 +752,6 @@
<string name="pref_backup">Còpia de seguretat</string>
<string name="pref_backup_explain">Copia els xats a un magatzem extern</string>
<string name="pref_backup_export_explain">La còpia de seguretat és útil a l\'hora de reinstal·lar l\'aplicació en aquest dispositiu o en un altre.\n\nLa còpia tindrà tot els missatges, contactes i xats i la configuració d\'extrem a extrem d\'Autocrypt. Conserveu el fitxer de la còpia en un lloc segur o esborreu-lo tan aviat com pugueu.</string>
<string name="pref_backup_export_this">Exporta aquest perfil</string>
<!-- the placeholder will be replaced by the number of profiles to export; the number is always larger than 1 -->
<string name="pref_backup_export_all">Exporta tots %1$d perfils</string>
<string name="pref_backup_export_start_button">Inicia la còpia de seguretat</string>
@@ -778,10 +761,6 @@
<string name="pref_background_btn_default">Usa la imatge per defecte</string>
<string name="pref_background_btn_gallery">Trieu de la galeria</string>
<string name="pref_imap_folder_warn_disable_defaults">Si desactiveu aquesta opció, assegureu-vos que el vostre servidor i els altres programes estiguin configurats de la mateixa manera.\n\nAltrament podeu tenir problemes de funcionament.</string>
<!-- No need to be literal here, you can also use "Use Multiple Devices", "Support Multiple Devices" or other fitting terms. However, it should fit to the wording or your language at https://delta.chat/help -->
<string name="pref_multidevice">Mode multidispositiu</string>
<string name="pref_multidevice_explain">Sincronitza els vostres missatges amb els vostres altres dispositius. Activat automàticament quan s\'afegeix un segon dispositiu.</string>
<string name="pref_multidevice_change_warn">El mode multidispositiu ha d\'estar habilitat quan feu servir el mateix compte des de diferents perfils. Només desactiveu aquesta opció si heu suprimit aquest perfil de tots els vostres altres dispositius.\n\nDesactivar aquesta opció mentre es fa servir en el mateix perfil en múltiples dispositius farà que es perdin missatges i hi hagi altres problemes.</string>
<string name="pref_auto_folder_moves">Moure automàticament a la carpeta de Delta Chat</string>
<string name="pref_only_fetch_mvbox_title">Agafa només de la carpeta de DeltaChat</string>
<string name="pref_show_emails">Mostra els correus clàssics</string>
@@ -789,7 +768,6 @@
<string name="pref_show_emails_accepted_contacts">Per a contactes acceptats</string>
<string name="pref_show_emails_all">Tot</string>
<string name="pref_experimental_features">Funcionalitats experimentals</string>
<string name="pref_experimental_features_explain">Aquestes funcionalitats poden ser inestables i poden canviar o ser suprimides</string>
<string name="pref_on_demand_location_streaming">Difusió de la ubicació sota demanda</string>
<string name="pref_background_default">Imatge per defecte</string>
<string name="pref_background_default_color">Color per defecte</string>
@@ -806,7 +784,6 @@
<string name="n_bytes_message">%1$s missatge</string>
<!-- %1$s will be replaced by human-readable date and time -->
<string name="download_max_available_until">Baixa el màxim disponible fins a %1$s</string>
<!-- deprecated -->
<string name="select_profile">Trieu Perfil</string>
<string name="profile_image_select">Trieu la imatge de perfil</string>
<string name="select_your_new_profile_image">Trieu la imatge nova de perfil</string>
@@ -816,14 +793,6 @@
<string name="disable_imap_idle">Desactiva IMAP IDLE</string>
<string name="disable_imap_idle_explain">No usis l\'extensió IMAP IDLE encara que el servidor la permeti. Activar aquesta opció endarreria l\'obtenció de missatges, activeu-ho només per a proves.</string>
<string name="send_stats_to_devs">Envia estadístiques als desenvolupadors del Delta Chat</string>
<string name="stats_device_message">Voleu ajudar a millorar Delta Chat i donar suport a la recerca enviant estadístiques d\'ús anònimes setmanalment?\n\n👉 Feu clic aquí… 👈</string>
<string name="stats_confirmation_dialog">Voleu ajudar a millorar Delta Chat i donar suport a la recerca enviant estadístiques d\'ús anònimes setmanalment?</string>
<string name="stats_thanks">Moltes gràcies! Sempre podeu desactivar l\'enviament a «Preferències -> Avançat».\n\nTeniu 5 minuts addicionals per a participar en un estudi científic sobre Delta Chat?</string>
<string name="stats_disable_dialog">L\'enviament d\'estadístiques ja està activat.\n\nVoleu desactivar-lo?</string>
<string name="disable">Desactiva</string>
<string name="stats_keep_sending">Continua enviant</string>
<string name="stats_msg_body">El fitxer adjunt conté estadístiques d\'ús anònimes que ens ajuden a millorar el Delta Chat. Veieu https://delta.chat/help#statssending per a més informació. Gràcies!</string>
<!-- Emoji picker and categories -->
<string name="emoji_search_results">Resultats de la cerca</string>
<string name="emoji_not_found">No s\'ha trobat cap emoji</string>
@@ -942,7 +911,6 @@
<string name="ephemeral_timer_weeks_by_you">Heu definit el temporitzador dels missatges fonedissos a %1$s setmanes.</string>
<!-- %1$s will be replaced by the number of weeks (always >1) the timer is set to, %2$s will be replaced by name of the contact -->
<string name="ephemeral_timer_weeks_by_other">%2$s ha definit el temporitzador dels missatges efímers a %1$s setmanes.</string>
<string name="chat_unencrypted_explanation">Els missatges en aquest xat fan servir el correu electrònic tradicional i no estan xifrats d\'extrem a extrem.</string>
<string name="chat_protection_enabled_tap_to_learn_more">Us garantim que a partir d\'ara els missatges són xifrats d\'extrem a extrem. Toqueu per a obtenir més informació.</string>
<string name="chat_protection_enabled_explanation">Ara es garanteix que tots els missatges d\'aquest xat són xifrats d\'extrem a extrem.\n\nEl xifratge d\'extrem a extrem manté privats els missatges entre vós i els companys de xat. Ni tan sols el vostre proveïdor de correu electrònic pot llegir-los.</string>
<string name="invalid_unencrypted_tap_to_learn_more">%1$srequereix xifratge d\'extrem a extrem, que encara no s\'ha configurat per a aquest xat. Toqueu per a obtenir més informació.</string>
@@ -967,7 +935,6 @@
<string name="qrscan_hint_desktop">Mou el codi QR a la càmera</string>
<string name="qrscan_failed">No s\'ha pogut descodificar el codi QR</string>
<string name="qrscan_ask_join_group">Voleu unir-vos al grup «%1$s»?</string>
<string name="qrscan_ask_join_channel">Voleu unir-vos al canal \"%1$s\"?</string>
<string name="qrscan_fingerprint_mismatch">L\'empremta escanejada no coincideix amb l\'última empremta de %1$s</string>
<string name="qrscan_no_addr_found">Aquest codi QR conté una empremta digital, però no hi ha cap adreça de correu electrònic.\n\nPer a una verificació fora de banda, establiu primer una connexió xifrada amb el destinatari.</string>
<string name="qrscan_contains_text">Text del codi QR escanejat:\n\n%1$s</string>
@@ -975,7 +942,6 @@
<string name="qrscan_fingerprint_label">Empremta</string>
<string name="withdraw_verifycontact_explain">Altres persones que vulguin contactar amb vós poden escanejar aquest codi QR.\n\nPodeu reinicialitzar-lo, de manera que el codi QR o enllaç actual existent ja no servirà.</string>
<string name="withdraw_verifygroup_explain">Altres poden escanejar aquest codi QR per a unir-se al grup «%1$s».\n\nPodeu reinicialitzar-lo, de manera que el codi QR existent, o l\'enllaç d\'invitació, deixarà de funcionar.</string>
<string name="withdraw_joinbroadcast_explain">Altres poden escanejar aquest codi QR per a unir-se al canal «%1$s».\n\nPodeu reinicialitzar-lo, de manera que el codi QR existent, o l\'enllaç d\'invitació, deixarà de funcionar.</string>
<string name="withdraw_qr_code">Reinicialitza el codi QR</string>
<string name="revive_verifycontact_explain">Aquest codi QR s\'ha reinicialitzat i ja no és actiu.</string>
<string name="revive_qr_code">Activa el codi QR</string>
@@ -999,13 +965,10 @@
<string name="qrlogin_ask_login">Voleu iniciar sessió a «%1$s»?</string>
<!-- first placeholder will be replaced by name of the inviter, second placeholder will be replaced by the name of the inviter. -->
<string name="secure_join_started">%1$s us ha convidat a unir-vos a aquest grup.\n\nS\'està esperant el dispositiu de %2$s per a respondre...</string>
<!-- first placeholder will be replaced by name of the inviter, second placeholder will be replaced by the name of the inviter. -->
<string name="secure_join_channel_started">%1$s us ha convidat a unir-vos a aquest canal.\n\nS\'està esperant que el dispositiu de %2$s respongui.</string>
<!-- placeholder will be replaced by the name of the inviter. -->
<string name="secure_join_replies">%1$s ha respost, espereu per a ser afegit al grup...</string>
<string name="secure_join_wait">S\'està establint el xifratge d\'extrem a extrem garantit, espereu...</string>
<string name="contact_verified">%1$s verificat.</string>
<string name="contact_blocked">Heu blocat el contacte</string>
<!-- Shown in contact profile. The placeholder will be replaced by the name of the contact that introduced the contact. -->
<string name="verified_by">Afegit per %1$s</string>
<string name="verified_by_you">Afegit per mi</string>
-3
View File
@@ -31,7 +31,6 @@
<string name="archive">ئەرشیڤ کردن</string>
<!-- Verb "to unarchive", as in "remove a chat from the archive", opposite of the previous string -->
<string name="unarchive">دەرهێنان لە ئەرشیڤ</string>
<!-- Verb "to mute". used to mute a chat, so that it is silenced e.g. does not make noise or cause notifications -->
<string name="mute">بێدەنگ کردن</string>
<string name="ephemeral_messages">ئەو پەیامانەی بزر دەبن</string>
<string name="ephemeral_messages_hint">ئەم ڕێکخستنانە بۆ هەموو بەکارهێنەرانی دێڵتا چات دەبن. ئەگەرچی ئەوان دەتوانن پەیامەکان کۆپی بکەن، پاشەکەوتیان بکەن و بڵاویان بکەنەوە یان لە پۆستێکی ئەلیکترۆنی دیکە کەڵک بگرن.</string>
@@ -105,7 +104,6 @@
<string name="reply_noun">وەڵام دانەوە</string>
<string name="gif">گیف</string>
<string name="images">وێنەکان</string>
<!-- a noun, used for "Music" and other "Audio" files -->
<string name="audio">دەنگ</string>
<string name="voice_message">پەیامی دەنگی</string>
<string name="forwarded_message">پەیامی هاوبەشکراو</string>
@@ -348,7 +346,6 @@
<string name="pref_your_name">ناو</string>
<!-- Label of the Bio/Signature/Status/Motto field -->
<string name="pref_default_status_label">دەقی واژۆ</string>
<!-- Option name for changing behaviour of the "Enter key" to sending out a message directly (instead of adding new line). If in doubt, please refer to a similar option in other messengers. -->
<string name="pref_enter_sends">کلیلی \"ئینتەر\" بنێرێت</string>
<string name="pref_enter_sends_explain">دەستنان بە کلیلی \"ئینتەر\"دا، پەیامەکان دەنێرێت.</string>
<string name="pref_outgoing_media_quality">چۆنێتی ڕەنگاڵە هەناردنییەکان</string>
-8
View File
@@ -51,9 +51,7 @@
<string name="archive">Archivovat</string>
<!-- Verb "to unarchive", as in "remove a chat from the archive", opposite of the previous string -->
<string name="unarchive">Vyjmout z archivu</string>
<!-- Verb "to mute". used to mute a chat, so that it is silenced e.g. does not make noise or cause notifications -->
<string name="mute">Ztlumit</string>
<!-- Adjective "smth is muted". used to describe a muted or silenced chat, that e.g. does not make noise or cause notifications -->
<string name="muted">Ztlumeno</string>
<string name="ephemeral_messages">Mizející zprávy</string>
<string name="ephemeral_messages_hint">Volba platí pro všechny členy tohoto chatu, kteří používají Delta Chat; stále ale mohou kopírovat, ukládat a přeposílat zprávy nebo používat jiné e-mailové klienty, které tuto volbu nerespektují.</string>
@@ -203,7 +201,6 @@
<string name="add_stickers_instructions">Přejete-li si přidat nálepky, klepněte na „Otevřít složku nálepek“, vytvořte podsložku pro balíček nálepek a přetáhněte do ní soubory obrázků a nálepek</string>
<string name="open_sticker_folder">Otevřít složku nálepek</string>
<string name="images">Obrázky</string>
<!-- a noun, used for "Music" and other "Audio" files -->
<string name="audio">Zvukové záznamy</string>
<string name="voice_message">Hlasová zpráva</string>
<string name="forwarded">Přeposlané</string>
@@ -240,7 +237,6 @@
<string name="games">Hry</string>
<string name="tools">Nástroje</string>
<string name="app_size">Velikost</string>
<!-- Used as a headline for a date. E.g. in the details of a mini app in the app picker -->
<string name="app_date_published">Zveřejněno</string>
<string name="add_to_chat">Přidat do chatu</string>
<!-- short for "Browse through the App Picker/Store/Catalogue"; could also be translated as "Discover" or "Search" -->
@@ -412,9 +408,7 @@
<string name="answer_call">Zvedni</string>
<!-- the action "to decline" a call, not: "the decline" -->
<string name="end_call">Pokles</string>
<!-- deprecated, use outgoing_audio_call or outgoing_video_call instead -->
<string name="outgoing_call">Odchozí hovor</string>
<!-- deprecated, use incoming_audio_call or incoming_video_call instead -->
<string name="incoming_call">Příchozí hovor</string>
<string name="declined_call">Odmítnutý hovor</string>
<string name="canceled_call">Zrušený hovor</string>
@@ -756,7 +750,6 @@
<string name="pref_your_name">Vaše jméno</string>
<!-- Label of the Bio/Signature/Status/Motto field -->
<string name="pref_default_status_label">Bio</string>
<!-- Option name for changing behaviour of the "Enter key" to sending out a message directly (instead of adding new line). If in doubt, please refer to a similar option in other messengers. -->
<string name="pref_enter_sends">Enter odesílá</string>
<string name="pref_enter_sends_explain">Stisk klávesy Enter odešle zprávu</string>
<string name="pref_outgoing_media_quality">Kvalita odesílaných multimédií</string>
@@ -845,7 +838,6 @@
<string name="n_bytes_message">%1$s zpráva</string>
<!-- %1$s will be replaced by human-readable date and time -->
<string name="download_max_available_until">Stahovat dostupné maximum do %1$s</string>
<!-- deprecated -->
<string name="select_profile">Vyberte profil</string>
<string name="profile_image_select">Vybrat profilový obrázek</string>
<string name="select_your_new_profile_image">Vyberte si svůj nový profilový obrázek</string>
-3
View File
@@ -35,7 +35,6 @@
<string name="archive">Arkivér</string>
<!-- Verb "to unarchive", as in "remove a chat from the archive", opposite of the previous string -->
<string name="unarchive">Fortryd arkivering</string>
<!-- Verb "to mute". used to mute a chat, so that it is silenced e.g. does not make noise or cause notifications -->
<string name="mute">Udsæt</string>
<string name="ephemeral_messages">Beskeder med tidsudløb</string>
<string name="ephemeral_messages_hint">Disse indstillinger gælder for alle deltagere der bruger Delta Chat. De kan dog kopiere, gemme og videresende beskeder eller bruge andre e-mail klienter. </string>
@@ -115,7 +114,6 @@
<!-- "Stickers" as known from other messengers; in some languages, the English "Sticker" is fine. -->
<string name="sticker">Klistermærker</string>
<string name="images">Billeder</string>
<!-- a noun, used for "Music" and other "Audio" files -->
<string name="audio">Lyd</string>
<string name="voice_message">Tale besked</string>
<string name="forwarded">Videresendt</string>
@@ -403,7 +401,6 @@
<string name="pref_your_name">Dit navn</string>
<!-- Label of the Bio/Signature/Status/Motto field -->
<string name="pref_default_status_label">Underskrift tekst</string>
<!-- Option name for changing behaviour of the "Enter key" to sending out a message directly (instead of adding new line). If in doubt, please refer to a similar option in other messengers. -->
<string name="pref_enter_sends">Enter tast sender</string>
<string name="pref_enter_sends_explain">Tryk på enter tast vil sende tekst beskeden</string>
<string name="pref_outgoing_media_quality">Udgående medie kvalitet</string>

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