Merge remote-tracking branch 'upstream/main'

This commit is contained in:
adbenitez
2025-10-09 15:34:30 +02:00
139 changed files with 2675 additions and 2516 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ on:
jobs:
artifacts-url-comments:
name: add artifact links to pull request
runs-on: windows-2019
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: add artifact links to pull request
+10 -10
View File
@@ -2,16 +2,16 @@ name: Upload Preview APK
on: pull_request
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
build:
name: Upload Preview APK
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.9.1
with:
access_token: ${{ github.token }}
- uses: actions/checkout@v3
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Validate Fastlane Metadata
@@ -19,12 +19,12 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
working-directory: jni/deltachat-core-rust
- uses: actions/setup-java@v3
- uses: actions/setup-java@v5
with:
java-version: 17
distribution: 'temurin'
- uses: android-actions/setup-android@v3
- uses: actions/cache@v3
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
@@ -37,6 +37,9 @@ jobs:
with:
ndk-version: r27
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v4
- name: Compile core
env:
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
@@ -44,9 +47,6 @@ jobs:
export PATH="${PATH}:${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/"
scripts/install-toolchains.sh && scripts/ndk-make.sh armeabi-v7a
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
- name: Build APK
run: ./gradlew --no-daemon -PABI_FILTER=armeabi-v7a assembleFossDebug
+12
View File
@@ -4,7 +4,19 @@
* target Android 15
* improve readability of info messages in dark mode
* drop too short disappearing messages options
* fix Direct Share shortcuts
* fix: don't show error message when cancelling profile creation
* enable permanent notification by default if push notifications are not available
* hide "clone chat" and member list for incoming channels
* show warning if background notifications will be unreliable
* warn if the app has not been updated after 6 months instead of 1 year
* avoid "unknown sender for this chat" error
* properly display "Messages are end-to-end encrypted." in all encrypted groups
* show dialog if user has permanently denied camera permission and tries to take picture for group avatar
* several small fixes and improvements
* add experimental built-in calls
* update to core 2.17.0
## v2.11.0
2025-08
-1
View File
@@ -233,7 +233,6 @@ dependencies {
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
implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.1' // used as JSON library
implementation 'com.google.code.gson:gson:2.12.1' // used as JSON library.
implementation 'com.github.Baseflow:PhotoView:2.3.0' // does the zooming on photos / media
implementation 'com.github.penfeizhou.android.animation:awebp:3.0.5' // animated webp support.
implementation 'com.caverock:androidsvg-aar:1.4' // SVG support.
Generated
+15 -15
View File
@@ -7,11 +7,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1733948466,
"narHash": "sha256-o/uq/tU458Ykudi8Zk3sRga5iazkuSczt9wDOCUDOSU=",
"lastModified": 1756239746,
"narHash": "sha256-0ibN685tT+u/Nbmbrrq9G3mRUzct2Votyv/a7Wwv26s=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "0bf99ffaea6a7c0948ae10cf2e40c2905e4e4d6b",
"rev": "256631d162ec883b2341ee59621516e1f65f0f6b",
"type": "github"
},
"original": {
@@ -28,11 +28,11 @@
]
},
"locked": {
"lastModified": 1728330715,
"narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=",
"lastModified": 1741473158,
"narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=",
"owner": "numtide",
"repo": "devshell",
"rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef",
"rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0",
"type": "github"
},
"original": {
@@ -79,11 +79,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1733759999,
"narHash": "sha256-463SNPWmz46iLzJKRzO3Q2b0Aurff3U1n0nYItxq7jU=",
"lastModified": 1756125398,
"narHash": "sha256-XexyKZpf46cMiO5Vbj+dWSAXOnr285GHsMch8FBoHbc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a73246e2eef4c6ed172979932bc80e1404ba2d56",
"rev": "3b9f00d7a7bf68acd4c4abb9d43695afb04e03a5",
"type": "github"
},
"original": {
@@ -95,11 +95,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1733749988,
"narHash": "sha256-+5qdtgXceqhK5ZR1YbP1fAUsweBIrhL38726oIEAtDs=",
"lastModified": 1756159630,
"narHash": "sha256-ohMvsjtSVdT/bruXf5ClBh8ZYXRmD4krmjKrXhEvwMg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "bc27f0fde01ce4e1bfec1ab122d72b7380278e68",
"rev": "84c256e42600cb0fdf25763b48d28df2f25a0c8b",
"type": "github"
},
"original": {
@@ -138,11 +138,11 @@
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1747017456,
"narHash": "sha256-C/U12fcO+HEF071b5mK65lt4XtAIZyJSSJAg9hdlvTk=",
"lastModified": 1756197489,
"narHash": "sha256-S16rPaBH1TnMbDyL5NlGSJcYd7wPlOEWTStdBDL7BHw=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "5b07506ae89b025b14de91f697eba23b48654c52",
"rev": "8ec04f46f1edeeed3f870da62191745b93975da7",
"type": "github"
},
"original": {
+1 -1
View File
@@ -18,7 +18,7 @@
build-tools-35-0-0
cmdline-tools-latest
platform-tools
platforms-android-34
platforms-android-35
ndk-27-2-12479018
]);
rust-version = pkgs.lib.removeSuffix "\n"
-21
View File
@@ -735,12 +735,6 @@ JNIEXPORT jint Java_com_b44t_messenger_DcContext_sendTextMsg(JNIEnv *env, jobjec
}
JNIEXPORT jint Java_com_b44t_messenger_DcContext_sendVideochatInvitation(JNIEnv *env, jobject obj, jint chat_id)
{
return (jint)dc_send_videochat_invitation(get_dc_context(env, obj), chat_id);
}
JNIEXPORT jboolean Java_com_b44t_messenger_DcContext_sendWebxdcStatusUpdate(JNIEnv *env, jobject obj, jint msg_id, jstring payload)
{
CHAR_REF(payload);
@@ -1608,21 +1602,6 @@ JNIEXPORT jstring Java_com_b44t_messenger_DcMsg_getSetupCodeBegin(JNIEnv *env, j
}
JNIEXPORT jstring Java_com_b44t_messenger_DcMsg_getVideochatUrl(JNIEnv *env, jobject obj)
{
char* temp = dc_msg_get_videochat_url(get_dc_msg(env, obj));
jstring ret = JSTRING_NEW(temp);
dc_str_unref(temp);
return ret;
}
JNIEXPORT jint Java_com_b44t_messenger_DcMsg_getVideochatType(JNIEnv *env, jobject obj)
{
return (jint)dc_msg_get_videochat_type(get_dc_msg(env, obj));
}
JNIEXPORT void Java_com_b44t_messenger_DcMsg_setSubject(JNIEnv *env, jobject obj, jstring text)
{
CHAR_REF(text);
+5 -9
View File
@@ -1,15 +1,11 @@
# native methods
-keep class com.b44t.messenger.** { * ; }
# Gson uses generic type information stored in a class file when working with
# fields. Proguard removes such information by default, keep it.
-keepattributes Signature
# This is also needed for R8 in compat mode since multiple
# optimizations will remove the generic signature such as class
# merging and argument removal. See:
# https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#troubleshooting-gson-gson
-keep class com.google.gson.reflect.TypeToken { *; }
-keep class * extends com.google.gson.reflect.TypeToken
# Keep metadata needed by the JSON parser
-keep class chat.delta.rpc.** { * ; }
-keepattributes *Annotation*,EnclosingMethod,Signature
-keepnames class com.fasterxml.jackson.** { *; }
#-dontwarn com.fasterxml.jackson.databind.**
# bug with video recoder
-keep class com.coremedia.iso.** { *; }
+9 -1
View File
@@ -370,7 +370,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
</activity>
<activity android:name=".videochat.VideochatActivity"
<activity android:name=".calls.CallActivity"
android:label=""
android:theme="@style/TextSecure.LightTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
@@ -436,6 +436,14 @@
</intent-filter>
</receiver>
<receiver android:name=".notifications.DeclineCallReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.notifications.DECLINE_CALL_NOTICED"/>
</intent-filter>
</receiver>
<receiver android:name=".notifications.RemoteReplyReceiver"
android:enabled="true"
android:exported="false">
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -784,8 +784,8 @@ there are no lock or similar markers on end-to-end encrypted messages, anymore.<
it is impossible to receive or send messages without end-to-end encryption.</p>
<p>If you instead create a profile using a classic e-mail server,
you can send and receive messages without end-to-end encryption.
Such messages lacking end-to-end encryption are marked with an e-mail icon
you can send and receive messages with or without end-to-end encryption.
Messages lacking end-to-end encryption are marked with an e-mail icon
<img style="vertical-align:middle; width:1.2em; margin:1px" src="../email-icon.png" alt="email" />.</p>
<h3 id="howtoe2ee">
+53 -53
View File
@@ -50,10 +50,10 @@
<li><a href="#tls">I messaggi contrassegnati dallicona della posta sono esposti su Internet?</a></li>
<li><a href="#message-metadata">In che modo Delta Chat protegge i metadati nei messaggi?</a></li>
<li><a href="#device-seizure">Come proteggere i metadati e contatti quando un dispositivo viene sequestrato?</a></li>
<li><a href="#sealedsender">Does Delta Chat support “Sealed Sender”?</a></li>
<li><a href="#sealedsender">Delta Chat supportaMittente Sigillato”?</a></li>
<li><a href="#pfs">Delta Chat supporta Perfect Forward Secrecy?</a></li>
<li><a href="#pqc">Does Delta Chat support Post-Quantum-Cryptography?</a></li>
<li><a href="#how-can-i-manually-check-encryption-information">How can I manually check encryption information?</a></li>
<li><a href="#pqc">Delta Chat supporta la Crittografia Post-Quantistica?</a></li>
<li><a href="#come-posso-controllare-manualmente-le-informazioni-di-crittografia">Come posso controllare manualmente le informazioni di crittografia?</a></li>
<li><a href="#importkey">Posso riutilizzare la mia chiave privata esistente?</a></li>
<li><a href="#security-audits">Delta Chat è stata verificata in modo indipendente per le vulnerabilità di sicurezza?</a></li>
</ul>
@@ -969,25 +969,25 @@ anche se il messaggio non sarà crittografato end-to-end.</p>
</h3>
<p>Unlike most other messengers,
Delta Chat apps do not store any metadata about contacts or groups on servers, also not in encrypted form.
Instead, all group metadata is end-to-end encrypted and stored on end-user devices, only.</p>
<p>A differenza della maggior parte degli altri servizi di messaggistica,
le apps Delta Chat non memorizzano alcun metadato sui contatti o sui gruppi sui server, né in forma crittografata.
Tutti i metadati dei gruppi sono invece crittografati end-to-end e memorizzati esclusivamente sui dispositivi degli utenti finali.</p>
<p>E-mail Servers can therefore only see</p>
<p>I Server E-mail possono quindi vedere solo</p>
<ul>
<li>
<p>the message date,</p>
<p>la data del messaggio,</p>
</li>
<li>
<p>sender and receiver addresses</p>
<p>indirizzi del mittente e del destinatario</p>
</li>
<li>
<p>and message size.</p>
<p>e dimensione del messaggio.</p>
</li>
</ul>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<p>Tutti gli altri metadati dei messaggi, dei contatti e dei gruppi risiedono nella parte crittografata end-to-end dei messaggi.</p>
<h3 id="device-seizure">
@@ -997,36 +997,36 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
</h3>
<p>Both for protecting against metadata-collecting e-mail 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 e-mail 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>
<p>Sia per proteggersi dai server e-mail che raccolgono metadati
sia per proteggersi dal rischio di sequestro del dispositivo,
consigliamo di utilizzare un <a href="https://chatmail.at/relays">chatmail relay</a>
per creare profili di chat utilizzando indirizzi e-mail casuali per il trasporto.
Si noti che le apps Delta Chat su tutte le piattaforme supportano più profili,
quindi è possibile utilizzare facilmente profili specifici per ogni situazione accanto al profilo “principale”,
con la consapevolezza che tutti i loro dati, insieme a tutti i metadati, verranno eliminati.
Inoltre, se un dispositivo viene sequestrato, i contatti di chat che utilizzano profili di breve durata
non possono essere identificati facilmente.</p>
<h3 id="sealedsender">
Does Delta Chat support “Sealed Sender”? <a href="#sealedsender" class="anchor"></a>
Delta Chat supportaMittente Sigillato”? <a href="#sealedsender" class="anchor"></a>
</h3>
<p>No, non ancora.</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.
It is particularly important because the Signal server knows the mobile number of each account,
which is usually associated with a passport identity.</p>
<p>Il messenger Signal ha introdotto <a href="https://signal.org/blog/sealed-sender/">Mittente Sigillato” nel 2018</a>
per impedire che la propria infrastruttura server venga a conoscenza di chi sta inviando un messaggio a un insieme di destinatari.
È particolarmente importante perché il server Signal conosce il numero di cellulare di ciascun profilo,
che di solito è associato a unidentità tramite passaporto.</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 e-mail addresses for sealed sending
but an implementation has not been agreed as a priority yet.</p>
<p>Anche se i <a href="https://chatmail.at/relays">relays di chat</a>
non richiedono dati privati (inclusi numeri di telefono),
potrebbe comunque valere la pena proteggere i metadati relazionali tra gli indirizzi.
Non prevediamo problemi maggiori nellutilizzo di indirizzi e-mail casuali usa e getta per linvio sigillato,
ma unimplementazione non è stata ancora concordata come priorità.</p>
<h3 id="pfs">
@@ -1038,41 +1038,41 @@ but an implementation has not been agreed as a priority yet.</p>
<p>No, non ancora.</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>Delta Chat al momento non supporta la tecnologia Perfect Forward Secrecy (PFS).
Ciò significa che se la tua chiave di decrittazione privata viene divulgata
e qualcuno ha raccolto i tuoi messaggi in transito precedenti,
sarà in grado di decifrarli e leggerli utilizzando la chiave di decrittazione divulgata.
Tieni presente che la tecnologia Forward Secrecy aumenta la sicurezza solo se elimini i messaggi.
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>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>
<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">
Does Delta Chat support Post-Quantum-Cryptography? <a href="#pqc" class="anchor"></a>
Delta Chat supporta la Crittografia Post-Quantistica? <a href="#pqc" class="anchor"></a>
</h3>
<p>No, non ancora.</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>
<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>.
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="how-can-i-manually-check-encryption-information">
<h3 id="come-posso-controllare-manualmente-le-informazioni-di-crittografia">
How can I manually check encryption information? <a href="#how-can-i-manually-check-encryption-information" class="anchor"></a>
Come posso controllare manualmente le informazioni di crittografia? <a href="#come-posso-controllare-manualmente-le-informazioni-di-crittografia" class="anchor"></a>
</h3>
@@ -1096,8 +1096,8 @@ la connessione è sicura.</p>
<p>Delta Chat genera chiavi OpenPGP sicure in conformità alle specifiche di Autocrypt 1.1.
Puoi esportare le tue chiavi private ma non puoi importare chiavi private aggiuntive.</p>
<p>In generale, non raccomandiamo on proponiamo agli utenti di gestire manualmente le chiavi.
Noi vogliamo assicurare che gli audit di sicurezza si possano focalizzare su pochi algoritmi crittografici comprovati
<p>In generale, non raccomandiamo o proponiamo agli utenti di gestire manualmente le chiavi.
Noi vogliamo assicurare che gli audits di sicurezza si possano focalizzare su pochi algoritmi crittografici comprovati
invece che sullintera estensione dei possibili algoritmi consentiti con OpenPGP.</p>
<h3 id="security-audits">
+5 -5
View File
@@ -606,7 +606,7 @@ Push-сповіщення автоматично активуються для
</h3>
<p>Delta Chat використовує [безпечну підмножину стандарту OpenPGP] (#openpgp-secure) для забезпечення автоматичного наскрізного шифрування за допомогою цих протоколів:</p>
<p>Delta Chat використовує <a href="#openpgp-secure">безпечну підмножину стандарту OpenPGP</a> для забезпечення автоматичного наскрізного шифрування за допомогою цих протоколів:</p>
<ul>
<li>
@@ -616,7 +616,7 @@ Push-сповіщення автоматично активуються для
<p><a href="https://autocrypt.org">Autocrypt</a> використовується для автоматичного встановлення наскрізного шифрування між контактами і всіма учасниками групового чату.</p>
</li>
<li>
<p>[Поширення контакту в чаті] (https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message) дозволяє отримувачам використовувати наскрізне шифрування з контактом.</p>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Поширення контакту в чаті</a> дозволяє отримувачам використовувати наскрізне шифрування з контактом.</p>
</li>
</ul>
@@ -687,7 +687,7 @@ Push-сповіщення автоматично активуються для
</h3>
<p>У профілі контакту може відображатися зелена галочка <img style="vertical-align:middle; width:1.5em; margin:1px" src="../green-checkmark.png" alt="green checkmark" /> і рядок “Представлений”. Кожен контакт із зеленою галочкою або зробив пряме [QR-сканування] (#howtoe2ee) з вами або був представлений іншим контактом, позначеним зеленою галочкою. Знайомство відбувається автоматично під час додавання учасників до груп. Той, хто додає контакт із зеленою галочкою до групи, в якій є лише учасники із зеленою галочкою стає представником. У профілі контакту ви можете кілька разів натиснути на текст “Представлений …” поки не потрапите до того, з ким ви безпосередньо зробили [QR-сканування] (#howtoe2ee).</p>
<p>У профілі контакту може відображатися зелена галочка <img style="vertical-align:middle; width:1.5em; margin:1px" src="../green-checkmark.png" alt="green checkmark" /> і рядок “Представлений”. Кожен контакт із зеленою галочкою або зробив пряме <a href="#howtoe2ee">QR-сканування</a> з вами або був представлений іншим контактом, позначеним зеленою галочкою. Знайомство відбувається автоматично під час додавання учасників до груп. Той, хто додає контакт із зеленою галочкою до групи, в якій є лише учасники із зеленою галочкою стає представником. У профілі контакту ви можете кілька разів натиснути на текст “Представлений …” поки не потрапите до того, з ким ви безпосередньо зробили <a href="#howtoe2ee">QR-сканування</a>.</p>
<p>Для більш детального обговорення “гарантованого наскрізного шифрування” будь ласка, перегляньте <a href="https://securejoin.delta.chat/en/latest/new.html">Протоколи безпечного приєднання</a> і, зокрема, прочитайте про “Перевірені групи”, технічний термін того, що тут називається чатами з “зеленою галочкою” або “гарантованим наскрізним шифруванням”.</p>
@@ -802,7 +802,7 @@ Delta Chat скоріше використовує реалізацію OpenPGP
<p>Ні, поки ще ні.</p>
<p>Месенджер Signal запровадив [“Запечатаного відправника” у 2018 році] (https://signal.org/blog/sealed-sender/) щоб їхня серверна інфраструктура не знала, хто надсилає повідомлення певній групі одержувачів. Це особливо важливо, оскільки сервер Signal знає номер мобільного телефону кожного акаунта, який зазвичай асоціюється з паспортними даними.</p>
<p>Месенджер Signal запровадив <a href="https://signal.org/blog/sealed-sender/">“Запечатаного відправника” у 2018 році</a> щоб їхня серверна інфраструктура не знала, хто надсилає повідомлення певній групі одержувачів. Це особливо важливо, оскільки сервер Signal знає номер мобільного телефону кожного акаунта, який зазвичай асоціюється з паспортними даними.</p>
<p>Навіть якщо <a href="https://chatmail.at/relays">чат-мейл релей</a> не запитує ніяких приватних даних (в тому числі номерів телефонів), все одно може мати сенс захистити реляційні метадані між адресами. Ми не передбачаємо великих проблем у використанні випадкових одноразових адрес електронної пошти для запечатаних відправлень але реалізація такої можливості ще не є пріоритетною.</p>
@@ -1265,7 +1265,7 @@ Delta Chat скоріше використовує реалізацію OpenPGP
</h3>
<ul>
<li>Більшість поштових серверів будуть працювати добре. Але ми особисто рекомендуємо використовувати сервер ретрансляції чатової пошти, як описано [в цій статті] (https://delta.chat/en/2023-12-13-chatmail).</li>
<li>Більшість поштових серверів будуть працювати добре. Але ми особисто рекомендуємо використовувати сервер ретрансляції чатової пошти, як описано <a href="https://delta.chat/en/2023-12-13-chatmail">в цій статті</a>.</li>
<li>Ви можете знайти <a href="https://github.com/chatmail/relay">посібник зі встановлення на GitHub</a>.</li>
</ul>
@@ -0,0 +1,124 @@
/* Autogenerated file, do not edit manually */
package chat.delta.rpc;
import chat.delta.util.SettableFuture;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
/* Basic RPC Transport implementation */
public abstract class BaseTransport implements Rpc.Transport {
private final Map<Integer, SettableFuture<JsonNode>> requestFutures = new ConcurrentHashMap<>();
private int requestId = 0;
private final ObjectMapper mapper = new ObjectMapper();
private Thread worker;
/* Send a Request as raw JSON String to the RPC server */
protected abstract void sendRequest(String jsonRequest);
/* Get next Response as raw JSON String from the RPC server */
protected abstract String getResponse();
public ObjectMapper getObjectMapper() {
return mapper;
}
public void call(String method, JsonNode... params) throws RpcException {
innerCall(method, params);
}
public <T> T callForResult(TypeReference<T> resultType, String method, JsonNode... params) throws RpcException {
try {
JsonNode node = innerCall(method, params);
if (node.isNull()) return null;
return mapper.readValue(node.traverse(), resultType);
} catch (IOException e) {
throw new RpcException(e.getMessage());
}
}
private JsonNode innerCall(String method, JsonNode... params) throws RpcException {
int id;
synchronized (this) {
id = ++requestId;
ensureWorkerThread();
}
try {
String jsonRequest = mapper.writeValueAsString(new Request(method, params, id));
SettableFuture<JsonNode> future = new SettableFuture<>();
requestFutures.put(id, future);
sendRequest(jsonRequest);
return future.get();
} catch (ExecutionException e) {
throw (RpcException)e.getCause();
} catch (InterruptedException e) {
throw new RpcException(e.getMessage());
} catch (JsonProcessingException e) {
throw new RpcException(e.getMessage());
}
}
private void ensureWorkerThread() {
if (worker != null) return;
worker = new Thread(() -> {
while (true) {
try {
processResponse();
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}, "jsonrpcThread");
worker.start();
}
private void processResponse() throws JsonProcessingException {
String jsonResponse = getResponse();
Response response = mapper.readValue(jsonResponse, Response.class);
if (response.id == 0) { // Got JSON-RPC notification/event, ignore
return;
}
SettableFuture<JsonNode> future = requestFutures.remove(response.id);
if (future == null) { // Got a response with unknown ID, ignore
return;
}
if (response.error != null) {
future.setException(new RpcException(response.error.toString()));
} else if (response.result != null) {
future.set(response.result);
} else {
future.setException(new RpcException("Got JSON-RPC response without result or error: " + jsonResponse));
}
}
private static class Request {
private final String jsonrpc = "2.0";
public final String method;
public final JsonNode[] params;
public final int id;
public Request(String method, JsonNode[] params, int id) {
this.method = method;
this.params = params;
this.id = id;
}
}
private static class Response {
public String jsonrpc;
public int id;
public JsonNode result;
public JsonNode error;
}
}
+249
View File
@@ -0,0 +1,249 @@
/* Autogenerated file, do not edit manually */
package chat.delta.rpc;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import chat.delta.rpc.types.*;
public class Rpc {
public interface Transport {
void call(String method, JsonNode... params) throws RpcException;
<T> T callForResult(TypeReference<T> resultType, String method, JsonNode... params) throws RpcException;
ObjectMapper getObjectMapper();
}
public final Transport transport;
private final ObjectMapper mapper;
public Rpc(Transport transport) {
this.transport = transport;
this.mapper = transport.getObjectMapper();
}
public Integer addAccount() throws RpcException {
return transport.callForResult(new TypeReference<Integer>(){}, "add_account");
}
/**
* Set the order of accounts.
* The provided list should contain all account IDs in the desired order.
* If an account ID is missing from the list, it will be appended at the end.
* If the list contains non-existent account IDs, they will be ignored.
*/
public void setAccountsOrder(java.util.List<Integer> order) throws RpcException {
transport.call("set_accounts_order", mapper.valueToTree(order));
}
/* Get the combined filesize of an account in bytes */
public Integer getAccountFileSize(Integer accountId) throws RpcException {
return transport.callForResult(new TypeReference<Integer>(){}, "get_account_file_size", mapper.valueToTree(accountId));
}
/**
* If there was an error while the account was opened
* and migrated to the current version,
* then this function returns it.
* <p>
* This function is useful because the key-contacts migration could fail due to bugs
* and then the account will not work properly.
* <p>
* After opening an account, the UI should call this function
* and show the error string if one is returned.
*/
public String getMigrationError(Integer accountId) throws RpcException {
return transport.callForResult(new TypeReference<String>(){}, "get_migration_error", mapper.valueToTree(accountId));
}
public Integer draftSelfReport(Integer accountId) throws RpcException {
return transport.callForResult(new TypeReference<Integer>(){}, "draft_self_report", mapper.valueToTree(accountId));
}
/* Returns configuration value for the given key. */
public String getConfig(Integer accountId, String key) throws RpcException {
return transport.callForResult(new TypeReference<String>(){}, "get_config", mapper.valueToTree(accountId), mapper.valueToTree(key));
}
/**
* Configures a new email account using the provided parameters
* and adds it as a transport.
* <p>
* If the email address is the same as an existing transport,
* then this existing account will be reconfigured instead of a new one being added.
* <p>
* This function stops and starts IO as needed.
* <p>
* Usually it will be enough to only set `addr` and `password`,
* and all the other settings will be autoconfigured.
* <p>
* During configuration, ConfigureProgress events are emitted;
* they indicate a successful configuration as well as errors
* and may be used to create a progress bar.
* This function will return after configuration is finished.
* <p>
* If configuration is successful,
* the working server parameters will be saved
* and used for connecting to the server.
* The parameters entered by the user will be saved separately
* so that they can be prefilled when the user opens the server-configuration screen again.
* <p>
* See also:
* - [Self::is_configured()] to check whether there is
* at least one working transport.
* - [Self::add_transport_from_qr()] to add a transport
* from a server encoded in a QR code.
* - [Self::list_transports()] to get a list of all configured transports.
* - [Self::delete_transport()] to remove a transport.
*/
public void addOrUpdateTransport(Integer accountId, EnteredLoginParam param) throws RpcException {
transport.call("add_or_update_transport", mapper.valueToTree(accountId), mapper.valueToTree(param));
}
/**
* Adds a new email account as a transport
* using the server encoded in the QR code.
* See [Self::add_or_update_transport].
*/
public void addTransportFromQr(Integer accountId, String qr) throws RpcException {
transport.call("add_transport_from_qr", mapper.valueToTree(accountId), mapper.valueToTree(qr));
}
/**
* Create a new unencrypted group chat.
* <p>
* Same as [`Self::create_group_chat`], but the chat is unencrypted and can only have
* address-contacts.
*/
public Integer createGroupChatUnencrypted(Integer accountId, String name) throws RpcException {
return transport.callForResult(new TypeReference<Integer>(){}, "create_group_chat_unencrypted", mapper.valueToTree(accountId), mapper.valueToTree(name));
}
/**
* Create a new **broadcast channel**
* (called "Channel" in the UI).
* <p>
* Broadcast channels are similar to groups on the sending device,
* however, recipients get the messages in a read-only chat
* and will not see who the other members are.
* <p>
* Called `broadcast` here rather than `channel`,
* because the word "channel" already appears a lot in the code,
* which would make it hard to grep for it.
* <p>
* After creation, the chat contains no recipients and is in _unpromoted_ state;
* see [`CommandApi::create_group_chat`] for more information on the unpromoted state.
* <p>
* Returns the created chat's id.
*/
public Integer createBroadcast(Integer accountId, String chatName) throws RpcException {
return transport.callForResult(new TypeReference<Integer>(){}, "create_broadcast", mapper.valueToTree(accountId), mapper.valueToTree(chatName));
}
/* Returns contact id of the created or existing DM chat with that contact */
public Integer createChatByContactId(Integer accountId, Integer contactId) throws RpcException {
return transport.callForResult(new TypeReference<Integer>(){}, "create_chat_by_contact_id", mapper.valueToTree(accountId), mapper.valueToTree(contactId));
}
/* Sets display name for existing contact. */
public void changeContactName(Integer accountId, Integer contactId, String name) throws RpcException {
transport.call("change_contact_name", mapper.valueToTree(accountId), mapper.valueToTree(contactId), mapper.valueToTree(name));
}
/* Parses a vCard file located at the given path. Returns contacts in their original order. */
public java.util.List<VcardContact> parseVcard(String path) throws RpcException {
return transport.callForResult(new TypeReference<java.util.List<VcardContact>>(){}, "parse_vcard", mapper.valueToTree(path));
}
/**
* Imports contacts from a vCard file located at the given path.
* <p>
* Returns the ids of created/modified contacts in the order they appear in the vCard.
*/
public java.util.List<Integer> importVcard(Integer accountId, String path) throws RpcException {
return transport.callForResult(new TypeReference<java.util.List<Integer>>(){}, "import_vcard", mapper.valueToTree(accountId), mapper.valueToTree(path));
}
/* Returns a vCard containing contacts with the given ids. */
public String makeVcard(Integer accountId, java.util.List<Integer> contacts) throws RpcException {
return transport.callForResult(new TypeReference<String>(){}, "make_vcard", mapper.valueToTree(accountId), mapper.valueToTree(contacts));
}
public void sendWebxdcRealtimeData(Integer accountId, Integer instanceMsgId, java.util.List<Integer> data) throws RpcException {
transport.call("send_webxdc_realtime_data", mapper.valueToTree(accountId), mapper.valueToTree(instanceMsgId), mapper.valueToTree(data));
}
public void sendWebxdcRealtimeAdvertisement(Integer accountId, Integer instanceMsgId) throws RpcException {
transport.call("send_webxdc_realtime_advertisement", mapper.valueToTree(accountId), mapper.valueToTree(instanceMsgId));
}
/**
* Leaves the gossip of the webxdc with the given message id.
* <p>
* NB: When this is called before closing a webxdc app in UIs, it must be guaranteed that
* `send_webxdc_realtime_*()` functions aren't called for the given `instance_message_id`
* anymore until the app is open again.
*/
public void leaveWebxdcRealtime(Integer accountId, Integer instanceMessageId) throws RpcException {
transport.call("leave_webxdc_realtime", mapper.valueToTree(accountId), mapper.valueToTree(instanceMessageId));
}
/* Starts an outgoing call. */
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. */
public void acceptIncomingCall(Integer accountId, Integer msgId, String acceptCallInfo) throws RpcException {
transport.call("accept_incoming_call", mapper.valueToTree(accountId), mapper.valueToTree(msgId), mapper.valueToTree(acceptCallInfo));
}
/* Ends incoming or outgoing call. */
public void endCall(Integer accountId, Integer msgId) throws RpcException {
transport.call("end_call", mapper.valueToTree(accountId), mapper.valueToTree(msgId));
}
/* Returns information about the call. */
public CallInfo callInfo(Integer accountId, Integer msgId) throws RpcException {
return transport.callForResult(new TypeReference<CallInfo>(){}, "call_info", mapper.valueToTree(accountId), mapper.valueToTree(msgId));
}
/* Returns JSON with ICE servers, to be used for WebRTC video calls. */
public String iceServers(Integer accountId) throws RpcException {
return transport.callForResult(new TypeReference<String>(){}, "ice_servers", mapper.valueToTree(accountId));
}
/**
* Makes an HTTP GET request and returns a response.
* <p>
* `url` is the HTTP or HTTPS URL.
*/
public HttpResponse getHttpResponse(Integer accountId, String url) throws RpcException {
return transport.callForResult(new TypeReference<HttpResponse>(){}, "get_http_response", mapper.valueToTree(accountId), mapper.valueToTree(url));
}
/**
* Send a reaction to message.
* <p>
* Reaction is a string of emojis separated by spaces. Reaction to a
* single message can be sent multiple times. The last reaction
* received overrides all previously received reactions. It is
* possible to remove all reactions by sending an empty string.
*/
public Integer sendReaction(Integer accountId, Integer messageId, java.util.List<String> reaction) throws RpcException {
return transport.callForResult(new TypeReference<Integer>(){}, "send_reaction", mapper.valueToTree(accountId), mapper.valueToTree(messageId), mapper.valueToTree(reaction));
}
/* Returns reactions to the message. */
public Reactions getMessageReactions(Integer accountId, Integer messageId) throws RpcException {
return transport.callForResult(new TypeReference<Reactions>(){}, "get_message_reactions", mapper.valueToTree(accountId), mapper.valueToTree(messageId));
}
/* Checks if messages can be sent to a given chat. */
public Boolean canSend(Integer accountId, Integer chatId) throws RpcException {
return transport.callForResult(new TypeReference<Boolean>(){}, "can_send", mapper.valueToTree(accountId), mapper.valueToTree(chatId));
}
}
@@ -0,0 +1,8 @@
/* Autogenerated file, do not edit manually */
package chat.delta.rpc;
public class RpcException extends Exception {
public RpcException(String message) { super(message); }
}
@@ -0,0 +1,19 @@
/* Autogenerated file, do not edit manually */
package chat.delta.rpc.types;
public class CallInfo {
/* True if SDP offer has a video. */
public Boolean hasVideo;
/**
* SDP offer.
* <p>
* Can be used to manually answer the call even if incoming call event was missed.
*/
public String sdpOffer;
/**
* Call state.
* <p>
* For example, if the call is accepted, active, canceled, declined etc.
*/
public CallState state;
}
@@ -0,0 +1,48 @@
/* Autogenerated file, do not edit manually */
package chat.delta.rpc.types;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
@JsonTypeInfo(use=Id.NAME, include=As.PROPERTY, property="kind")
@JsonSubTypes({@Type(value = CallState.Alerting.class, name="Alerting"), @Type(value = CallState.Active.class, name="Active"), @Type(value = CallState.Completed.class, name="Completed"), @Type(value = CallState.Missed.class, name="Missed"), @Type(value = CallState.Declined.class, name="Declined"), @Type(value = CallState.Canceled.class, name="Canceled")})
public abstract class CallState {
/**
* Fresh incoming or outgoing call that is still ringing.
* <p>
* There is no separate state for outgoing call that has been dialled but not ringing on the other side yet as we don't know whether the other side received our call.
*/
public static class Alerting extends CallState {
}
/* Active call. */
public static class Active extends CallState {
}
/* Completed call that was once active and then was terminated for any reason. */
public static class Completed extends CallState {
/* Call duration in seconds. */
public Integer duration;
}
/* Incoming call that was not picked up within a timeout or was explicitly ended by the caller before we picked up. */
public static class Missed extends CallState {
}
/* Incoming call that was explicitly ended on our side before picking up or outgoing call that was declined before the timeout. */
public static class Declined extends CallState {
}
/**
* Outgoing call that has been canceled on our side before receiving a response.
* <p>
* Incoming calls cannot be canceled, on the receiver side canceled calls usually result in missed calls.
*/
public static class Canceled extends CallState {
}
}
@@ -0,0 +1,13 @@
/* Autogenerated file, do not edit manually */
package chat.delta.rpc.types;
public enum EnteredCertificateChecks {
/* `Automatic` means that provider database setting should be taken. If there is no provider database setting for certificate checks, check certificates strictly. */
automatic,
/* Ensure that TLS certificate is valid for the server hostname. */
strict,
/* Accept certificates that are expired, self-signed or otherwise not valid for the server hostname. */
acceptInvalidCertificates,
}
@@ -0,0 +1,51 @@
/* Autogenerated file, do not edit manually */
package chat.delta.rpc.types;
/**
* Login parameters entered by the user.
* <p>
* Usually it will be enough to only set `addr` and `password`, and all the other settings will be autoconfigured.
*/
public class EnteredLoginParam {
/* Email address. */
public String addr;
/* TLS options: whether to allow invalid certificates and/or invalid hostnames. Default: Automatic */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public EnteredCertificateChecks certificateChecks;
/* Imap server port. */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public Integer imapPort;
/* Imap socket security. */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public Socket imapSecurity;
/* Imap server hostname or IP address. */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String imapServer;
/* Imap username. */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String imapUser;
/* If true, login via OAUTH2 (not recommended anymore). Default: false */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public Boolean oauth2;
/* Password. */
public String password;
/**
* SMTP Password.
* <p>
* Only needs to be specified if different than IMAP password.
*/
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String smtpPassword;
/* SMTP server port. */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public Integer smtpPort;
/* SMTP socket security. */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public Socket smtpSecurity;
/* SMTP server hostname or IP address. */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String smtpServer;
/* SMTP username. */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String smtpUser;
}
@@ -0,0 +1,13 @@
/* Autogenerated file, do not edit manually */
package chat.delta.rpc.types;
public class HttpResponse {
/* base64-encoded response body. */
public String blob;
/* Encoding, e.g. "utf-8". */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String encoding;
/* MIME type, e.g. "text/plain" or "text/html". */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String mimetype;
}
@@ -0,0 +1,12 @@
/* Autogenerated file, do not edit manually */
package chat.delta.rpc.types;
/* A single reaction emoji. */
public class Reaction {
/* Emoji frequency. */
public Integer count;
/* Emoji. */
public String emoji;
/* True if we reacted with this emoji. */
public Boolean isFromSelf;
}
@@ -0,0 +1,10 @@
/* Autogenerated file, do not edit manually */
package chat.delta.rpc.types;
/* Structure representing all reactions to a particular message. */
public class Reactions {
/* Unique reactions and their count, sorted in descending order. */
public java.util.List<Reaction> reactions;
/* Map from a contact to it's reaction to message. */
public java.util.Map<String, java.util.List<String>> reactionsByContact;
}
@@ -0,0 +1,16 @@
/* Autogenerated file, do not edit manually */
package chat.delta.rpc.types;
public enum Socket {
/* Unspecified socket security, select automatically. */
automatic,
/* TLS connection. */
ssl,
/* STARTTLS connection. */
starttls,
/* No TLS, plaintext connection. */
plain,
}
@@ -0,0 +1,20 @@
/* Autogenerated file, do not edit manually */
package chat.delta.rpc.types;
public class VcardContact {
/* Email address. */
public String addr;
/* Contact color as hex string. */
public String color;
/* The contact's name, or the email address if no name was given. */
public String displayName;
/* Public PGP key in Base64. */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String key;
/* Profile image in Base64. */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String profileImage;
/* Last update timestamp. */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public Integer timestamp;
}
@@ -0,0 +1,14 @@
/* Autogenerated file, do not edit manually */
package chat.delta.util;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
public interface ListenableFuture<T> extends Future<T> {
void addListener(Listener<T> listener);
public interface Listener<T> {
public void onSuccess(T result);
public void onFailure(ExecutionException e);
}
}
@@ -0,0 +1,137 @@
/* Autogenerated file, do not edit manually */
package chat.delta.util;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class SettableFuture<T> implements ListenableFuture<T> {
private final List<Listener<T>> listeners = new LinkedList<>();
private boolean completed;
private boolean canceled;
private volatile T result;
private volatile Throwable exception;
public SettableFuture() { }
public SettableFuture(T value) {
this.result = value;
this.completed = true;
}
@Override
public synchronized boolean cancel(boolean mayInterruptIfRunning) {
if (!completed && !canceled) {
canceled = true;
return true;
}
return false;
}
@Override
public synchronized boolean isCancelled() {
return canceled;
}
@Override
public synchronized boolean isDone() {
return completed;
}
public boolean set(T result) {
synchronized (this) {
if (completed || canceled) return false;
this.result = result;
this.completed = true;
notifyAll();
}
notifyAllListeners();
return true;
}
public boolean setException(Throwable throwable) {
synchronized (this) {
if (completed || canceled) return false;
this.exception = throwable;
this.completed = true;
notifyAll();
}
notifyAllListeners();
return true;
}
public void deferTo(ListenableFuture<T> other) {
other.addListener(new Listener<T>() {
@Override
public void onSuccess(T result) {
SettableFuture.this.set(result);
}
@Override
public void onFailure(ExecutionException e) {
SettableFuture.this.setException(e.getCause());
}
});
}
@Override
public synchronized T get() throws InterruptedException, ExecutionException {
while (!completed) wait();
if (exception != null) throw new ExecutionException(exception);
else return result;
}
@Override
public synchronized T get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException
{
long startTime = System.currentTimeMillis();
while (!completed && System.currentTimeMillis() - startTime > unit.toMillis(timeout)) {
wait(unit.toMillis(timeout));
}
if (!completed) throw new TimeoutException();
else return get();
}
@Override
public void addListener(Listener<T> listener) {
synchronized (this) {
listeners.add(listener);
if (!completed) return;
}
notifyListener(listener);
}
private void notifyAllListeners() {
List<Listener<T>> localListeners;
synchronized (this) {
localListeners = new LinkedList<>(listeners);
}
for (Listener<T> listener : localListeners) {
notifyListener(listener);
}
}
private void notifyListener(Listener<T> listener) {
if (exception != null) listener.onFailure(new ExecutionException(exception));
else listener.onSuccess(result);
}
}
@@ -50,4 +50,14 @@ public class DcAccounts {
private native long getJsonrpcInstanceCPtr ();
private native long getAccountCPtr (int accountId);
private native long getSelectedAccountCPtr ();
public boolean isAllChatmail() {
for (int accountId : getAll()) {
DcContext dcContext = getAccount(accountId);
if (!dcContext.isChatmail()) {
return false;
}
}
return true;
}
}
@@ -31,6 +31,10 @@ public class DcContext {
public final static int DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121;
public final static int DC_EVENT_WEBXDC_REALTIME_DATA = 2150;
public final static int DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200;
public final static int DC_EVENT_INCOMING_CALL = 2550;
public final static int DC_EVENT_INCOMING_CALL_ACCEPTED = 2560;
public final static int DC_EVENT_OUTGOING_CALL_ACCEPTED = 2570;
public final static int DC_EVENT_CALL_ENDED = 2580;
public final static int DC_IMEX_EXPORT_SELF_KEYS = 1;
public final static int DC_IMEX_IMPORT_SELF_KEYS = 2;
@@ -201,7 +205,6 @@ public class DcContext {
public native boolean resendMsgs (int msg_ids[]);
public native int sendMsg (int chat_id, DcMsg msg);
public native int sendTextMsg (int chat_id, String text);
public native int sendVideochatInvitation(int chat_id);
public native boolean sendWebxdcStatusUpdate(int msg_id, String payload);
public native String getWebxdcStatusUpdates(int msg_id, int last_known_serial);
public native void setWebxdcIntegration (String file);
+2 -4
View File
@@ -18,7 +18,7 @@ public class DcMsg {
public final static int DC_MSG_VOICE = 41;
public final static int DC_MSG_VIDEO = 50;
public final static int DC_MSG_FILE = 60;
public final static int DC_MSG_VIDEOCHAT_INVITATION = 70;
public final static int DC_MSG_CALL = 71;
public final static int DC_MSG_WEBXDC = 80;
public final static int DC_MSG_VCARD = 90;
@@ -155,8 +155,6 @@ public class DcMsg {
public native boolean isInfo ();
public native boolean hasHtml ();
public native String getSetupCodeBegin ();
public native String getVideochatUrl ();
public native int getVideochatType ();
public native void setText (String text);
public native void setSubject (String text);
public native void setHtml (String text);
@@ -196,7 +194,7 @@ public class DcMsg {
public boolean canSave() {
// saving info-messages out of context results in confusion, see https://github.com/deltachat/deltachat-ios/issues/2567
return !isInfo() && getType() != DC_MSG_VIDEOCHAT_INVITATION;
return !isInfo();
}
public File getFileAsFile() {
@@ -0,0 +1,22 @@
package com.b44t.messenger;
import chat.delta.rpc.BaseTransport;
/* RPC transport over C FFI */
public class FFITransport extends BaseTransport {
private final DcJsonrpcInstance dcJsonrpcInstance;
public FFITransport(DcJsonrpcInstance dcJsonrpcInstance) {
this.dcJsonrpcInstance = dcJsonrpcInstance;
}
@Override
protected void sendRequest(String jsonRequest) {
dcJsonrpcInstance.request(jsonRequest);
}
@Override
protected String getResponse() {
return dcJsonrpcInstance.getNextResponse();
}
}
@@ -1,171 +0,0 @@
package com.b44t.messenger.rpc;
public class EnteredLoginParam {
// Email address.
private final String addr;
// Password.
private final String password;
// ============ IMAP settings ============
// Server hostname or IP address.
private final String imapServer;
// Server port.
private final int imapPort;
// Socket security.
private final SocketSecurity imapSecurity;
// Username.
private final String imapUser;
// ============ SMTP settings ============
// Server hostname or IP address.
private final String smtpServer;
// Server port.
private final int smtpPort;
// Socket security.
private final SocketSecurity smtpSecurity;
// Username.
private final String smtpUser;
// SMTP Password. Only needs to be specified if different than IMAP password.
private final String smtpPassword;
// TLS options: whether to allow invalid certificates and/or
// invalid hostnames
private final EnteredCertificateChecks certificateChecks;
// If true, login via OAUTH2 (not recommended anymore)
private final boolean oauth2;
public EnteredLoginParam(String addr,
String password,
String imapServer,
int imapPort,
SocketSecurity imapSecurity,
String imapUser,
String smtpServer,
int smtpPort,
SocketSecurity smtpSecurity,
String smtpUser,
String smtpPassword,
EnteredCertificateChecks certificateChecks,
boolean oauth2) {
this.addr = addr;
this.password = password;
this.imapServer = imapServer;
this.imapPort = imapPort;
this.imapSecurity = imapSecurity;
this.imapUser = imapUser;
this.smtpServer = smtpServer;
this.smtpPort = smtpPort;
this.smtpSecurity = smtpSecurity;
this.smtpUser = smtpUser;
this.smtpPassword = smtpPassword;
this.certificateChecks = certificateChecks;
this.oauth2 = oauth2;
}
public String getAddr() {
return addr;
}
public String getPassword() {
return password;
}
public String getImapServer() {
return imapServer;
}
public int getImapPort() {
return imapPort;
}
public SocketSecurity getImapSecurity() {
return imapSecurity;
}
public String getImapUser() {
return imapUser;
}
public String getSmtpServer() {
return smtpServer;
}
public int getSmtpPort() {
return smtpPort;
}
public SocketSecurity getSmtpSecurity() {
return smtpSecurity;
}
public String getSmtpUser() {
return smtpUser;
}
public String getSmtpPassword() {
return smtpPassword;
}
public EnteredCertificateChecks getCertificateChecks() {
return certificateChecks;
}
public boolean isOauth2() {
return oauth2;
}
public enum EnteredCertificateChecks {
automatic, strict, acceptInvalidCertificates,
}
public static EnteredCertificateChecks certificateChecksFromInt(int position) {
switch (position) {
case 0:
return EnteredCertificateChecks.automatic;
case 1:
return EnteredCertificateChecks.strict;
case 2:
return EnteredCertificateChecks.acceptInvalidCertificates;
}
throw new IllegalArgumentException("Invalid certificate position: " + position);
}
public enum SocketSecurity {
// Unspecified socket security, select automatically.
automatic,
// TLS connection.
ssl,
// STARTTLS connection.
starttls,
// No TLS, plaintext connection.
plain,
}
public static SocketSecurity socketSecurityFromInt(int position) {
switch (position) {
case 0:
return SocketSecurity.automatic;
case 1:
return SocketSecurity.ssl;
case 2:
return SocketSecurity.starttls;
case 3:
return SocketSecurity.plain;
}
throw new IllegalArgumentException("Invalid socketSecurity position: " + position);
}
}
@@ -1,33 +0,0 @@
package com.b44t.messenger.rpc;
import android.util.Base64;
public class HttpResponse {
// base64-encoded response body.
private final String blob;
// MIME type, e.g. "text/plain" or "text/html".
private final String mimetype;
// Encoding, e.g. "utf-8".
private final String encoding;
public HttpResponse(String blob, String mimetype, String encoding) {
this.blob = blob;
this.mimetype = mimetype;
this.encoding = encoding;
}
public byte[] getBlob() {
if (blob == null) {
return null;
}
return Base64.decode(blob, Base64.NO_WRAP | Base64.NO_PADDING);
}
public String getMimetype() {
return mimetype;
}
public String getEncoding() {
return encoding;
}
}
@@ -1,39 +0,0 @@
package com.b44t.messenger.rpc;
import androidx.annotation.Nullable;
public class Reaction {
// The reaction emoji string.
private final String emoji;
// The count of users that have reacted with this reaction.
private final int count;
// true if self-account reacted with this reaction, false otherwise.
private final boolean isFromSelf;
public Reaction(String emoji, int count, boolean isFromSelf) {
this.emoji = emoji;
this.count = count;
this.isFromSelf = isFromSelf;
}
public String getEmoji() {
return emoji;
}
public int getCount() {
return count;
}
public boolean isFromSelf() {
return isFromSelf;
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj instanceof Reaction) {
Reaction reaction = (Reaction) obj;
return emoji.equals(reaction.getEmoji()) && count == reaction.getCount() && isFromSelf == reaction.isFromSelf();
}
return false;
}
}
@@ -1,26 +0,0 @@
package com.b44t.messenger.rpc;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Reactions {
// Map from a contact to it's reaction to message.
private final HashMap<Integer, String[]> reactionsByContact;
// Unique reactions, sorted in descending order.
private final ArrayList<Reaction> reactions;
public Reactions(HashMap<Integer, String[]> reactionsByContact, ArrayList<Reaction> reactions) {
this.reactionsByContact = reactionsByContact;
this.reactions = reactions;
}
public Map<Integer, String[]> getReactionsByContact() {
return reactionsByContact;
}
public List<Reaction> getReactions() {
return reactions;
}
}
@@ -1,203 +0,0 @@
package com.b44t.messenger.rpc;
import android.util.Log;
import com.b44t.messenger.DcJsonrpcInstance;
import com.b44t.messenger.util.concurrent.SettableFuture;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
public class Rpc {
private final static String TAG = Rpc.class.getSimpleName();
private final Map<Integer, SettableFuture<JsonElement>> requestFutures = new ConcurrentHashMap<>();
private final DcJsonrpcInstance dcJsonrpcInstance;
private int requestId = 0;
private boolean started = false;
private final Gson gson = new GsonBuilder().serializeNulls().create();
public Rpc(DcJsonrpcInstance dcJsonrpcInstance) {
this.dcJsonrpcInstance = dcJsonrpcInstance;
}
private void processResponse() throws JsonSyntaxException {
String jsonResponse = dcJsonrpcInstance.getNextResponse();
Response response = gson.fromJson(jsonResponse, Response.class);
if (response == null) {
Log.e(TAG, "Error parsing JSON: " + jsonResponse);
return;
} else if (response.id == 0) {
// Got JSON-RPC notification/event, ignore
return;
}
SettableFuture<JsonElement> future = requestFutures.remove(response.id);
if (future == null) { // Got a response with unknown ID, ignore
return;
}
if (response.error != null) {
String message;
try {
message = response.error.getAsJsonObject().get("message").getAsString();
} catch (Exception e) {
Log.e(TAG, "Can't get response error message: " + e);
message = response.error.toString();
}
future.setException(new RpcException(message));
} else if (response.result != null) {
future.set(response.result);
} else {
future.setException(new RpcException("Got JSON-RPC response without result or error: " + jsonResponse));
}
}
public void start() {
started = true;
new Thread(() -> {
while (true) {
try {
processResponse();
} catch (Exception e) {
e.printStackTrace();
}
}
}, "jsonrpcThread").start();
}
public SettableFuture<JsonElement> call(String method, Object... params) throws RpcException {
if (!started) throw new RpcException("RPC not started yet.");
int id;
synchronized (this) {
id = ++requestId;
}
String jsonRequest = gson.toJson(new Request(method, params, id));
SettableFuture<JsonElement> future = new SettableFuture<>();
requestFutures.put(id, future);
dcJsonrpcInstance.request(jsonRequest);
return future;
}
public JsonElement getResult(String method, Object... params) throws RpcException {
try {
return call(method, params).get();
} catch (ExecutionException e) {
throw (RpcException)e.getCause();
} catch (InterruptedException e) {
throw new RpcException(e.getMessage());
}
}
public List<VcardContact> parseVcard(String path) throws RpcException {
TypeToken<List<VcardContact>> listType = new TypeToken<List<VcardContact>>(){};
return gson.fromJson(getResult("parse_vcard", path), listType.getType());
}
public String makeVcard(int accountId, int... contacts) throws RpcException {
return gson.fromJson(getResult("make_vcard", accountId, contacts), String.class);
}
public List<Integer> importVcard(int accountId, String path) throws RpcException {
TypeToken<List<Integer>> listType = new TypeToken<List<Integer>>(){};
return gson.fromJson(getResult("import_vcard", accountId, path), listType.getType());
}
public HttpResponse getHttpResponse(int accountId, String url) throws RpcException {
return gson.fromJson(getResult("get_http_response", accountId, url), HttpResponse.class);
}
public Reactions getMsgReactions(int accountId, int msgId) throws RpcException {
return gson.fromJson(getResult("get_message_reactions", accountId, msgId), Reactions.class);
}
public int sendReaction(int accountId, int msgId, String... reaction) throws RpcException {
return getResult("send_reaction", accountId, msgId, reaction).getAsInt();
}
public int draftSelfReport(int accountId) throws RpcException {
return getResult("draft_self_report", accountId).getAsInt();
}
public void sendWebxdcRealtimeData(Integer accountId, Integer instanceMsgId, List<Integer> data) throws RpcException {
getResult("send_webxdc_realtime_data", accountId, instanceMsgId, data);
}
public void sendWebxdcRealtimeAdvertisement(Integer accountId, Integer instanceMsgId) throws RpcException {
getResult("send_webxdc_realtime_advertisement", accountId, instanceMsgId);
}
public void leaveWebxdcRealtime(Integer accountId, Integer instanceMessageId) throws RpcException {
getResult("leave_webxdc_realtime", accountId, instanceMessageId);
}
public int getAccountFileSize(int accountId) throws RpcException {
return getResult("get_account_file_size", accountId).getAsInt();
}
public void changeContactName(int accountId, int contactId, String name) throws RpcException {
getResult("change_contact_name", accountId, contactId, name);
}
public int addAccount() throws RpcException {
return getResult("add_account").getAsInt();
}
public void addTransportFromQr(int accountId, String qrCode) throws RpcException {
getResult("add_transport_from_qr", accountId, qrCode);
}
public void addOrUpdateTransport(int accountId, EnteredLoginParam param) throws RpcException {
getResult("add_or_update_transport", accountId, param);
}
public int createBroadcast(int accountId, String chatName) throws RpcException {
return gson.fromJson(getResult("create_broadcast", accountId, chatName), Integer.class);
}
public int createGroupChatUnencrypted(int accountId, String chatName) throws RpcException {
return gson.fromJson(getResult("create_group_chat_unencrypted", accountId, chatName), Integer.class);
}
public void setAccountsOrder(List<Integer> order) throws RpcException {
getResult("set_accounts_order", order);
}
private static class Request {
private final String jsonrpc = "2.0";
public final String method;
public final Object[] params;
public final int id;
public Request(String method, Object[] params, int id) {
this.method = method;
this.params = params;
this.id = id;
}
}
public String getMigrationError(int accountId) throws RpcException {
return gson.fromJson(getResult("get_migration_error", accountId), String.class);
}
private static class Response {
public final int id;
public final JsonElement result;
public final JsonElement error;
public Response(int id, JsonElement result, JsonElement error) {
this.id = id;
this.result = result;
this.error = error;
}
}
}
@@ -1,11 +0,0 @@
package com.b44t.messenger.rpc;
/**
* An exception occurred while processing a request in ArcaneChat core.
**/
public class RpcException extends Exception {
public RpcException(String message) {
super(message);
}
}
@@ -1,60 +0,0 @@
package com.b44t.messenger.rpc;
import android.util.Base64;
public class VcardContact {
// Email address.
private final String addr;
// The contact's name, or the email address if no name was given.
private final String displayName;
// Public PGP key in Base64.
private final String key;
// Profile image in Base64.
private final String profileImage;
// Contact color in HTML color format.
private final String color;
// Last update timestamp.
private final int timestamp;
public VcardContact(String addr, String displayName, String key, String profileImage, String color, int timestamp) {
this.addr = addr;
this.displayName = displayName;
this.key = key;
this.profileImage = profileImage;
this.color = color;
this.timestamp = timestamp;
}
public String getAddr() {
return addr;
}
public String getDisplayName() {
return displayName;
}
public byte[] getKey() {
return key == null? null : Base64.decode(key, Base64.NO_WRAP | Base64.NO_PADDING);
}
public boolean hasProfileImage() {
return profileImage != null;
}
public byte[] getProfileImage() {
return profileImage == null? null : Base64.decode(profileImage, Base64.NO_WRAP | Base64.NO_PADDING);
}
public String getColor() {
return color;
}
public int getTimestamp() {
return timestamp;
}
}
@@ -25,8 +25,7 @@ import com.b44t.messenger.DcAccounts;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcEventEmitter;
import com.b44t.messenger.rpc.Rpc;
import com.b44t.messenger.rpc.RpcException;
import com.b44t.messenger.FFITransport;
import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcEventCenter;
@@ -53,6 +52,9 @@ import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.concurrent.TimeUnit;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class ApplicationContext extends MultiDexApplication {
private static final String TAG = ApplicationContext.class.getSimpleName();
@@ -109,8 +111,7 @@ public class ApplicationContext extends MultiDexApplication {
System.loadLibrary("native-utils");
dcAccounts = new DcAccounts(new File(getFilesDir(), "accounts").getAbsolutePath());
rpc = new Rpc(dcAccounts.getJsonrpcInstance());
rpc.start();
rpc = new Rpc(new FFITransport(dcAccounts.getJsonrpcInstance()));
AccountManager.getInstance().migrateToDcAccounts(this);
int[] allAccounts = dcAccounts.getAll();
for (int accountId : allAccounts) {
@@ -14,7 +14,6 @@ import androidx.appcompat.app.AlertDialog;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import com.b44t.messenger.rpc.Rpc;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -24,6 +23,8 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.HashSet;
import java.util.Set;
import chat.delta.rpc.Rpc;
public abstract class BaseConversationItem extends LinearLayout
implements BindableConversationItem
{
@@ -74,8 +74,6 @@ import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcMsg;
import com.b44t.messenger.rpc.Rpc;
import com.b44t.messenger.rpc.RpcException;
import com.b44t.messenger.util.concurrent.ListenableFuture;
import com.b44t.messenger.util.concurrent.SettableFuture;
@@ -122,13 +120,17 @@ 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.videochat.VideochatUtil;
import org.thoughtcrime.securesms.calls.CallUtil;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
/**
* Activity for displaying a message thread, as well as
* composing/sending a new message into that thread.
@@ -448,6 +450,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
menu.findItem(R.id.menu_show_map).setVisible(false);
}
menu.findItem(R.id.menu_start_call).setVisible(
Prefs.isCallsEnabled(this)
&& dcChat.canSend()
&& dcChat.isEncrypted()
&& !dcChat.isSelfTalk()
&& !dcChat.isMultiUser()
);
if (!dcChat.isEncrypted() || !dcChat.canSend() || dcChat.isMailingList() ) {
menu.findItem(R.id.menu_ephemeral_messages).setVisible(false);
}
@@ -538,6 +548,9 @@ 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_call) {
CallUtil.startCall(this, chatId);
return true;
} else if (itemId == R.id.menu_all_media) {
handleAllMedia();
return true;
@@ -972,8 +985,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
AttachmentManager.selectGallery(this, PICK_GALLERY); break;
case AttachmentTypeSelector.ADD_DOCUMENT:
AttachmentManager.selectDocument(this, PICK_DOCUMENT); break;
case AttachmentTypeSelector.INVITE_VIDEO_CHAT:
new VideochatUtil().invite(this, chatId); break;
case AttachmentTypeSelector.ADD_CONTACT_INFO:
startContactChooserActivity(); break;
case AttachmentTypeSelector.ADD_LOCATION:
@@ -1012,7 +1023,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
try {
byte[] vcard = rpc.makeVcard(dcContext.getAccountId(), contactId).getBytes();
byte[] vcard = rpc.makeVcard(dcContext.getAccountId(), Collections.singletonList(contactId)).getBytes();
String mimeType = "application/octet-stream";
setMedia(PersistentBlobProvider.getInstance().create(this, vcard, mimeType, "vcard.vcf"), MediaType.DOCUMENT);
} catch (RpcException e) {
@@ -1464,7 +1475,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
Recipient author = new Recipient(this, dcContext.getContact(msg.getFromId()));
SlideDeck slideDeck = new SlideDeck();
if (msg.getType() != DcMsg.DC_MSG_TEXT) {
if (msg.hasFile()) {
slideDeck.addSlide(MediaUtil.getSlideForMsg(this, msg));
}
@@ -77,9 +77,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
private static final int MESSAGE_TYPE_THUMBNAIL_INCOMING = 6;
private static final int MESSAGE_TYPE_DOCUMENT_OUTGOING = 7;
private static final int MESSAGE_TYPE_DOCUMENT_INCOMING = 8;
private static final int MESSAGE_TYPE_VIDEOCHAT_INVITE = 9;
private static final int MESSAGE_TYPE_STICKER_INCOMING = 10;
private static final int MESSAGE_TYPE_STICKER_OUTGOING = 11;
private static final int MESSAGE_TYPE_STICKER_INCOMING = 9;
private static final int MESSAGE_TYPE_STICKER_OUTGOING = 10;
private final Set<DcMsg> batchSelected = Collections.synchronizedSet(new HashSet<DcMsg>());
@@ -279,7 +278,6 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
case MESSAGE_TYPE_STICKER_INCOMING:
case MESSAGE_TYPE_INCOMING: return R.layout.conversation_item_received;
case MESSAGE_TYPE_INFO: return R.layout.conversation_item_update;
case MESSAGE_TYPE_VIDEOCHAT_INVITE:return R.layout.conversation_item_videochat;
default: throw new IllegalArgumentException("unsupported item view type given to ConversationAdapter");
}
}
@@ -303,9 +301,6 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
else if (type == DcMsg.DC_MSG_STICKER) {
return dcMsg.isOutgoing()? MESSAGE_TYPE_STICKER_OUTGOING : MESSAGE_TYPE_STICKER_INCOMING;
}
else if (type == DcMsg.DC_MSG_VIDEOCHAT_INVITATION) {
return MESSAGE_TYPE_VIDEOCHAT_INVITE;
}
else {
return dcMsg.isOutgoing()? MESSAGE_TYPE_OUTGOING : MESSAGE_TYPE_INCOMING;
}
@@ -66,7 +66,7 @@ import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.ConversationAdaptiveActionsToolbar;
import org.thoughtcrime.securesms.videochat.VideochatUtil;
import org.thoughtcrime.securesms.calls.CallUtil;
import java.util.Collections;
import java.util.LinkedList;
@@ -367,8 +367,7 @@ public class ConversationFragment extends MessageSelectorFragment
}
static boolean canReplyToMsg(DcMsg dcMsg) {
boolean canReply = dcMsg.getType() != DcMsg.DC_MSG_VIDEOCHAT_INVITATION;
if (canReply && dcMsg.isInfo()) {
if (dcMsg.isInfo()) {
switch (dcMsg.getInfoType()) {
case DcMsg.DC_INFO_GROUP_NAME_CHANGED:
case DcMsg.DC_INFO_GROUP_IMAGE_CHANGED:
@@ -377,16 +376,16 @@ public class ConversationFragment extends MessageSelectorFragment
case DcMsg.DC_INFO_LOCATIONSTREAMING_ENABLED:
case DcMsg.DC_INFO_EPHEMERAL_TIMER_CHANGED:
case DcMsg.DC_INFO_WEBXDC_INFO_MESSAGE:
break;
return true;
default:
canReply = false;
return false;
}
}
return canReply;
return true;
}
static boolean canEditMsg(DcMsg dcMsg) {
return dcMsg.isOutgoing() && !dcMsg.isInfo() && dcMsg.getType() != DcMsg.DC_MSG_VIDEOCHAT_INVITATION && !dcMsg.hasHtml() && !dcMsg.getText().isEmpty();
return dcMsg.isOutgoing() && !dcMsg.isInfo() && dcMsg.getType() != DcMsg.DC_MSG_CALL && !dcMsg.hasHtml() && !dcMsg.getText().isEmpty();
}
public void handleClearChat() {
@@ -758,9 +757,6 @@ public class ConversationFragment extends MessageSelectorFragment
actionMode.setTitleOptionalHint(false); // the title represents important information, also indicating implicitly, more items can be selected
}
}
else if (messageRecord.getType()==DcMsg.DC_MSG_VIDEOCHAT_INVITATION) {
new VideochatUtil().join(getActivity(), messageRecord.getId());
}
else if(DozeReminder.isDozeReminderMsg(getContext(), messageRecord)) {
DozeReminder.dozeReminderTapped(getContext());
}
@@ -40,14 +40,12 @@ import androidx.appcompat.app.AlertDialog;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcMsg;
import com.b44t.messenger.rpc.Reactions;
import com.b44t.messenger.rpc.RpcException;
import com.b44t.messenger.rpc.VcardContact;
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;
import org.thoughtcrime.securesms.components.ConversationItemFooter;
import org.thoughtcrime.securesms.components.ConversationItemThumbnail;
import org.thoughtcrime.securesms.components.DocumentView;
@@ -73,10 +71,17 @@ 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;
import chat.delta.rpc.RpcException;
import chat.delta.rpc.types.CallInfo;
import chat.delta.rpc.types.CallState;
import chat.delta.rpc.types.Reactions;
import chat.delta.rpc.types.VcardContact;
/**
* A view that displays an individual conversation item within a conversation
* thread. Used by ComposeMessageActivity's ListActivity via a ConversationAdapter.
@@ -118,6 +123,7 @@ public class ConversationItem extends BaseConversationItem
private @NonNull Stub<WebxdcView> webxdcViewStub;
private Stub<BorderlessImageView> stickerStub;
private Stub<VcardView> vcardViewStub;
private Stub<CallItemView> callViewStub;
private @Nullable EventListener eventListener;
private int measureCalls;
@@ -152,6 +158,7 @@ public class ConversationItem extends BaseConversationItem
this.webxdcViewStub = new Stub<>(findViewById(R.id.webxdc_view_stub));
this.stickerStub = new Stub<>(findViewById(R.id.sticker_view_stub));
this.vcardViewStub = new Stub<>(findViewById(R.id.vcard_view_stub));
this.callViewStub = new Stub<>(findViewById(R.id.call_view_stub));
this.groupSenderHolder = findViewById(R.id.group_sender_holder);
this.quoteView = findViewById(R.id.quote_view);
this.container = findViewById(R.id.container);
@@ -322,6 +329,11 @@ public class ConversationItem extends BaseConversationItem
vcardViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
vcardViewStub.get().setClickable(batchSelected.isEmpty());
}
if (callViewStub.resolved()) {
callViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
callViewStub.get().setClickable(batchSelected.isEmpty());
}
}
private void setContentDescription() {
@@ -338,6 +350,8 @@ public class ConversationItem extends BaseConversationItem
desc += webxdcViewStub.get().getDescription() + "\n";
} else if (vcardViewStub.resolved() && vcardViewStub.get().getVisibility() == View.VISIBLE) {
desc += vcardViewStub.get().getDescription() + "\n";
} else if (callViewStub.resolved() && callViewStub.get().getVisibility() == View.VISIBLE) {
desc += callViewStub.get().getDescription() + "\n";
} else if (mediaThumbnailStub.resolved() && mediaThumbnailStub.get().getVisibility() == View.VISIBLE) {
desc += mediaThumbnailStub.get().getDescription() + "\n";
} else if (stickerStub.resolved() && stickerStub.get().getVisibility() == View.VISIBLE) {
@@ -399,7 +413,7 @@ public class ConversationItem extends BaseConversationItem
String text = messageRecord.getText();
if (text.isEmpty()) {
if (messageRecord.getType() == DcMsg.DC_MSG_CALL || text.isEmpty()) {
bodyText.setVisibility(View.GONE);
}
else {
@@ -490,6 +504,7 @@ public class ConversationItem extends BaseConversationItem
if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
if (callViewStub.resolved()) callViewStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
int duration = messageRecord.getDuration();
@@ -515,6 +530,7 @@ public class ConversationItem extends BaseConversationItem
if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
if (callViewStub.resolved()) callViewStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
documentViewStub.get().setDocument(new DocumentSlide(context, messageRecord));
@@ -533,6 +549,7 @@ public class ConversationItem extends BaseConversationItem
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
if (callViewStub.resolved()) callViewStub.get().setVisibility(View.GONE);
webxdcViewStub.get().setWebxdc(messageRecord, context.getString(R.string.webxdc_app));
webxdcViewStub.get().setWebxdcClickListener(new ThumbnailClickListener());
@@ -550,6 +567,7 @@ public class ConversationItem extends BaseConversationItem
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (callViewStub.resolved()) callViewStub.get().setVisibility(View.GONE);
vcardViewStub.get().setVcard(glideRequests, new VcardSlide(context, messageRecord), rpc);
vcardViewStub.get().setVcardClickListener(new ThumbnailClickListener());
@@ -561,6 +579,27 @@ public class ConversationItem extends BaseConversationItem
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
footer.setVisibility(VISIBLE);
}
else if (messageRecord.getType() == DcMsg.DC_MSG_CALL) {
callViewStub.get().setVisibility(View.VISIBLE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
try {
callViewStub.get().setCallItem(messageRecord.isOutgoing(), rpc.callInfo(dcContext.getAccountId(), messageRecord.getId()));
} catch (RpcException e) {
Log.e(TAG, "Error in Rpc.callInfo", e);
}
callViewStub.get().setCallClickListener(new CallClickListener());
callViewStub.get().setOnLongClickListener(passthroughClickListener);
callViewStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
else if (hasThumbnail(messageRecord)) {
mediaThumbnailStub.get().setVisibility(View.VISIBLE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
@@ -568,6 +607,7 @@ public class ConversationItem extends BaseConversationItem
if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
if (callViewStub.resolved()) callViewStub.get().setVisibility(View.GONE);
Slide slide = MediaUtil.getSlideForMsg(context, messageRecord);
@@ -607,6 +647,7 @@ public class ConversationItem extends BaseConversationItem
if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
if (callViewStub.resolved()) callViewStub.get().setVisibility(View.GONE);
bodyBubble.setBackgroundColor(Color.TRANSPARENT);
@@ -627,6 +668,7 @@ public class ConversationItem extends BaseConversationItem
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
if (callViewStub.resolved()) callViewStub.get().setVisibility(View.GONE);
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
@@ -757,11 +799,11 @@ public class ConversationItem extends BaseConversationItem
private void setReactions(@NonNull DcMsg current) {
try {
Reactions reactions = rpc.getMsgReactions(dcContext.getAccountId(), current.getId());
Reactions reactions = rpc.getMessageReactions(dcContext.getAccountId(), current.getId());
if (reactions == null) {
reactionsView.clear();
} else {
reactionsView.setReactions(reactions.getReactions());
reactionsView.setReactions(reactions.reactions);
reactionsView.setOnClickListener(view -> {
if (eventListener != null && batchSelected.isEmpty()) {
eventListener.onReactionClicked(current);
@@ -780,6 +822,8 @@ public class ConversationItem extends BaseConversationItem
return stickerStub.get().getFooter();
} else if (hasOnlyThumbnail(messageRecord) && TextUtils.isEmpty(messageRecord.getText())) {
return mediaThumbnailStub.get().getFooter();
} else if (messageRecord.getType() == DcMsg.DC_MSG_CALL) {
return callViewStub.get().getFooter();
} else {
return footer;
}
@@ -881,6 +925,7 @@ public class ConversationItem extends BaseConversationItem
else if (documentViewStub.resolved()) documentViewStub.get().performClick();
else if (webxdcViewStub.resolved()) webxdcViewStub.get().performClick();
else if (vcardViewStub.resolved()) vcardViewStub.get().performClick();
else if (callViewStub.resolved()) callViewStub.get().performClick();
}
/// Event handlers
@@ -896,7 +941,7 @@ public class ConversationItem extends BaseConversationItem
String path = slide.asAttachment().getRealPath(context);
VcardContact vcardContact = rpc.parseVcard(path).get(0);
new AlertDialog.Builder(context)
.setMessage(context.getString(R.string.ask_start_chat_with, vcardContact.getDisplayName()))
.setMessage(context.getString(R.string.ask_start_chat_with, vcardContact.displayName))
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
try {
List<Integer> contactIds = rpc.importVcard(dcContext.getAccountId(), path);
@@ -942,4 +987,21 @@ public class ConversationItem extends BaseConversationItem
}
}
}
private class CallClickListener implements CallItemView.CallClickListener {
public void onClick(final View v, final CallInfo callInfo) {
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) {
performClick();
} else {
int accId = dcContext.getAccountId();
int chatId = messageRecord.getChatId();
if (!messageRecord.isOutgoing() && callInfo.state instanceof CallState.Alerting) {
int callId = messageRecord.getId();
CallUtil.openCall(getContext(), accId, chatId, callId, callInfo.sdpOffer);
} else {
CallUtil.startCall(getContext(), accId, chatId);
}
}
}
}
}
@@ -1,99 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.ConversationItemFooter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Set;
public class ConversationVideochatItem extends LinearLayout
implements BindableConversationItem
{
private TextView body;
private ConversationItemFooter footer;
private AvatarImageView contactPhoto;
private ViewGroup contactPhotoHolder;
private DcMsg dcMsg;
public ConversationVideochatItem(Context context) {
super(context);
}
public ConversationVideochatItem(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onFinishInflate() {
super.onFinishInflate();
this.body = findViewById(R.id.conversation_update_body);
this.footer = findViewById(R.id.conversation_item_footer);
this.contactPhoto = findViewById(R.id.contact_photo);
this.contactPhotoHolder = findViewById(R.id.contact_photo_container);
}
@Override
public void bind(@NonNull DcMsg dcMsg,
@NonNull DcChat dcChat,
@NonNull GlideRequests glideRequests,
@NonNull Set<DcMsg> batchSelected,
@NonNull Recipient conversationRecipient,
boolean pulseUpdate)
{
this.dcMsg = dcMsg;
DcContext dcContext = DcHelper.getContext(getContext());
DcContact dcContact = dcContext.getContact(dcMsg.getFromId());
CharSequence line1 = dcMsg.isOutgoing()? getContext().getString(R.string.videochat_you_invited_hint) :
getContext().getString(R.string.videochat_contact_invited_hint, dcContact.getDisplayName());
CharSequence line2 = Util.getBoldedString(dcMsg.isOutgoing()? getContext().getString(R.string.videochat_tap_to_open) :
getContext().getString(R.string.videochat_tap_to_join));
body.setText(TextUtils.concat(line1, "\n", line2));
contactPhoto.setAvatar(glideRequests, new Recipient(getContext(), dcContact), true);
setSelected(batchSelected.contains(dcMsg));
setFooter(dcMsg);
}
private void setFooter(@NonNull DcMsg dcMsg) {
ViewUtil.updateLayoutParams(footer, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
footer.setVisibility(VISIBLE);
footer.setMessageRecord(dcMsg);
}
@Override
public void setEventListener(@Nullable EventListener listener) {
// No events to report yet
}
@Override
public DcMsg getMessageRecord() {
return dcMsg;
}
@Override
public void unbind() {
}
}
@@ -60,14 +60,12 @@ public class EphemeralMessagesDialog {
.setPositiveButton(R.string.ok, (dialog, which) -> {
final long burnAfter;
switch (selectedChoice[0]) {
case 1: burnAfter = TimeUnit.MINUTES.toSeconds(1); break;
case 2: burnAfter = TimeUnit.MINUTES.toSeconds(5); break;
case 3: burnAfter = TimeUnit.MINUTES.toSeconds(30); break;
case 4: burnAfter = TimeUnit.HOURS.toSeconds(1); break;
case 5: burnAfter = TimeUnit.DAYS.toSeconds(1); break;
case 6: burnAfter = TimeUnit.DAYS.toSeconds(7); break;
case 7: burnAfter = TimeUnit.DAYS.toSeconds(35); break;
case 8: burnAfter = TimeUnit.DAYS.toSeconds(365); break;
case 1: burnAfter = TimeUnit.MINUTES.toSeconds(5); break;
case 2: burnAfter = TimeUnit.HOURS.toSeconds(1); break;
case 3: burnAfter = TimeUnit.DAYS.toSeconds(1); break;
case 4: burnAfter = TimeUnit.DAYS.toSeconds(7); break;
case 5: burnAfter = TimeUnit.DAYS.toSeconds(35); break;
case 6: burnAfter = TimeUnit.DAYS.toSeconds(365); break;
default: burnAfter = 0; break;
}
listener.onTimeSelected(burnAfter);
@@ -84,30 +82,23 @@ public class EphemeralMessagesDialog {
if (timespan == 0) {
return 0; // off
}
// Choose timespan close to the current one out of available options.
if (timespan < TimeUnit.MINUTES.toSeconds(5)) {
return 1; // 1 minute
}
if (timespan < TimeUnit.MINUTES.toSeconds(30)) {
return 2; // 5 minutes
}
if (timespan < TimeUnit.HOURS.toSeconds(1)) {
return 3; // 30 minutes
return 1; // 5 minutes
}
if (timespan < TimeUnit.DAYS.toSeconds(1)) {
return 4; // 1 hour
return 2; // 1 hour
}
if (timespan < TimeUnit.DAYS.toSeconds(7)) {
return 5; // 1 day
return 3; // 1 day
}
if (timespan < TimeUnit.DAYS.toSeconds(35)) {
return 6; // 1 week
return 4; // 1 week
}
if (timespan < TimeUnit.DAYS.toSeconds(365)) {
return 7; // 5 weeks
return 5; // 5 weeks
}
return 8; // 1 year
return 6; // 1 year
}
}
@@ -13,17 +13,19 @@ import android.webkit.WebView;
import androidx.appcompat.app.AlertDialog;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.rpc.HttpResponse;
import com.b44t.messenger.rpc.Rpc;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.Util;
import java.io.ByteArrayInputStream;
import java.lang.ref.WeakReference;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.types.HttpResponse;
public class FullMsgActivity extends WebViewActivity
{
public static final String MSG_ID_EXTRA = "msg_id";
@@ -227,11 +229,12 @@ public class FullMsgActivity extends WebViewActivity
throw new Exception("no url specified");
}
HttpResponse httpResponse = rpc.getHttpResponse(dcContext.getAccountId(), url);
String mimeType = httpResponse.getMimetype();
String mimeType = httpResponse.mimetype;
if (mimeType == null) {
mimeType = "application/octet-stream";
}
res = new WebResourceResponse(mimeType, httpResponse.getEncoding(), new ByteArrayInputStream(httpResponse.getBlob()));
byte[] blob = JsonUtils.decodeBase64(httpResponse.blob);
res = new WebResourceResponse(mimeType, httpResponse.encoding, new ByteArrayInputStream(blob));
} catch (Exception e) {
e.printStackTrace();
res = new WebResourceResponse("text/plain", "UTF-8", new ByteArrayInputStream(("Error: " + e.getMessage()).getBytes()));
@@ -23,7 +23,6 @@ import androidx.loader.app.LoaderManager;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.rpc.RpcException;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
@@ -33,6 +32,7 @@ import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
import org.thoughtcrime.securesms.util.SelectedContactsAdapter;
@@ -44,6 +44,8 @@ import java.io.File;
import java.util.ArrayList;
import java.util.Objects;
import chat.delta.rpc.RpcException;
public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
implements ItemClickListener
{
@@ -340,6 +342,12 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
return ret;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@Override
public void onActivityResult(int reqCode, int resultCode, final Intent data) {
super.onActivityResult(reqCode, resultCode, data);
@@ -34,8 +34,6 @@ import androidx.loader.app.LoaderManager;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcLot;
import com.b44t.messenger.rpc.Rpc;
import com.b44t.messenger.rpc.RpcException;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
@@ -64,6 +62,9 @@ import java.io.IOException;
import java.security.SecureRandom;
import java.util.Objects;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class InstantOnboardingActivity extends BaseActionBarActivity implements DcEventCenter.DcEventDelegate {
private static final String TAG = InstantOnboardingActivity.class.getSimpleName();
@@ -89,7 +90,10 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
private AttachmentManager attachmentManager;
private Bitmap avatarBmp;
private @Nullable ProgressDialog progressDialog;
private boolean cancelled;
private DcContext dcContext;
@Override
@@ -422,7 +426,6 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
}
private void progressSuccess() {
DcHelper.getEventCenter(this).endCaptureNextError();
if (progressDialog != null) {
progressDialog.dismiss();
}
@@ -481,11 +484,14 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
progressDialog = null;
}
cancelled = false;
progressDialog = new ProgressDialog(this);
progressDialog.setMessage(getResources().getString(R.string.one_moment));
progressDialog.setCanceledOnTouchOutside(false);
progressDialog.setCancelable(false);
progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getResources().getString(android.R.string.cancel), (dialog, which) -> {
cancelled = true;
dcContext.stopOngoingProcess();
});
progressDialog.show();
@@ -496,9 +502,13 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
Rpc rpc = DcHelper.getRpc(this);
try {
rpc.addTransportFromQr(dcContext.getAccountId(), qrCode);
DcHelper.getEventCenter(this).endCaptureNextError();
progressSuccess();
} catch (RpcException e) {
Util.runOnMain(() -> progressError(e.getMessage()));
DcHelper.getEventCenter(this).endCaptureNextError();
if (!cancelled) {
Util.runOnMain(() -> progressError(e.getMessage()));
}
}
}).start();
}
@@ -23,8 +23,6 @@ import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.rpc.Rpc;
import com.b44t.messenger.rpc.RpcException;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
@@ -36,6 +34,9 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import java.io.File;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class ProfileActivity extends PassphraseRequiredActionBarActivity
implements DcEventCenter.DcEventDelegate
{
@@ -109,7 +110,7 @@ public class ProfileActivity extends PassphraseRequiredActionBarActivity
if (chatId != 0) {
DcChat dcChat = dcContext.getChat(chatId);
menu.findItem(R.id.menu_clone).setVisible(chatIsMultiUser && !chatIsMailingList);
menu.findItem(R.id.menu_clone).setVisible(chatIsMultiUser && !chatIsInBroadcast && !chatIsMailingList);
if (chatIsDeviceTalk) {
menu.findItem(R.id.edit_name).setVisible(false);
menu.findItem(R.id.show_encr_info).setVisible(false);
@@ -278,6 +278,7 @@ public class ProfileAdapter extends RecyclerView.Adapter
itemDataStatusText = "";
isOutBroadcast = dcChat != null && dcChat.isOutBroadcast();
boolean isMailingList = dcChat != null && dcChat.isMailingList();
boolean isInBroadcast = dcChat != null && dcChat.isInBroadcast();
boolean isSelfTalk = dcChat != null && dcChat.isSelfTalk();
boolean isDeviceTalk = dcChat != null && dcChat.isDeviceTalk();
memberCount = memberList!=null ? memberList.length : 0;
@@ -311,7 +312,7 @@ public class ProfileAdapter extends RecyclerView.Adapter
}
*/
if (memberList!=null && !isMailingList) {
if (memberList!=null && !isInBroadcast && !isMailingList) {
itemData.add(new ItemData(ITEM_DIVIDER, null, 0));
if (dcChat != null) {
if (dcChat.canSend() && dcChat.isEncrypted()) {
@@ -46,9 +46,6 @@ import androidx.constraintlayout.widget.Group;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcProvider;
import com.b44t.messenger.rpc.EnteredLoginParam;
import com.b44t.messenger.rpc.Rpc;
import com.b44t.messenger.rpc.RpcException;
import com.b44t.messenger.util.concurrent.ListenableFuture;
import com.b44t.messenger.util.concurrent.SettableFuture;
import com.google.android.material.textfield.TextInputEditText;
@@ -66,6 +63,12 @@ import org.thoughtcrime.securesms.util.views.ProgressDialog;
import java.lang.ref.WeakReference;
import java.util.concurrent.ExecutionException;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import chat.delta.rpc.types.EnteredCertificateChecks;
import chat.delta.rpc.types.EnteredLoginParam;
import chat.delta.rpc.types.Socket;
public class RegistrationActivity extends BaseActionBarActivity implements DcEventCenter.DcEventDelegate {
private enum VerificationType {
@@ -87,6 +90,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements DcEve
private Group advancedGroup;
private ImageView advancedIcon;
private ProgressDialog progressDialog;
private boolean cancelled = false;
Spinner imapSecurity;
Spinner smtpSecurity;
@@ -563,6 +567,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements DcEve
return;
}
cancelled = false;
setupConfig();
if (progressDialog != null) {
@@ -574,7 +579,10 @@ public class RegistrationActivity extends BaseActionBarActivity implements DcEve
progressDialog.setMessage(getString(R.string.one_moment));
progressDialog.setCanceledOnTouchOutside(false);
progressDialog.setCancelable(false);
progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getString(android.R.string.cancel), (dialog, which) -> stopLoginProcess());
progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getString(android.R.string.cancel), (dialog, which) -> {
cancelled = true;
DcHelper.getContext(this).stopOngoingProcess();
});
progressDialog.show();
}
@@ -584,24 +592,49 @@ public class RegistrationActivity extends BaseActionBarActivity implements DcEve
&& !passwordInput.getText().toString().isEmpty();
}
private EnteredCertificateChecks certificateChecksFromInt(int position) {
switch (position) {
case 0:
return EnteredCertificateChecks.automatic;
case 1:
return EnteredCertificateChecks.strict;
case 2:
return EnteredCertificateChecks.acceptInvalidCertificates;
}
throw new IllegalArgumentException("Invalid certificate position: " + position);
}
public static Socket socketSecurityFromInt(int position) {
switch (position) {
case 0:
return Socket.automatic;
case 1:
return Socket.ssl;
case 2:
return Socket.starttls;
case 3:
return Socket.plain;
}
throw new IllegalArgumentException("Invalid socketSecurity position: " + position);
}
private void setupConfig() {
DcHelper.getEventCenter(this).captureNextError();
EnteredLoginParam param = new EnteredLoginParam(
getParam(R.id.email_text, true),
getParam(R.id.password_text, false),
getParam(R.id.imap_server_text, true),
Util.objectToInt(getParam(R.id.imap_port_text, true)),
EnteredLoginParam.socketSecurityFromInt(imapSecurity.getSelectedItemPosition()),
getParam(R.id.imap_login_text, false),
getParam(R.id.smtp_server_text, true),
Util.objectToInt(getParam(R.id.smtp_port_text, true)),
EnteredLoginParam.socketSecurityFromInt(smtpSecurity.getSelectedItemPosition()),
getParam(R.id.smtp_login_text, false),
getParam(R.id.smtp_password_text, false),
EnteredLoginParam.certificateChecksFromInt(certCheck.getSelectedItemPosition()),
authMethod.getSelectedItemPosition() == 1
);
EnteredLoginParam param = new EnteredLoginParam();
param.addr = getParam(R.id.email_text, true);
param.password = getParam(R.id.password_text, false);
param.imapServer = getParam(R.id.imap_server_text, true);
param.imapPort = Util.objectToInt(getParam(R.id.imap_port_text, true));
param.imapSecurity = socketSecurityFromInt(imapSecurity.getSelectedItemPosition());
param.imapUser = getParam(R.id.imap_login_text, false);
param.smtpServer = getParam(R.id.smtp_server_text, true);
param.smtpPort = Util.objectToInt(getParam(R.id.smtp_port_text, true));
param.smtpSecurity = socketSecurityFromInt(smtpSecurity.getSelectedItemPosition());
param.smtpUser = getParam(R.id.smtp_login_text, false);
param.smtpPassword = getParam(R.id.smtp_password_text, false);
param.certificateChecks = certificateChecksFromInt(certCheck.getSelectedItemPosition());
param.oauth2 = authMethod.getSelectedItemPosition() == 1;
new Thread(() -> {
Rpc rpc = DcHelper.getRpc(this);
@@ -613,11 +646,13 @@ public class RegistrationActivity extends BaseActionBarActivity implements DcEve
startActivity(conversationList);
finish();
} catch (RpcException e) {
Util.runOnMain(() -> {
DcHelper.getEventCenter(this).endCaptureNextError();
progressDialog.dismiss();
WelcomeActivity.maybeShowConfigurationError(this, e.getMessage());
});
DcHelper.getEventCenter(this).endCaptureNextError();
if (!cancelled) {
Util.runOnMain(() -> {
progressDialog.dismiss();
WelcomeActivity.maybeShowConfigurationError(this, e.getMessage());
});
}
}
}).start();
}
@@ -631,10 +666,6 @@ public class RegistrationActivity extends BaseActionBarActivity implements DcEve
return value.isEmpty()? null : value;
}
private void stopLoginProcess() {
DcHelper.getContext(this).stopOngoingProcess();
}
@Override
public void handleEvent(@NonNull DcEvent event) {
if (event.getId()==DcContext.DC_EVENT_CONFIGURE_PROGRESS) {
@@ -38,8 +38,6 @@ import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcMsg;
import com.b44t.messenger.rpc.Rpc;
import com.b44t.messenger.rpc.RpcException;
import com.google.common.base.Charsets;
import org.json.JSONObject;
@@ -63,6 +61,9 @@ import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcEventDelegate {
private static final String TAG = WebxdcActivity.class.getSimpleName();
private static final String EXTRA_ACCOUNT_ID = "accountId";
@@ -17,13 +17,11 @@ import android.widget.Toast;
import androidx.appcompat.app.ActionBar;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.rpc.HttpResponse;
import com.b44t.messenger.rpc.Rpc;
import com.b44t.messenger.rpc.RpcException;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.util.IntentUtils;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.Util;
@@ -31,6 +29,10 @@ import org.thoughtcrime.securesms.util.Util;
import java.io.ByteArrayInputStream;
import java.util.HashMap;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import chat.delta.rpc.types.HttpResponse;
public class WebxdcStoreActivity extends PassphraseRequiredActionBarActivity {
private static final String TAG = WebxdcStoreActivity.class.getSimpleName();
@@ -59,7 +61,8 @@ public class WebxdcStoreActivity extends PassphraseRequiredActionBarActivity {
Util.runOnAnyBackgroundThread(() -> {
try {
HttpResponse httpResponse = rpc.getHttpResponse(dcContext.getAccountId(), url);
Uri uri = PersistentBlobProvider.getInstance().create(WebxdcStoreActivity.this, httpResponse.getBlob(), "application/octet-stream", "app.xdc");
byte[] blob = JsonUtils.decodeBase64(httpResponse.blob);
Uri uri = PersistentBlobProvider.getInstance().create(WebxdcStoreActivity.this, blob, "application/octet-stream", "app.xdc");
Intent intent = new Intent();
intent.setData(uri);
setResult(Activity.RESULT_OK, intent);
@@ -108,13 +111,14 @@ public class WebxdcStoreActivity extends PassphraseRequiredActionBarActivity {
throw new Exception("no url specified");
}
HttpResponse httpResponse = rpc.getHttpResponse(dcContext.getAccountId(), url);
String mimeType = httpResponse.getMimetype();
String mimeType = httpResponse.mimetype;
if (mimeType == null) {
mimeType = "application/octet-stream";
}
ByteArrayInputStream data = new ByteArrayInputStream(httpResponse.getBlob());
res = new WebResourceResponse(mimeType, httpResponse.getEncoding(), data);
byte[] blob = JsonUtils.decodeBase64(httpResponse.blob);
ByteArrayInputStream data = new ByteArrayInputStream(blob);
res = new WebResourceResponse(mimeType, httpResponse.encoding, data);
} catch (Exception e) {
e.printStackTrace();
ByteArrayInputStream data = new ByteArrayInputStream(("Could not load apps. Are you online?\n\n" + e.getMessage()).getBytes());
@@ -26,8 +26,6 @@ import com.b44t.messenger.DcAccounts;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.rpc.Rpc;
import com.b44t.messenger.rpc.RpcException;
import org.thoughtcrime.securesms.ConnectivityActivity;
import org.thoughtcrime.securesms.ConversationListActivity;
@@ -43,6 +41,9 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Arrays;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class AccountSelectionListFragment extends DialogFragment implements DcEventCenter.DcEventDelegate
{
private static final String TAG = AccountSelectionListFragment.class.getSimpleName();
@@ -0,0 +1,203 @@
package org.thoughtcrime.securesms.calls;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import android.view.Menu;
import android.webkit.JavascriptInterface;
import android.webkit.PermissionRequest;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import androidx.annotation.NonNull;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.WebViewActivity;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.Util;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class CallActivity extends WebViewActivity implements DcEventCenter.DcEventDelegate {
private static final String TAG = CallActivity.class.getSimpleName();
public static final String EXTRA_ACCOUNT_ID = "acc_id";
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";
private DcContext dcContext;
private Rpc rpc;
private int accId;
private int chatId;
private int callId;
private boolean ended = false;
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(Bundle state, boolean ready) {
super.onCreate(state, ready);
Bundle bundle = getIntent().getExtras();
assert bundle != null;
String hash = bundle.getString(EXTRA_HASH, "");
accId = bundle.getInt(EXTRA_ACCOUNT_ID, -1);
chatId = bundle.getInt(EXTRA_CHAT_ID, 0);
callId = bundle.getInt(EXTRA_CALL_ID, 0);
rpc = DcHelper.getRpc(this);
dcContext = DcHelper.getAccounts(this).getAccount(accId);
DcHelper.getNotificationCenter(this).removeCallNotification(accId, callId);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setMediaPlaybackRequiresUserGesture(false);
webView.addJavascriptInterface(new InternalJSApi(), "calls");
webView.setWebChromeClient(new WebChromeClient() {
@Override
public void onPermissionRequest(PermissionRequest request) {
Util.runOnMain(() -> request.grant(request.getResources()));
}
});
DcEventCenter eventCenter = DcHelper.getEventCenter(getApplicationContext());
eventCenter.addObserver(DcContext.DC_EVENT_OUTGOING_CALL_ACCEPTED, this);
eventCenter.addObserver(DcContext.DC_EVENT_CALL_ENDED, this);
Util.runOnAnyBackgroundThread(() -> {
final DcChat chat = dcContext.getChat(chatId);
Util.runOnMain(() -> Objects.requireNonNull(getSupportActionBar()).setTitle(chat.getName()));
});
Permissions.with(this)
.request(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_camera_denied))
.onAllGranted(() -> {
String url = "file:///android_asset/calls/index.html";
webView.loadUrl(url + hash);
}).onAnyDenied(this::finish)
.execute();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@Override
protected void onDestroy() {
DcHelper.getEventCenter(this).removeObservers(this);
if (callId != 0 && !ended) {
try {
rpc.endCall(accId, callId);
} catch (RpcException e) {
Log.e(TAG, "Error", e);
}
}
super.onDestroy();
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// do not call super.onPrepareOptionsMenu() as the default "Search" menu is not needed
return true;
}
@Override
protected boolean openOnlineUrl(String url) {
finish();
return true;
}
@Override
public void handleEvent(@NonNull DcEvent event) {
switch (event.getId()) {
case DcContext.DC_EVENT_OUTGOING_CALL_ACCEPTED:
if (event.getData1Int() == callId) {
try {
String base64 = Base64.encodeToString(event.getData2Str().getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP);
String hash = "#onAnswer=" + URLEncoder.encode(base64, "UTF-8");
webView.evaluateJavascript("window.location.hash = `"+hash+"`", null);
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Error", e);
}
}
break;
case DcContext.DC_EVENT_CALL_ENDED:
if (event.getData1Int() == callId) {
ended = true;
finish();
}
break;
}
}
class InternalJSApi {
@JavascriptInterface
public String getIceServers() {
try {
return rpc.iceServers(accId);
} catch (RpcException e) {
Log.e(TAG, "Error", e);
return null;
}
}
@JavascriptInterface
public void startCall(String payload) {
try {
callId = rpc.placeOutgoingCall(accId, chatId, payload);
} catch (RpcException e) {
Log.e(TAG, "Error", e);
}
}
@JavascriptInterface
public void acceptCall(String payload) {
try {
rpc.acceptIncomingCall(accId, callId, payload);
} catch (RpcException e) {
Log.e(TAG, "Error", e);
}
}
@JavascriptInterface
public void endCall() {
finish();
}
@JavascriptInterface
public String getAvatar() {
final Context context = CallActivity.this;
final DcChat dcChat = dcContext.getChat(chatId);
if (!TextUtils.isEmpty(dcChat.getProfileImage())) {
return AvatarUtil.asDataUri(dcChat.getProfileImage());
} else {
final Recipient recipient = new Recipient(context, dcChat);
return AvatarUtil.asDataUri(recipient.getFallbackAvatarDrawable(context, false));
}
}
}
}
@@ -0,0 +1,60 @@
package org.thoughtcrime.securesms.calls;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.util.Base64;
import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.permissions.Permissions;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public class CallUtil {
private static final String TAG = CallUtil.class.getSimpleName();
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);
})
.execute();
}
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_HASH, "#startCall");
context.startActivity(intent);
}
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 {
hash = "#offerIncomingCall=" + URLEncoder.encode(base64, "UTF-8");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Error", e);
}
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_CALL_ID, callId);
intent.putExtra(CallActivity.EXTRA_HASH, hash);
context.startActivity(intent);
}
}
@@ -35,19 +35,17 @@ public class AttachmentTypeSelector extends PopupWindow {
public static final int ADD_GALLERY = 1;
public static final int ADD_DOCUMENT = 2;
public static final int INVITE_VIDEO_CHAT = 3;
public static final int ADD_CONTACT_INFO = 4;
public static final int TAKE_PHOTO = 5;
public static final int ADD_LOCATION = 6;
public static final int RECORD_VIDEO = 7;
public static final int ADD_WEBXDC = 8;
public static final int ADD_CONTACT_INFO = 3;
public static final int TAKE_PHOTO = 4;
public static final int ADD_LOCATION = 5;
public static final int RECORD_VIDEO = 6;
public static final int ADD_WEBXDC = 7;
private static final int ANIMATION_DURATION = 300;
private final @NonNull LoaderManager loaderManager;
private final @NonNull RecentPhotoViewRail recentRail;
private final @NonNull ImageView imageButton;
private final @NonNull ImageView videoChatButton;
private final @NonNull ImageView documentButton;
private final @NonNull ImageView contactButton;
//private final @NonNull ImageView cameraButton;
@@ -70,7 +68,6 @@ public class AttachmentTypeSelector extends PopupWindow {
this.chatId = chatId;
this.recentRail = ViewUtil.findById(layout, R.id.recent_photos);
this.imageButton = ViewUtil.findById(layout, R.id.gallery_button);
this.videoChatButton = ViewUtil.findById(layout, R.id.invite_video_chat_button);
this.documentButton = ViewUtil.findById(layout, R.id.document_button);
this.contactButton = ViewUtil.findById(layout, R.id.contact_button);
//this.cameraButton = ViewUtil.findById(layout, R.id.camera_button);
@@ -79,7 +76,6 @@ public class AttachmentTypeSelector extends PopupWindow {
this.webxdcButton = ViewUtil.findById(layout, R.id.webxdc_button);
this.imageButton.setOnClickListener(new PropagatingClickListener(ADD_GALLERY));
this.videoChatButton.setOnClickListener(new PropagatingClickListener(INVITE_VIDEO_CHAT));
this.documentButton.setOnClickListener(new PropagatingClickListener(ADD_DOCUMENT));
this.contactButton.setOnClickListener(new PropagatingClickListener(ADD_CONTACT_INFO));
//this.cameraButton.setOnClickListener(new PropagatingClickListener(TAKE_PHOTO));
@@ -137,7 +133,6 @@ public class AttachmentTypeSelector extends PopupWindow {
animateButtonIn(locationButton, ANIMATION_DURATION / 4);
animateButtonIn(documentButton, ANIMATION_DURATION / 4);
animateButtonIn(webxdcButton, 0);
animateButtonIn(videoChatButton, 0);
}
@Override
@@ -0,0 +1,103 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import chat.delta.rpc.types.CallInfo;
import chat.delta.rpc.types.CallState;
public class CallItemView extends FrameLayout {
private static final String TAG = CallItemView.class.getSimpleName();
private final @NonNull ImageView icon;
private final @NonNull TextView title;
private final @NonNull ConversationItemFooter footer;
private CallInfo callInfo;
private CallClickListener viewListener;
public CallItemView(Context context) {
this(context, null);
}
public CallItemView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CallItemView(final Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
inflate(context, R.layout.call_item_view, this);
this.icon = findViewById(R.id.call_icon);
this.title = findViewById(R.id.title);
this.footer = findViewById(R.id.footer);
setOnClickListener(v -> {
if (viewListener != null && callInfo != null) {
viewListener.onClick(v, callInfo);
}
});
}
public void setCallClickListener(CallClickListener listener) {
viewListener = listener;
}
public void setCallItem(boolean isOutgoing, CallInfo callInfo) {
this.callInfo = callInfo;
if (callInfo.state instanceof CallState.Completed) {
footer.setCallDuration(((CallState.Completed) callInfo.state).duration);
} else {
footer.setCallDuration(0); // reset
}
if (callInfo.state instanceof CallState.Missed) {
title.setText(R.string.missed_call);
} else if (callInfo.state instanceof CallState.Canceled) {
title.setText(R.string.canceled_call);
} else if (callInfo.state instanceof CallState.Declined) {
title.setText(R.string.declined_call);
} else {
title.setText(isOutgoing? R.string.outgoing_call : R.string.incoming_call);
}
int[] attrs;
if (isOutgoing) {
attrs = new int[]{
R.attr.conversation_item_outgoing_text_primary_color,
R.attr.conversation_item_outgoing_text_secondary_color,
};
} else {
attrs = new int[]{
R.attr.conversation_item_incoming_text_primary_color,
R.attr.conversation_item_incoming_text_secondary_color,
};
}
try (TypedArray ta = getContext().obtainStyledAttributes(attrs)) {
icon.setColorFilter(ta.getColor(0, Color.BLACK));
footer.setTextColor(ta.getColor(1, Color.BLACK));
}
}
public ConversationItemFooter getFooter() {
return footer;
}
public String getDescription() {
return title.getText() + "\n" + footer.getDescription();
}
public interface CallClickListener {
void onClick(View v, CallInfo callInfo);
}
}
@@ -26,6 +26,7 @@ public class ConversationItemFooter extends LinearLayout {
private ImageView locationIndicatorView;
private DeliveryStatusView deliveryStatusView;
private Integer textColor = null;
private int callDuration = 0;
public ConversationItemFooter(Context context) {
super(context);
@@ -54,12 +55,16 @@ public class ConversationItemFooter extends LinearLayout {
if (attrs != null) {
try (TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemFooter, 0, 0)) {
textColor = typedArray.getInt(R.styleable.ConversationItemFooter_footer_text_color, getResources().getColor(R.color.core_white));
setTextColor(textColor);
setTextColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_text_color, getResources().getColor(R.color.core_white)));
}
}
}
/* Call duration in seconds. Only >0 if this is a call message */
public void setCallDuration(int duration) {
callDuration = duration;
}
public void setMessageRecord(@NonNull DcMsg messageRecord) {
presentDate(messageRecord);
boolean bookmark = messageRecord.getOriginalMsgId() != 0 || messageRecord.getSavedMsgId() != 0;
@@ -77,7 +82,8 @@ public class ConversationItemFooter extends LinearLayout {
presentDeliveryStatus(messageRecord);
}
private void setTextColor(int color) {
public void setTextColor(int color) {
textColor = color;
dateView.setTextColor(color);
editedView.setTextColor(color);
bookmarkIndicatorView.setColorFilter(color);
@@ -86,18 +92,29 @@ public class ConversationItemFooter extends LinearLayout {
deliveryStatusView.setTint(color);
}
private void presentDate(@NonNull DcMsg messageRecord) {
private void presentDate(@NonNull DcMsg dcMsg) {
dateView.forceLayout();
dateView.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), messageRecord.getTimestamp()));
Context context = getContext();
String date = dcMsg.getType() == DcMsg.DC_MSG_CALL?
DateUtils.getExtendedTimeSpanString(context, dcMsg.getTimestamp())
: DateUtils.getExtendedRelativeTimeSpanString(context, dcMsg.getTimestamp());
if (callDuration > 0) {
String duration = DateUtils.getFormattedCallDuration(context, callDuration);
dateView.setText(context.getString(R.string.call_date_and_duration, date, duration));
} else {
dateView.setText(date);
}
}
private void presentDeliveryStatus(@NonNull DcMsg messageRecord) {
// isDownloading is temporary and should be checked first.
boolean isDownloading = messageRecord.getDownloadState() == DcMsg.DC_DOWNLOAD_IN_PROGRESS;
boolean isCall = messageRecord.getType() == DcMsg.DC_MSG_CALL;
if (isDownloading) deliveryStatusView.setDownloading();
else if (messageRecord.isPending()) deliveryStatusView.setPending();
else if (messageRecord.isFailed()) deliveryStatusView.setFailed();
else if (!messageRecord.isOutgoing()) deliveryStatusView.setNone();
else if (!messageRecord.isOutgoing() || isCall) deliveryStatusView.setNone();
else if (messageRecord.isRemoteRead()) deliveryStatusView.setRead();
else if (messageRecord.isDelivered()) deliveryStatusView.setSent();
else if (messageRecord.isPreparing()) deliveryStatusView.setPreparing();
@@ -19,8 +19,6 @@ import androidx.annotation.Nullable;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcMsg;
import com.b44t.messenger.rpc.RpcException;
import com.b44t.messenger.rpc.VcardContact;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.json.JSONObject;
@@ -41,6 +39,9 @@ import org.thoughtcrime.securesms.util.Util;
import java.util.List;
import chat.delta.rpc.RpcException;
import chat.delta.rpc.types.VcardContact;
public class QuoteView extends FrameLayout implements RecipientForeverObserver {
private static final String TAG = QuoteView.class.getSimpleName();
@@ -9,16 +9,16 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.b44t.messenger.rpc.Rpc;
import com.b44t.messenger.rpc.RpcException;
import com.b44t.messenger.rpc.VcardContact;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.VcardSlide;
import org.thoughtcrime.securesms.recipients.Recipient;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import chat.delta.rpc.types.VcardContact;
public class VcardView extends FrameLayout {
private static final String TAG = VcardView.class.getSimpleName();
@@ -60,8 +60,8 @@ public class VcardView extends FrameLayout {
public void setVcard(@NonNull GlideRequests glideRequests, final @NonNull VcardSlide slide, final @NonNull Rpc rpc) {
try {
VcardContact vcardContact = rpc.parseVcard(slide.asAttachment().getRealPath(getContext())).get(0);
name.setText(vcardContact.getDisplayName());
address.setText(vcardContact.getAddr());
name.setText(vcardContact.displayName);
address.setText(vcardContact.addr);
avatar.setAvatar(glideRequests, new Recipient(getContext(), vcardContact), false);
this.slide = slide;
} catch (RpcException e) {
@@ -109,18 +109,9 @@ public class DozeReminder {
}
}
private static boolean isAllChatmail() {
for (int accountId : ApplicationContext.dcAccounts.getAll()) {
DcContext context = ApplicationContext.dcAccounts.getAccount(accountId);
if (!context.isChatmail()) {
return false;
}
}
return true;
}
private static boolean isPushAvailableAndSufficient() {
return isAllChatmail() && FcmReceiveService.getToken() != null;
return ApplicationContext.dcAccounts.isAllChatmail()
&& FcmReceiveService.getToken() != null;
}
public static void maybeAskDirectly(Context context) {
@@ -13,8 +13,6 @@ import androidx.preference.PreferenceManager;
import com.b44t.messenger.DcAccounts;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.rpc.Rpc;
import com.b44t.messenger.rpc.RpcException;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ConversationListActivity;
@@ -24,6 +22,9 @@ import org.thoughtcrime.securesms.accounts.AccountSelectionListFragment;
import java.io.File;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class AccountManager {
private static final String TAG = AccountManager.class.getSimpleName();
@@ -183,6 +183,15 @@ public class DcEventCenter {
DcHelper.getNotificationCenter(context).notifyWebxdc(accountId, event.getData1Int(), event.getData2Int(), event.getData2Str());
break;
case DcContext.DC_EVENT_INCOMING_CALL:
DcHelper.getNotificationCenter(context).notifyCall(accountId, event.getData1Int(), event.getData2Str());
break;
case DcContext.DC_EVENT_INCOMING_CALL_ACCEPTED:
case DcContext.DC_EVENT_CALL_ENDED:
DcHelper.getNotificationCenter(context).removeCallNotification(accountId, event.getData1Int());
break;
case DcContext.DC_EVENT_MSGS_NOTICED:
DcHelper.getNotificationCenter(context).removeNotifications(accountId, event.getData1Int());
break;
@@ -22,8 +22,6 @@ import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcLot;
import com.b44t.messenger.DcMsg;
import com.b44t.messenger.rpc.Rpc;
import com.b44t.messenger.rpc.RpcException;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.BuildConfig;
@@ -43,6 +41,9 @@ import java.io.File;
import java.util.Date;
import java.util.HashMap;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class DcHelper {
private static final String TAG = DcHelper.class.getSimpleName();
@@ -69,16 +70,12 @@ public class DcHelper {
public static final String CONFIG_BCC_SELF = "bcc_self";
public static final String CONFIG_SHOW_EMAILS = "show_emails";
public static final String CONFIG_MEDIA_QUALITY = "media_quality";
public static final String CONFIG_WEBRTC_INSTANCE = "webrtc_instance";
public static final String CONFIG_PROXY_ENABLED = "proxy_enabled";
public static final String CONFIG_PROXY_URL = "proxy_url";
public static final String CONFIG_VERIFIED_ONE_ON_ONE_CHATS = "verified_one_on_one_chats";
public static final String CONFIG_WEBXDC_REALTIME_ENABLED = "webxdc_realtime_enabled";
public static final String CONFIG_PRIVATE_TAG = "private_tag";
public static final String DEFAULT_VIDEOCHAT_URL_PREFIX = "https://meet.systemli.org/";
public static final String DEFAULT_VIDEOCHAT_URL = DEFAULT_VIDEOCHAT_URL_PREFIX + "$ROOM#config.prejoinConfig.enabled=false&config.notifications=[]&config.toolbarButtons=[%22microphone%22,%22camera%22,%22hangup%22]";
public static DcContext getContext(@NonNull Context context) {
return ApplicationContext.getInstance(context).dcContext;
}
@@ -247,6 +244,11 @@ public class DcHelper {
dcContext.setStockTranslation(177, context.getString(R.string.reaction_by_other));
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));
}
public static File getImexDir() {
@@ -411,14 +413,6 @@ public class DcHelper {
}
public static String getVideochatURL(DcContext dcContext) {
String instance = dcContext.getConfig(DcHelper.CONFIG_WEBRTC_INSTANCE);
if (instance != null && !instance.isEmpty()) {
return instance;
}
return DEFAULT_VIDEOCHAT_URL;
}
@NonNull
public static ThreadRecord getThreadRecord(Context context, DcLot summary, DcChat chat) { // adapted from ThreadDatabase.getCurrent()
int chatId = chat.getId();
@@ -6,7 +6,7 @@ import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.b44t.messenger.rpc.VcardContact;
import org.thoughtcrime.securesms.util.JsonUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@@ -14,6 +14,8 @@ import java.io.InputStream;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import chat.delta.rpc.types.VcardContact;
public class VcardContactPhoto implements ContactPhoto {
private final VcardContact vContact;
@@ -23,7 +25,7 @@ public class VcardContactPhoto implements ContactPhoto {
@Override
public InputStream openInputStream(Context context) throws IOException {
byte[] blob = vContact.getProfileImage();
byte[] blob = JsonUtils.decodeBase64(vContact.profileImage);
return (blob == null)? null : new ByteArrayInputStream(blob);
}
@@ -39,7 +41,7 @@ public class VcardContactPhoto implements ContactPhoto {
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update(vContact.getAddr().getBytes());
messageDigest.update(ByteBuffer.allocate(4).putFloat(vContact.getTimestamp()).array());
messageDigest.update(vContact.addr.getBytes());
messageDigest.update(ByteBuffer.allocate(4).putFloat(vContact.timestamp).array());
}
}
@@ -40,7 +40,6 @@ import androidx.appcompat.app.AlertDialog;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import com.b44t.messenger.rpc.RpcException;
import com.b44t.messenger.util.concurrent.ListenableFuture;
import com.b44t.messenger.util.concurrent.ListenableFuture.Listener;
import com.b44t.messenger.util.concurrent.SettableFuture;
@@ -78,6 +77,8 @@ import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import chat.delta.rpc.RpcException;
public class AttachmentManager {
@@ -0,0 +1,41 @@
package org.thoughtcrime.securesms.notifications;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.util.Util;
import chat.delta.rpc.RpcException;
public class DeclineCallReceiver extends BroadcastReceiver {
private static final String TAG = DeclineCallReceiver.class.getSimpleName();
public static final String DECLINE_ACTION = "org.thoughtcrime.securesms.notifications.DECLINE_CALL";
public static final String ACCOUNT_ID_EXTRA = "account_id";
public static final String CALL_ID_EXTRA = "call_id";
@Override
public void onReceive(final Context context, Intent intent) {
if (!DECLINE_ACTION.equals(intent.getAction())) {
return;
}
final int accountId = intent.getIntExtra(ACCOUNT_ID_EXTRA, 0);
final int callId = intent.getIntExtra(CALL_ID_EXTRA, 0);
if (accountId == 0) {
return;
}
Util.runOnAnyBackgroundThread(() -> {
DcHelper.getNotificationCenter(context).removeCallNotification(accountId, callId);
try {
DcHelper.getRpc(context).endCall(accountId, callId);
} catch (RpcException e) {
Log.e(TAG, "Error", e);
}
});
}
}
@@ -13,9 +13,11 @@ import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.media.AudioAttributes;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -47,8 +49,12 @@ import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.Pair;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.calls.CallActivity;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HashMap;
@@ -158,6 +164,41 @@ 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) {
final Intent chatIntent = new Intent(context, ConversationActivity.class)
.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, chatData.accountId)
.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatData.chatId)
.setAction(Intent.ACTION_VIEW);
String base64 = Base64.encodeToString(payload.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP);
String hash = "";
try {
hash = (autoAccept? "#acceptCall=" : "#offerIncomingCall=") + URLEncoder.encode(base64, "UTF-8");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Error", e);
}
Intent intent = new Intent(context, CallActivity.class);
intent.setAction(autoAccept? Intent.ACTION_ANSWER : Intent.ACTION_VIEW);
intent.putExtra(CallActivity.EXTRA_ACCOUNT_ID, chatData.accountId);
intent.putExtra(CallActivity.EXTRA_CHAT_ID, chatData.chatId);
intent.putExtra(CallActivity.EXTRA_CALL_ID, callId);
intent.putExtra(CallActivity.EXTRA_HASH, hash);
intent.setPackage(context.getPackageName());
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(chatIntent)
.addNextIntent(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT | IntentUtils.FLAG_MUTABLE());
}
public PendingIntent getDeclineCallIntent(ChatData chatData, int callId) {
Intent intent = new Intent(DeclineCallReceiver.DECLINE_ACTION);
intent.setClass(context, DeclineCallReceiver.class);
intent.putExtra(DeclineCallReceiver.ACCOUNT_ID_EXTRA, chatData.accountId);
intent.putExtra(DeclineCallReceiver.CALL_ID_EXTRA, callId);
intent.setPackage(context.getPackageName());
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | IntentUtils.FLAG_MUTABLE());
}
// Groups and Notification channel groups
// --------------------------------------------------------------------------------------------
@@ -202,6 +243,7 @@ public class NotificationCenter {
public static final String CH_MSG_VERSION = "5";
public static final String CH_PERMANENT = "dc_foreground_notification_ch";
public static final String CH_GENERIC = "ch_generic";
public static final String CH_CALLS_PREFIX = "call_chan";
private boolean notificationChannelsSupported() {
return Build.VERSION.SDK_INT >= 26;
@@ -333,10 +375,107 @@ public class NotificationCenter {
return channelId;
}
public String getCallNotificationChannel(NotificationManagerCompat notificationManager, ChatData chatData, String name) {
String channelId = CH_CALLS_PREFIX + "-" + chatData.accountId + "-"+ chatData.chatId;
if (notificationChannelsSupported()) {
try {
name = "(calls) " + name;
// check if there is already a channel with the given name
List<NotificationChannel> channels = notificationManager.getNotificationChannels();
boolean channelExists = false;
for (int i = 0; i < channels.size(); i++) {
String currChannelId = channels.get(i).getId();
if (currChannelId.startsWith(CH_CALLS_PREFIX)) {
// this is one of the calls channels handled here ...
if (currChannelId.equals(channelId)) {
// ... this is the actually required channel, fine :)
// update the name to reflect localize changes and chat renames
channelExists = true;
channels.get(i).setName(name);
}
}
}
// create a the channel
if(!channelExists) {
NotificationChannel channel = new NotificationChannel(channelId, name, NotificationManager.IMPORTANCE_MAX);
channel.setDescription("Informs about incoming calls.");
channel.setShowBadge(true);
Uri ringtone = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
channel.setSound(ringtone, new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build());
notificationManager.createNotificationChannel(channel);
}
} catch(Exception e) {
Log.e(TAG, "Error in getCallNotificationChannel()", e);
}
}
return channelId;
}
// add notifications & co.
// --------------------------------------------------------------------------------------------
public void notifyCall(int accId, int callId, String payload) {
Util.runOnAnyBackgroundThread(() -> {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
DcContext dcContext = context.dcAccounts.getAccount(accId);
int chatId = dcContext.getMsg(callId).getChatId();
DcChat dcChat = dcContext.getChat(chatId);
String name = dcChat.getName();
ChatData chatData = new ChatData(accId, chatId);
String notificationChannel = getCallNotificationChannel(notificationManager, chatData, name);
PendingIntent declineCallIntent = getDeclineCallIntent(chatData, callId);
PendingIntent openCallIntent = getOpenCallIntent(chatData, callId, payload, false);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, notificationChannel)
.setSmallIcon(R.drawable.icon_notification)
.setColor(context.getResources().getColor(R.color.def_accent))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setOngoing(true)
.setOnlyAlertOnce(false)
.setTicker(name)
.setContentTitle(name)
.setFullScreenIntent(openCallIntent, true)
.setContentIntent(openCallIntent)
.setContentText("Incoming Call");
builder.addAction(
new NotificationCompat.Action.Builder(
R.drawable.baseline_call_end_24,
context.getString(R.string.end_call),
declineCallIntent).build());
builder.addAction(
new NotificationCompat.Action.Builder(
R.drawable.baseline_call_24,
context.getString(R.string.answer_call),
getOpenCallIntent(chatData, callId, payload, true)).build());
Bitmap bitmap = getAvatar(dcChat);
if (bitmap != null) {
builder.setLargeIcon(bitmap);
}
Notification notif = builder.build();
notif.flags = notif.flags | Notification.FLAG_INSISTENT;
try {
notificationManager.notify("call-" + accId, callId, notif);
} catch (Exception e) {
Log.e(TAG, "cannot add notification", e);
}
});
}
public void notifyMessage(int accountId, int chatId, int msgId) {
Util.runOnAnyBackgroundThread(() -> {
DcContext dcContext = context.dcAccounts.getAccount(accountId);
@@ -495,31 +634,11 @@ public class NotificationCenter {
}
// set avatar
Recipient recipient = new Recipient(context, dcChat);
if (privacy.isDisplayContact()) {
try {
Drawable drawable;
ContactPhoto contactPhoto = recipient.getContactPhoto(context);
if (contactPhoto != null) {
drawable = GlideApp.with(context.getApplicationContext())
.load(contactPhoto)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.submit(context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height))
.get();
} else {
drawable = recipient.getFallbackContactPhoto().asDrawable(context, recipient.getFallbackAvatarColor());
}
if (drawable != null) {
int wh = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size);
Bitmap bitmap = BitmapUtil.createFromDrawable(drawable, wh, wh);
if (bitmap != null) {
builder.setLargeIcon(bitmap);
}
}
} catch (Exception e) { Log.w(TAG, e); }
Bitmap bitmap = getAvatar(dcChat);
if (bitmap != null) {
builder.setLargeIcon(bitmap);
}
}
// add buttons that allow some actions without opening ArcaneChat.
@@ -619,6 +738,40 @@ public class NotificationCenter {
}
}
public Bitmap getAvatar(DcChat dcChat) {
Recipient recipient = new Recipient(context, dcChat);
try {
Drawable drawable;
ContactPhoto contactPhoto = recipient.getContactPhoto(context);
if (contactPhoto != null) {
drawable = GlideApp.with(context.getApplicationContext())
.load(contactPhoto)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.submit(context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height))
.get();
} else {
drawable = recipient.getFallbackContactPhoto().asDrawable(context, recipient.getFallbackAvatarColor());
}
if (drawable != null) {
int wh = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size);
return BitmapUtil.createFromDrawable(drawable, wh, wh);
}
} catch (Exception e) { Log.w(TAG, e); }
return null;
}
public void removeCallNotification(int accountId, int callId) {
try {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
String tag = "call-" + accountId;
notificationManager.cancel(tag, callId);
} catch (Exception e) { Log.w(TAG, e); }
}
public void removeNotifications(int accountId, int chatId) {
boolean removeSummary;
synchronized (inboxes) {
@@ -692,7 +845,7 @@ public class NotificationCenter {
}
}
private static class ChatData {
public static class ChatData {
public final int accountId;
public final int chatId;
@@ -27,8 +27,6 @@ import androidx.preference.CheckBoxPreference;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import com.b44t.messenger.rpc.RpcException;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.LogViewActivity;
@@ -49,6 +47,8 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.util.Objects;
import chat.delta.rpc.RpcException;
public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
implements DcEventCenter.DcEventDelegate
@@ -132,12 +132,6 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
submitDebugLog.setOnPreferenceClickListener(new ViewLogListener());
}
Preference webrtcInstance = this.findPreference("pref_webrtc_instance");
if (webrtcInstance != null) {
webrtcInstance.setOnPreferenceClickListener(new WebrtcInstanceListener());
}
updateWebrtcSummary();
Preference webxdcStore = this.findPreference(Prefs.WEBXDC_STORE_URL_PREF);
if (webxdcStore != null) {
webxdcStore.setOnPreferenceClickListener(new WebxdcStoreUrlListener());
@@ -259,29 +253,6 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
}
}
private class WebrtcInstanceListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(@NonNull Preference preference) {
View gl = View.inflate(requireActivity(), R.layout.single_line_input, null);
EditText inputField = gl.findViewById(R.id.input_field);
inputField.setHint(R.string.videochat_instance_placeholder);
inputField.setText(DcHelper.getVideochatURL(dcContext));
inputField.setSelection(inputField.getText().length());
inputField.setInputType(TYPE_TEXT_VARIATION_URI);
new AlertDialog.Builder(requireActivity())
.setTitle(R.string.videochat_instance)
.setMessage(getString(R.string.videochat_instance_explain_2)+"\n\n"+getString(R.string.videochat_instance_example))
.setView(gl)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
dcContext.setConfig(DcHelper.CONFIG_WEBRTC_INSTANCE, inputField.getText().toString());
updateWebrtcSummary();
})
.show();
return true;
}
}
private class WebxdcStoreUrlListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(@NonNull Preference preference) {
@@ -305,13 +276,6 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
}
}
private void updateWebrtcSummary() {
Preference webrtcInstance = this.findPreference("pref_webrtc_instance");
if (webrtcInstance != null) {
webrtcInstance.setSummary(DcHelper.getVideochatURL(dcContext));
}
}
private void updateWebxdcStoreSummary() {
Preference preference = this.findPreference(Prefs.WEBXDC_STORE_URL_PREF);
if (preference != null) {
@@ -28,9 +28,10 @@ import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.connect.KeepAliveService;
import org.thoughtcrime.securesms.notifications.FcmReceiveService;
import org.thoughtcrime.securesms.util.Prefs;
public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragment {
public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragment implements Preference.OnPreferenceChangeListener {
private static final String TAG = NotificationsPreferenceFragment.class.getSimpleName();
private static final int REQUEST_CODE_NOTIFICATION_SELECTED = 1;
@@ -38,6 +39,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
private CheckBoxPreference ignoreBattery;
private CheckBoxPreference notificationsEnabled;
private CheckBoxPreference mentionNotifEnabled;
private CheckBoxPreference reliableService;
@Override
public void onCreate(Bundle paramBundle) {
@@ -85,18 +87,12 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
}
CheckBoxPreference reliableService = this.findPreference("pref_reliable_service");
// reliableService is just used for displaying the actual value
// of the reliable service preference that is managed via
// Prefs.setReliableService() and Prefs.reliableService()
reliableService = this.findPreference("pref_reliable_service2");
if (reliableService != null) {
reliableService.setOnPreferenceChangeListener((preference, newValue) -> {
Context context = getContext();
boolean enabled = (Boolean) newValue; // Prefs.reliableService() still has the old value
if (enabled) {
KeepAliveService.startSelf(context);
} else {
context.stopService(new Intent(context, KeepAliveService.class));
}
return true;
});
reliableService.setOnPreferenceChangeListener(this);
}
notificationsEnabled = this.findPreference("pref_enable_notifications");
@@ -104,6 +100,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
notificationsEnabled.setOnPreferenceChangeListener((preference, newValue) -> {
boolean enabled = (Boolean) newValue;
dcContext.setMuted(!enabled);
notificationsEnabled.setSummary(getSummary(getContext(), false));
return true;
});
}
@@ -131,7 +128,13 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
// update ignoreBattery in onResume() to reflects changes done in the system settings
ignoreBattery.setChecked(isIgnoringBatteryOptimizations());
notificationsEnabled.setChecked(!dcContext.isMuted());
notificationsEnabled.setSummary(getSummary(getContext(), false));
mentionNotifEnabled.setChecked(dcContext.isMentionsEnabled());
// set without altering "unset" state of the preference
reliableService.setOnPreferenceChangeListener(null);
reliableService.setChecked(Prefs.reliableService(getActivity()));
reliableService.setOnPreferenceChangeListener(this);
}
@Override
@@ -150,6 +153,20 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
}
}
@Override
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
Context context = getContext();
boolean enabled = (Boolean) newValue;
Prefs.setReliableService(context, enabled);
if (enabled) {
KeepAliveService.startSelf(context);
} else {
context.stopService(new Intent(context, KeepAliveService.class));
}
notificationsEnabled.setSummary(getSummary(context, false));
return true;
}
private class RingtoneSummaryListener implements Preference.OnPreferenceChangeListener {
@Override
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
@@ -226,11 +243,21 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
}
public static CharSequence getSummary(Context context) {
return getSummary(context, true);
}
public static CharSequence getSummary(Context context, boolean detailed) {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || notificationManager.areNotificationsEnabled()) {
return context.getString(DcHelper.getContext(context).isMuted() ? R.string.off : R.string.on);
if (DcHelper.getContext(context).isMuted()) {
return detailed? context.getString(R.string.off) : "";
}
if (FcmReceiveService.getToken() == null && !Prefs.reliableService(context)) {
return "⚠️ " + context.getString(R.string.unreliable_bg_notifications);
}
return detailed? context.getString(R.string.on) : "";
} else {
return context.getString(R.string.disabled_in_system_settings);
return "⚠️ " + context.getString(R.string.disabled_in_system_settings);
}
}
}
@@ -13,16 +13,19 @@ import androidx.emoji2.emojipicker.EmojiPickerView;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import com.b44t.messenger.rpc.Reactions;
import com.b44t.messenger.rpc.Rpc;
import com.b44t.messenger.rpc.RpcException;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import chat.delta.rpc.types.Reactions;
public class AddReactionView extends LinearLayout {
private AppCompatTextView[] defaultReactionViews;
private AppCompatTextView anyReactionView;
@@ -66,7 +69,6 @@ public class AddReactionView extends LinearLayout {
init(); // init delayed as needed
if ( msgToReactTo.isInfo()
|| msgToReactTo.getType() == DcMsg.DC_MSG_VIDEOCHAT_INVITATION
|| !dcContext.getChat(msgToReactTo.getChatId()).canSend()) {
return;
}
@@ -123,12 +125,12 @@ public class AddReactionView extends LinearLayout {
private String getSelfReaction() {
String result = null;
try {
final Reactions reactions = rpc.getMsgReactions(dcContext.getAccountId(), msgToReactTo.getId());
final Reactions reactions = rpc.getMessageReactions(dcContext.getAccountId(), msgToReactTo.getId());
if (reactions != null) {
final Map<Integer, String[]> reactionsByContact = reactions.getReactionsByContact();
final String [] selfReactions = reactionsByContact.get(DcContact.DC_CONTACT_ID_SELF);
if (selfReactions != null && selfReactions.length > 0) {
result = selfReactions[0];
final Map<String, List<String>> reactionsByContact = reactions.reactionsByContact;
final List<String> selfReactions = reactionsByContact.get(String.valueOf(DcContact.DC_CONTACT_ID_SELF));
if (selfReactions != null && !selfReactions.isEmpty()) {
result = selfReactions.get(0);
}
}
} catch(RpcException e) {
@@ -175,9 +177,9 @@ public class AddReactionView extends LinearLayout {
private void sendReaction(final String reaction) {
try {
if (reaction == null || reaction.equals(getSelfReaction())) {
rpc.sendReaction(dcContext.getAccountId(), msgToReactTo.getId(), "");
rpc.sendReaction(dcContext.getAccountId(), msgToReactTo.getId(), Collections.singletonList(""));
} else {
rpc.sendReaction(dcContext.getAccountId(), msgToReactTo.getId(), reaction);
rpc.sendReaction(dcContext.getAccountId(), msgToReactTo.getId(), Collections.singletonList(reaction));
}
} catch(Exception e) {
e.printStackTrace();
@@ -14,14 +14,14 @@ import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.core.content.ContextCompat;
import com.b44t.messenger.rpc.Reaction;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.ArrayList;
import java.util.List;
import chat.delta.rpc.types.Reaction;
public class ReactionsConversationView extends LinearLayout {
// Normally 6dp, but we have 1dp left+right margin on the pills themselves
@@ -80,10 +80,14 @@ public class ReactionsConversationView extends LinearLayout {
int count = 0;
boolean isFromSelf = false;
for (int index = 2; index < reactions.size(); index++) {
count += reactions.get(index).getCount();
isFromSelf = isFromSelf || reactions.get(index).isFromSelf();
count += reactions.get(index).count;
isFromSelf = isFromSelf || reactions.get(index).isFromSelf;
}
shortened.add(new Reaction(null, count, isFromSelf));
Reaction reaction = new Reaction();
reaction.emoji = null;
reaction.count = count;
reaction.isFromSelf = isFromSelf;
shortened.add(reaction);
return shortened;
} else {
@@ -97,11 +101,11 @@ public class ReactionsConversationView extends LinearLayout {
TextView countView = root.findViewById(R.id.reactions_pill_count);
View spacer = root.findViewById(R.id.reactions_pill_spacer);
if (reaction.getEmoji() != null) {
emojiView.setText(reaction.getEmoji());
if (reaction.emoji != null) {
emojiView.setText(reaction.emoji);
if (reaction.getCount() > 1) {
countView.setText(String.valueOf(reaction.getCount()));
if (reaction.count > 1) {
countView.setText(String.valueOf(reaction.count));
} else {
countView.setVisibility(GONE);
spacer.setVisibility(GONE);
@@ -109,10 +113,10 @@ public class ReactionsConversationView extends LinearLayout {
} else {
emojiView.setVisibility(GONE);
spacer.setVisibility(GONE);
countView.setText("+" + reaction.getCount());
countView.setText("+" + reaction.count);
}
if (reaction.isFromSelf()) {
if (reaction.isFromSelf) {
root.setBackground(ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected));
countView.setTextColor(ContextCompat.getColor(context, R.color.reaction_pill_text_color_selected));
} else {
@@ -16,9 +16,6 @@ import androidx.recyclerview.widget.RecyclerView;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.rpc.Reactions;
import com.b44t.messenger.rpc.Rpc;
import com.b44t.messenger.rpc.RpcException;
import org.thoughtcrime.securesms.ProfileActivity;
import org.thoughtcrime.securesms.R;
@@ -29,8 +26,14 @@ import org.thoughtcrime.securesms.util.Pair;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import chat.delta.rpc.types.Reactions;
public class ReactionsDetailsFragment extends DialogFragment implements DcEventCenter.DcEventDelegate {
private static final String TAG = ReactionsDetailsFragment.class.getSimpleName();
@@ -87,14 +90,14 @@ public class ReactionsDetailsFragment extends DialogFragment implements DcEventC
int accId = DcHelper.getContext(requireActivity()).getAccountId();
try {
final Reactions reactions = DcHelper.getRpc(requireActivity()).getMsgReactions(accId, msgId);
final Reactions reactions = DcHelper.getRpc(requireActivity()).getMessageReactions(accId, msgId);
ArrayList<Pair<Integer, String>> contactsReactions = new ArrayList<>();
if (reactions != null) {
Map<Integer, String[]> reactionsByContact = reactions.getReactionsByContact();
String[] selfReactions = reactionsByContact.remove(DcContact.DC_CONTACT_ID_SELF);
for (Integer contact: reactionsByContact.keySet()) {
Map<String, List<String>> reactionsByContact = reactions.reactionsByContact;
List<String> selfReactions = reactionsByContact.remove(String.valueOf(DcContact.DC_CONTACT_ID_SELF));
for (String contact: reactionsByContact.keySet()) {
for (String reaction: reactionsByContact.get(contact)) {
contactsReactions.add(new Pair<>(contact, reaction));
contactsReactions.add(new Pair<>(Integer.parseInt(contact), reaction));
}
}
if (selfReactions != null) {
@@ -118,12 +121,12 @@ public class ReactionsDetailsFragment extends DialogFragment implements DcEventC
private String getSelfReaction(Rpc rpc, int accId) {
String result = null;
try {
final Reactions reactions = rpc.getMsgReactions(accId, msgId);
final Reactions reactions = rpc.getMessageReactions(accId, msgId);
if (reactions != null) {
final Map<Integer, String[]> reactionsByContact = reactions.getReactionsByContact();
final String [] selfReactions = reactionsByContact.get(DcContact.DC_CONTACT_ID_SELF);
if (selfReactions != null && selfReactions.length > 0) {
result = selfReactions[0];
final Map<String, List<String>> reactionsByContact = reactions.reactionsByContact;
final List<String> selfReactions = reactionsByContact.get(String.valueOf(DcContact.DC_CONTACT_ID_SELF));
if (selfReactions != null && !selfReactions.isEmpty()) {
result = selfReactions.get(0);
}
}
} catch(RpcException e) {
@@ -139,9 +142,9 @@ public class ReactionsDetailsFragment extends DialogFragment implements DcEventC
try {
if (reaction == null || reaction.equals(getSelfReaction(rpc, accId))) {
rpc.sendReaction(accId, msgId, "");
rpc.sendReaction(accId, msgId, Collections.singletonList(""));
} else {
rpc.sendReaction(accId, msgId, reaction);
rpc.sendReaction(accId, msgId, Collections.singletonList(reaction));
}
} catch(Exception e) {
e.printStackTrace();
@@ -29,7 +29,6 @@ import androidx.annotation.Nullable;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.rpc.VcardContact;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
@@ -50,6 +49,8 @@ import java.util.HashSet;
import java.util.Set;
import java.util.WeakHashMap;
import chat.delta.rpc.types.VcardContact;
public class Recipient {
private final Set<RecipientModifiedListener> listeners = Collections.newSetFromMap(new WeakHashMap<RecipientModifiedListener, Boolean>());
@@ -149,7 +150,7 @@ public class Recipient {
return dcContact.getDisplayName();
}
else if(vContact!=null) {
return vContact.getDisplayName();
return vContact.displayName;
}
return "";
}
@@ -199,7 +200,7 @@ public class Recipient {
rgb = dcContact.getColor();
}
else if(vContact!=null) {
rgb = Color.parseColor(vContact.getColor());
rgb = Color.parseColor(vContact.color);
}
return Color.argb(0xFF, Color.red(rgb), Color.green(rgb), Color.blue(rgb));
}
@@ -235,7 +236,7 @@ public class Recipient {
}
}
if (vContact!=null && vContact.hasProfileImage()) {
if (vContact!=null && vContact.profileImage != null) {
return new VcardContactPhoto(vContact);
}
@@ -0,0 +1,51 @@
package org.thoughtcrime.securesms.util;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.util.Base64;
import java.io.ByteArrayOutputStream;
public class AvatarUtil {
/**
* convert image path to data URI.
*
* @param filePath File path to image
* @return data URI like "data:image/jpeg;base64,..."
*/
public static String asDataUri(String filePath) {
Bitmap bitmap = BitmapFactory.decodeFile(filePath);
if (bitmap == null) return null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, baos);
byte[] bytes = baos.toByteArray();
String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
return "data:image/jpeg;base64," + base64;
}
/**
* convert Drawable to data URI.
*
* @param drawable avatar image as Drawable
* @return data URI like "data:image/png;base64,..."
*/
public static String asDataUri(Drawable drawable) {
int w = Math.max(1, drawable.getIntrinsicWidth());
int h = Math.max(1, drawable.getIntrinsicHeight());
Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, w, h);
drawable.draw(canvas);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, baos);
byte[] bytes = baos.toByteArray();
String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
return "data:image/jpeg;base64," + base64;
}
}
@@ -119,6 +119,15 @@ public class DateUtils extends android.text.format.DateUtils {
TimeUnit.MILLISECONDS.toSeconds(millis-(TimeUnit.MILLISECONDS.toMinutes(millis)*60000)));
}
public static String getFormattedCallDuration(Context c, int seconds) {
if (seconds < 60) {
return c.getResources().getQuantityString(R.plurals.n_seconds_ext, seconds, seconds);
}
int mins = seconds / 60;
return c.getResources().getQuantityString(R.plurals.n_minutes_ext, mins, mins);
}
public static String getFormattedTimespan(Context c, int timestamp) {
int mins = timestamp / (1000 * 60);
if (mins / 60 == 0) {
@@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.util;
import android.util.Base64;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
@@ -21,6 +23,13 @@ public class JsonUtils {
objectMapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);
}
public static byte[] decodeBase64(String base64) {
if (base64 == null) {
return null;
}
return Base64.decode(base64, Base64.NO_WRAP | Base64.NO_PADDING);
}
public static <T> T fromJson(byte[] serialized, Class<T> clazz) throws IOException {
return fromJson(new String(serialized), clazz);
}
@@ -12,10 +12,12 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import com.b44t.messenger.DcAccounts;
import com.b44t.messenger.DcContext;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.notifications.FcmReceiveService;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import java.util.ArrayList;
@@ -28,6 +30,7 @@ public class Prefs {
private static final String TAG = Prefs.class.getSimpleName();
public static final String RELIABLE_SERVICE_PREF = "pref_reliable_service";
public static final String DISABLE_PASSPHRASE_PREF = "pref_disable_passphrase";
public static final String THEME_PREF = "pref_theme";
public static final String BACKGROUND_PREF = "pref_chat_background";
@@ -171,6 +174,11 @@ public class Prefs {
return true;
}
public static boolean isCallsEnabled(Context context) {
return true;
}
// ringtone
public static @NonNull Uri getNotificationRingtone(Context context) {
@@ -207,13 +215,20 @@ public class Prefs {
return result==null? null : Uri.parse(result);
}
public static void setReliableService(Context context, boolean value) {
setBooleanPreference(context, RELIABLE_SERVICE_PREF, value);
}
public static boolean reliableService(Context context) {
try {
return getBooleanPreference(context, "pref_reliable_service", false);
}
catch(Exception e) {
return false;
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (prefs.contains(RELIABLE_SERVICE_PREF)) {
try {
return prefs.getBoolean(RELIABLE_SERVICE_PREF, true);
} catch(Exception e) {}
}
// if the key was unset, then calculate default value
return !isPushEnabled(context) || !DcHelper.getAccounts(context).isAllChatmail();
}
// vibrate
@@ -1,68 +0,0 @@
package org.thoughtcrime.securesms.videochat;
import android.os.Bundle;
import android.view.Menu;
import android.webkit.PermissionRequest;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import org.thoughtcrime.securesms.WebViewActivity;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.util.Util;
public class VideochatActivity extends WebViewActivity {
private static final String TAG = VideochatActivity.class.getSimpleName();
public static final String EXTRA_CHAT_ID = "chat_id";
public static final String EXTRA_URL = "url";
private DcContext dcContext;
private String url = "";
@Override
protected void onCreate(Bundle state, boolean ready) {
super.onCreate(state, ready);
this.dcContext = DcHelper.getContext(getApplicationContext());
Bundle b = getIntent().getExtras();
url = b.getString(EXTRA_URL, "");
int chatId = b.getInt(EXTRA_CHAT_ID, 0);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setUserAgentString("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36");
webView.setWebChromeClient(new WebChromeClient() {
@Override
public void onPermissionRequest(PermissionRequest request) {
Util.runOnMain(() -> {
request.grant(request.getResources());
});
}
});
Util.runOnAnyBackgroundThread(() -> {
final DcChat chat = dcContext.getChat(chatId);
Util.runOnMain(() -> {
getSupportActionBar().setTitle(chat.getName());
});
});
webView.loadUrl(url);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// do not call super.onPrepareOptionsMenu() as the default "Search" menu is not needed
return true;
}
@Override
protected boolean openOnlineUrl(String url) {
finish();
return true;
}
}
@@ -1,77 +0,0 @@
package org.thoughtcrime.securesms.videochat;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import androidx.appcompat.app.AlertDialog;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.permissions.Permissions;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public class VideochatUtil {
public void invite(Activity activity, int chatId) {
DcContext dcContext = DcHelper.getContext(activity);
DcChat dcChat = dcContext.getChat(chatId);
new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.videochat_invite_user_to_videochat, dcChat.getName()))
.setMessage(R.string.videochat_invite_user_hint)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok, (dialog, which) -> {
String instance = dcContext.getConfig(DcHelper.CONFIG_WEBRTC_INSTANCE);
boolean unset = instance == null || instance.isEmpty();
if (unset) {
dcContext.setConfig(DcHelper.CONFIG_WEBRTC_INSTANCE, DcHelper.DEFAULT_VIDEOCHAT_URL);
}
int msgId = dcContext.sendVideochatInvitation(dcChat.getId());
if (unset) {
dcContext.setConfig(DcHelper.CONFIG_WEBRTC_INSTANCE, null);
}
if (msgId != 0) {
join(activity, msgId);
}
})
.show();
}
public void join(Activity activity, int msgId) {
Permissions.with(activity)
.request(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
.ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_camera_denied))
.onAllGranted(() -> {
DcContext dcContext = DcHelper.getContext(activity);
DcMsg dcMsg = dcContext.getMsg(msgId);
String url = dcMsg.getVideochatUrl();
if (url.startsWith(DcHelper.DEFAULT_VIDEOCHAT_URL_PREFIX) && url.contains("#")) {
String name = dcContext.getName();
try {
name = URLEncoder.encode(dcContext.getName(), StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException ignored) {}
url += "&userInfo.displayName=%22" + name +"%22";
}
Intent intent = new Intent(activity, VideochatActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(VideochatActivity.EXTRA_CHAT_ID, dcMsg.getChatId());
intent.putExtra(VideochatActivity.EXTRA_URL, url);
activity.startActivity(intent);
})
.execute();
}
}
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,9c-1.6,0 -3.15,0.25 -4.6,0.72v3.1c0,0.39 -0.23,0.74 -0.56,0.9 -0.98,0.49 -1.87,1.12 -2.66,1.85 -0.18,0.18 -0.43,0.28 -0.7,0.28 -0.28,0 -0.53,-0.11 -0.71,-0.29L0.29,13.08c-0.18,-0.17 -0.29,-0.42 -0.29,-0.7 0,-0.28 0.11,-0.53 0.29,-0.71C3.34,8.78 7.46,7 12,7s8.66,1.78 11.71,4.67c0.18,0.18 0.29,0.43 0.29,0.71 0,0.28 -0.11,0.53 -0.29,0.71l-2.48,2.48c-0.18,0.18 -0.43,0.29 -0.71,0.29 -0.27,0 -0.52,-0.11 -0.7,-0.28 -0.79,-0.74 -1.69,-1.36 -2.67,-1.85 -0.33,-0.16 -0.56,-0.5 -0.56,-0.9v-3.1C15.15,9.25 13.6,9 12,9z"/>
</vector>
@@ -161,30 +161,6 @@
</LinearLayout>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1"
android:orientation="vertical">
<org.thoughtcrime.securesms.components.CircleColorImageView
android:id="@+id/invite_video_chat_button"
android:layout_width="53dp"
android:layout_height="53dp"
android:src="@drawable/baseline_call_24"
android:scaleType="center"
android:contentDescription="@string/videochat"
app:circleColor="@color/audio_icon"/>
<TextView android:layout_marginTop="10dp"
android:id="@+id/invite_video_chat_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/AttachmentTypeLabel"
android:text="@string/videochat"/>
</LinearLayout>
<LinearLayout android:id="@+id/location_linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+50
View File
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="210dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:focusable="true">
<LinearLayout android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="16dp"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:fontFamily="sans-serif"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?conversation_item_outgoing_text_primary_color"
tools:text="Incoming call" />
<org.thoughtcrime.securesms.components.ConversationItemFooter
android:id="@+id/footer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:gravity="start"
android:clipChildren="false"
android:clipToPadding="false"
/>
</LinearLayout>
<ImageView
android:id="@+id/call_icon"
android:layout_width="30dp"
android:layout_height="30dp"
android:contentDescription="@null"
android:src="@drawable/baseline_call_24"
/>
</LinearLayout>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.CallItemView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/call_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible"/>
@@ -172,6 +172,16 @@
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginRight="@dimen/message_bubble_horizontal_padding" />
<ViewStub
android:id="@+id/call_view_stub"
android:layout="@layout/conversation_item_call"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_top_padding"
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginRight="@dimen/message_bubble_horizontal_padding" />
<org.thoughtcrime.securesms.components.emoji.AutoScaledEmojiTextView
android:id="@+id/conversation_item_body"
android:layout_width="wrap_content"
@@ -154,6 +154,16 @@
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginRight="@dimen/message_bubble_horizontal_padding" />
<ViewStub
android:id="@+id/call_view_stub"
android:layout="@layout/conversation_item_call"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_top_padding"
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginRight="@dimen/message_bubble_horizontal_padding" />
<org.thoughtcrime.securesms.components.emoji.AutoScaledEmojiTextView
android:id="@+id/conversation_item_body"
android:layout_width="wrap_content"
@@ -1,105 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.ConversationVideochatItem
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/conversation_item_background"
android:focusable="true"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="13dp"
android:paddingBottom="13dp"
android:paddingLeft="28dp"
android:paddingRight="28dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginBottom="5dp"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/video_icon_container"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:background="@drawable/circle_universal_overlay"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true">
<ImageView android:id="@+id/video_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:gravity="center"
app:srcCompat="@drawable/ic_videocam_white_24dp" />
</FrameLayout>
<FrameLayout
android:id="@+id/contact_photo_container"
android:layout_width="36dp"
android:layout_height="wrap_content"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true">
<org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/contact_photo"
android:foreground="@drawable/contact_photo_background"
android:layout_width="36dp"
android:layout_height="36dp"
android:cropToPadding="true"
android:contentDescription="@null" />
</FrameLayout>
</LinearLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/conversation_update_body"
style="@style/Delta.Text.UpdateHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="none"
android:linksClickable="false"
android:gravity="center"
android:textColor="?attr/conversation_item_update_text_color"
android:background="@drawable/conversation_item_update_background"
android:paddingLeft="10dp"
android:paddingTop="9dp"
android:paddingRight="10dp"
android:paddingBottom="10dp"
android:lineSpacingMultiplier="1.3"
tools:text="Paddy invited to a video chat" />
<org.thoughtcrime.securesms.components.ConversationItemFooter
android:id="@+id/conversation_item_footer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
android:layout_marginTop="3dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:paddingLeft="@dimen/message_bubble_horizontal_padding"
android:paddingRight="@dimen/message_bubble_horizontal_padding"
android:layout_gravity="end"
android:gravity="end"
android:clipChildren="false"
android:clipToPadding="false"
android:background="@drawable/conversation_item_update_background"
app:footer_text_color="@color/core_dark_05"/>
</LinearLayout>
</org.thoughtcrime.securesms.ConversationVideochatItem>
@@ -3,6 +3,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="@color/gray95">
<org.thoughtcrime.securesms.components.viewpager.HackyViewPager
+5
View File
@@ -25,6 +25,11 @@
android:visible="false"
app:showAsAction="always"/>
<item android:id="@+id/menu_start_call"
android:title="@string/start_call"
android:icon="@drawable/baseline_call_24"
app:showAsAction="always" />
<item android:id="@+id/menu_all_media"
android:title="@string/apps_and_media"
android:icon="@drawable/ic_apps_24"
-3
View File
@@ -205,9 +205,6 @@
<!-- permissions -->
<string name="perm_required_title">منح إحدى الأذونات مطلو</string>
<string name="perm_continue">استمر</string>
<!-- deprecated, use string "E-Mail" for non-encrypted messages instead -->
<string name="encrypted_message">رسالة مشفرة</string>
<string name="global_menu_preferences_language_desktop">اختر اللغة…</string>
<string name="global_menu_file_desktop">الملف</string>
<string name="global_menu_edit_desktop">تعديل</string>
-7
View File
@@ -396,10 +396,6 @@
<string name="qrshow_join_contact_hint">%1$silə əlaqə qurmaq üçün bunu skan edin</string>
<string name="qrshow_join_contact_no_connection_toast">İnternet bağlantısı yoxdur, QR kodunu qura bilmir.</string>
<string name="contact_verified">%1$s yoxlanıldı.</string>
<!-- deprecated, was stock string -->
<string name="contact_not_verified">%1$sDoğrula bilmir</string>
<!-- deprecated, was stock string -->
<string name="contact_setup_changed">%1$süçün quraşdırma dəyişdirildi</string>
<!-- notifications -->
<string name="notify_reply_button">Cavab ver</string>
<string name="notify_new_message">Yeni mesaj</string>
@@ -417,9 +413,6 @@
<string name="perm_explain_access_to_mic_denied">Delta Chat yer əlavə etmək üçün yer icazə tələb edir, lakin daimi olaraq rədd edilmişdir. \"İcazələr\"-i seçin və\"Yerləşmə\" izni verin. </string>
<string name="perm_explain_access_to_storage_denied">Delta Chat, fotoşəkilləri, videoları və ya səsləri əlavə etmək və ya ixrac etmək üçün saxlama icazəsini tələb edir, lakin bu daimi olaraq rədd edildi. Xahiş edirik tətbiq ayarları menyusuna davam edin, \"İzinlər\" seçin və \"Yaddaş\" funksiyasını aktivləşdirin.</string>
<string name="perm_explain_access_to_location_denied">Delta Chat yer əlavə etmək üçün yer icazə tələb edir, lakin daimi olaraq rədd edilmişdir. \"İcazələr\"-i seçin və\"Yerləşmə\" izni verin. </string>
<!-- deprecated, use string "E-Mail" for non-encrypted messages instead -->
<string name="encrypted_message">Şifrələnmiş mesaj</string>
<string name="global_menu_preferences_language_desktop">Dil səç</string>
<string name="global_menu_file_desktop">Fayl</string>
<string name="global_menu_file_quit_desktop">Çıxış</string>
-36
View File
@@ -199,19 +199,7 @@
<string name="menu_new_chat">Нов чат</string>
<string name="menu_new_group">Нова група</string>
<string name="clone_chat">Клониране на чат</string>
<!-- deprecated -->
<string name="broadcast_list">Списък за разпространение</string>
<!-- deprecated -->
<string name="broadcast_lists">Списъци за разпространение</string>
<!-- deprecated -->
<string name="new_broadcast_list">Нов списък за разпространение</string>
<string name="add_recipients">Добавяне на получатели</string>
<!-- deprecated -->
<string name="edit_broadcast_list">Редактиране на списък за разпространение</string>
<!-- deprecated -->
<string name="broadcast_list_name">Име на списък за разпространение</string>
<!-- deprecated -->
<string name="please_enter_broadcast_list_name">Моля, въведете име за списъка за разпространение</string>
<string name="menu_send">Изпращане</string>
<string name="menu_toggle_keyboard">Включване/изключване на Emoji клавиатура</string>
<string name="menu_edit_group">Редактиране на група</string>
@@ -343,8 +331,6 @@
<string name="ask_start_chat_with">Чат с %1$s?</string>
<!-- %1$s is replaced by a comma-separated list of names -->
<string name="ask_remove_members">Премахване на %1$s от групата?</string>
<!-- deprecated -->
<string name="ask_remove_from_broadcast">Да бъдат ли премахнати %1$s от списъка за разпространение?</string>
<string name="open_url_confirmation">Искате ли да отворите тази връзка?</string>
@@ -374,8 +360,6 @@
</plurals>
<!-- The placeholder will be replaced by the name of the recipient in a one-to-one chat. -->
<string name="chat_new_one_to_one_hint">Изпращане на съобщение до %1$s.</string>
<!-- deprecated -->
<string name="chat_new_broadcast_hint">Получателите в един списък за разпространение получават съобщения в чат \"само за четене\" с Вас.</string>
<string name="chat_new_group_hint">Другите ще виждат тази група едва след като изпратите първо съобщение.</string>
<string name="chat_record_slide_to_cancel">Приплъзнете, за да откажете</string>
<string name="chat_record_explain">Докоснете и задръжте, за да запишете гласово съобщение, отпуснете, за да го изпратите</string>
@@ -473,11 +457,6 @@
<!-- option to show images in the gallery as square (instead of using correct width/height) -->
<string name="square_grid">Мрежа от квадрати</string>
<string name="send_message">Изпращане на съобщение</string>
<!-- deprecated, was stock string -->
<string name="aeap_addr_changed">%1$s промени своя адрес от %2$s на %3$s</string>
<!-- deprecated -->
<string name="aeap_explanation">Променихте своя email адрес от %1$s на %2$s.\n\nАко от тук нататък изпратите съобщение до проверена група, контактите в нея автоматично ще заменят стария с новия Ви адрес.\n\nСилно се препоръчва да извършите настройка в предишния си доставчик на електронна поща така, че всички email съобщения да се препращат към новия Ви email адрес. В противен случай е възможно да не получавате съобщения от контакти, които все още не са получили новия Ви адрес.</string>
<!-- Multi Device -->
<!-- "Second Device" can also be translated as "Another Device", if that is catchier in the destination language. However, make sure to use the term consistently. -->
@@ -844,14 +823,8 @@
<string name="ephemeral_timer_weeks_by_you">Установихте таймера на изчезващите съобщения на %1$s седмици.</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 and address of the contact -->
<string name="ephemeral_timer_weeks_by_other">Таймерът на изчезващите съобщения е установен на %1$s седмици от %2$s.</string>
<!-- deprecated -->
<string name="chat_protection_broken">%1$s изпрати съобщение от друго устройство.</string>
<string name="chat_protection_enabled_tap_to_learn_more">От тук нататък е гарантирано, че съобщенията ще бъдат криптирани от край до край. Докоснете, за да научите повече.</string>
<string name="chat_protection_enabled_explanation">От тук нататък е гарантирано, че всички съобщения в този чат са криптирани от край до край.\n\nКриптирането от край до край запазва поверителността на съобщенията между вас и вашите партньори в чата. Дори Вашият e-mail доставчик не може да ги прочете.</string>
<!-- deprecated -->
<string name="chat_protection_broken_tap_to_learn_more">%1$s изпрати съобщение от друго съобщение. Докоснете, за да научите повече.</string>
<!-- deprecated -->
<string name="chat_protection_broken_explanation">Криптирането от край до край повече не може да бъде гарантирано, вероятно защото %1$s преинсталира Delta Chat или изпрати съобщение от друго устройство.\n\nБихте могли да се срещнете с него/нея лично, да сканирате неговия/нейния QR код отново и отново да установите гарантирано криптиране от край до край.</string>
<string name="invalid_unencrypted_tap_to_learn_more">⚠️%1$s изисква криптиране от край до край, което все още не е установено за този чат. Докоснете, за да научите повече.</string>
<string name="invalid_unencrypted_explanation">За да установите криптиране от край до край, бихте могли да се срещате с лицата, с които желаете да контактувате, лично и да сканирате техните QR кодове, за да ги въведете.</string>
<string name="learn_more">Научете повече</string>
@@ -910,16 +883,10 @@
<!-- placeholder will be replaced by the name of the inviter. -->
<string name="secure_join_replies">%1$s отговори, изчаква се да бъдете добавени към групата…</string>
<string name="secure_join_wait">Установява се гарантирано криптиране от край до край, моля изчакайте…</string>
<!-- deprecated -->
<string name="secure_join_wait_timeout">Все още не може да бъде установено гарантирано криптиране от край до край, но вече можете да пратите съобщение.</string>
<string name="contact_verified">%1$s е проверен.</string>
<!-- deprecated, was stock string -->
<string name="contact_not_verified">Не може да бъде проверен %1$s.</string>
<!-- Shown in contact profile. The placeholder will be replaced by the name of the contact that introduced the contact. -->
<string name="verified_by">Проверен от%1$s</string>
<string name="verified_by_you">Представен от мен</string>
<!-- deprecated, was stock string -->
<string name="contact_setup_changed">Настройката за %1$s е сменена.</string>
<string name="verified_contact_required_explain">За да се гарантира криптирането от край до край, можете да добавяте в тази група само контакти, отбелязани със зелена отметка.\n\nБихте могли да се срещнете с лицата, с които желаете да контактувате, лично и да сканирате техните QR кодове, за да ги въведете.</string>
<string name="mailto_dialog_header_select_chat">Изберете чат, към който да бъде пратено съобщението</string>
<!-- first placeholder is the name of the chat -->
@@ -962,9 +929,6 @@
<string name="ImageEditorHud_flip">Обръщане</string>
<string name="ImageEditorHud_rotate">Завъртане</string>
<!-- deprecated, use string "E-Mail" for non-encrypted messages instead -->
<string name="encrypted_message">Криптирано съобщение</string>
<!-- strings introduced on desktop. we want to share strings between the os, in general, please do not add generic strings here -->
<string name="about_offical_app_desktop">Това е официалното настолно приложение Delta Chat.</string>
<string name="about_licensed_under_desktop">Този софтуер е лицензиран под GNU GPL версия 3. Изходният код е наличен в GitHub.</string>
+62 -44
View File
@@ -24,7 +24,7 @@
<string name="open">گۊشیڌن</string>
<string name="download">دانلود</string>
<string name="downloading">هونی دانلود ابۊ...</string>
<string name="open_attachment">گۊشیڌن زمینه</string>
<string name="open_attachment">گۊشیڌن پیوست</string>
<string name="join">ٱووڌن</string>
<string name="rejoin">ز نۊ ٱووڌن</string>
<string name="delete">پاک کردن</string>
@@ -33,7 +33,7 @@
<string name="info">زبار</string>
<string name="update">ورۊ کردن</string>
<string name="emoji">ایموجی</string>
<string name="attachment">زمینه</string>
<string name="attachment">پیوست</string>
<!-- the placeholder will be replaced by the name of the image or file to attach -->
<string name="ask_attach">«%1$s» پیوست بۊ؟</string>
<string name="back">وورگشتن</string>
@@ -54,17 +54,17 @@
<string name="mute">بؽ دونگ</string>
<string name="muted">بؽ دونگ</string>
<string name="ephemeral_messages">پیوما بؽڌارکۊنی</string>
<string name="ephemeral_messages_hint">یو ری پوی منتورا ای گوفت ۉ لوفت، ٱر دلتا چت ن و کار بگرن ائمال ابۊ. ولی هنی ترن پیوما ن لف گیری، زفت وو فوروارد کونن.</string>
<string name="ephemeral_messages_hint">یو ری پوی منتورا ای گوفت وو لوفت، ٱر دلتا چت ن و کار بگرن ائمال ابۊ. ولی هنی ترن پیوما ن لف گیری، زفت وو فوروارد کونن.</string>
<string name="save">زفت کردن</string>
<string name="chat">گوفت ۉ لوفت</string>
<string name="chat">گوفت وو لوفت</string>
<string name="media">وارسگر</string>
<string name="apps_and_media">برنومه یل وو وارسگرا</string>
<string name="profile">پوروفایل</string>
<string name="all_profiles">پوی پوروفایلا</string>
<string name="current_profile">پوروفایل هیم سکویی</string>
<string name="main_menu">نومگه ٱسلی</string>
<string name="start_chat">ناڌن پا گوفت ۉ لوفت</string>
<string name="show_full_message">نشۉ داڌن پیوم کامل...</string>
<string name="start_chat">ناڌن پا گوفت وو لوفت</string>
<string name="show_full_message">نشووݩ داڌن پیوم کامل...</string>
<!-- Stay short here, say ~16 characters. The source string could also be "All Read", maybe that hint can make translations easier :) -->
<string name="mark_all_as_read">پوی ن خونده بیڌه بزݩ</string>
<string name="mark_as_read">نشووه ناڌن و عونوان خونده وابیڌه</string>
@@ -78,12 +78,12 @@
<string name="always">همیشه</string>
<string name="always_load_remote_images">شؽواتا دیر ز دسرس هی بوگوئشن</string>
<string name="once">هیم ی کرت</string>
<string name="show_warning">نشۉ داڌن هوشدار</string>
<string name="show_password">نشۉ داڌن رزم</string>
<string name="show_warning">نشووݩ داڌن هوشدار</string>
<string name="show_password">نشووݩ داڌن رزم</string>
<string name="hide_password">بؽڌار کردن رزم</string>
<string name="not_now">سکو ن</string>
<string name="never">هیچ</string>
<string name="one_moment">ی دمۉ...</string>
<string name="one_moment">ی دمووݩ...</string>
<string name="done">ٱنجوم وابی</string>
<string name="undo">وورگندن</string>
<string name="offline">آفلاین</string>
@@ -97,11 +97,11 @@
<!-- the placeholder will be replaced by the name of a file -->
<string name="cannot_save_file">نتره %1$s ن زفت کونه.</string>
<string name="copied_to_clipboard">من ویرگه لف گیری وابی.</string>
<string name="contacts_headline">هومدنگۉݩ</string>
<string name="contacts_headline">هومدنگووݩ</string>
<string name="email_address">نشۊوی ایمیل</string>
<string name="bad_email_address">نشۊوی ایمیل موشکل داره.</string>
<string name="password">رزم</string>
<string name="existing_password">رزم مۉجۊڌ</string>
<string name="existing_password">رزم مووجۊد</string>
<string name="now">سکو</string>
<!-- Headline for destructive actions with no undo. Could also be "Caution" or "Warning". -->
<string name="danger">بپا</string>
@@ -188,6 +188,7 @@
<string name="files_attach_hint">فشناڌن فایلا ٱسلی وو شؽواتا تپنیڌه نوابیڌه</string>
<!-- "Files" here means the "Files Selector App" or "Files Manager App" -->
<string name="choose_from_files">پسند ز من فایلا</string>
<string name="choose_from_gallery">پسند ز شؽوات مال</string>
<!-- "App" is used to present "Webxdc App" (https://webxdc.org) in a user friendly way. Please stay close to the original term and keep it short (it is used in menus with few screen space). -->
<string name="webxdc_app">برنومه</string>
<!-- plural of "App"; used to present "Webxdc App" (https://webxdc.org) in a user friendly way. Please stay close to the original term and keep it short (it is used in menus with few screen space). -->
@@ -200,7 +201,7 @@
<string name="tools">ٱوزار</string>
<string name="app_size">هندا</string>
<string name="app_date_published">تیجنیڌن</string>
<string name="add_to_chat">ٱووردن و گوفت ۉ لوفت</string>
<string name="add_to_chat">ٱووردن و گوفت وو لوفت</string>
<!-- short for "Browse through the App Picker/Store/Catalogue"; could also be translated as "Discover" or "Search" -->
<string name="browse">پیتینیڌن</string>
<string name="unknown">ناشناس</string>
@@ -227,43 +228,56 @@
<!-- menu labels (or icon, buttons...) -->
<string name="menu_new_contact">هومدنگ نۊ</string>
<string name="menu_new_classic_contact">ٱووردن هومدنگ و هالت دستی</string>
<string name="menu_new_chat">گوفت ۉ لوفت نۊ</string>
<string name="menu_new_chat">گوفت وو لوفت نۊ</string>
<string name="menu_new_group">جرگه نۊ</string>
<!-- "Chat" is a verb here, "Message to" would also fit. the string might be used in the "New Chat" screen above the contact list -->
<string name="chat_with">ناڌن پا گوفت ۉ لوفت وا...</string>
<string name="clone_chat">وورکل گوفت ۉ لوفت نۊ ز ای گوفت ۉ لوفت</string>
<!-- deprecated -->
<string name="broadcast_list">نومگه Broadcast</string>
<!-- deprecated -->
<string name="broadcast_lists">نومگه یل Broadcast</string>
<!-- deprecated -->
<string name="new_broadcast_list">نومگه Broadcast نۊ</string>
<!-- deprecated -->
<string name="edit_broadcast_list">آلشت نومگه Broadcast</string>
<string name="chat_with">ناڌن پا گوفت وو لوفت وا...</string>
<string name="clone_chat">وورکل گوفت وو لوفت نۊ ز ای گوفت وو لوفت</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="channel">تورگه</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="channels">تورگه یل</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="new_channel">تورگه نۊ</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="channel_name">نوم تورگه</string>
<string name="email">ایمیل</string>
<!-- "New" as in "Create New E-Mail"; shown together with "New Group" and "New Channel" -->
<string name="new_email">ایمیل نۊ</string>
<!-- the "Subject" of an e-mail, use the term common in classic e-mail apps -->
<string name="subject">سرتال</string>
<string name="menu_send">فشناڌن</string>
<string name="menu_toggle_keyboard">آلشت تخته کیلیت ایموجی یل</string>
<string name="menu_edit_group">آلشت جرگه</string>
<string name="menu_group_name_and_image">نوم وو شؽوات جرگه</string>
<string name="menu_archive_chat">بایگۊوی گوفت ۉ لوفت</string>
<string name="menu_unarchive_chat">و در زیڌن گوفت ۉ لوفت ز بایگۊوی</string>
<string name="menu_add_attachment">ازاف کردن زمینه</string>
<string name="menu_show_global_map">نشووݩ داڌن پوی جاگه یل</string>
<string name="menu_archive_chat">بایگۊوی گوفت وو لوفت</string>
<string name="menu_unarchive_chat">و در زیڌن گوفت وو لوفت ز بایگۊوی</string>
<string name="menu_add_attachment">ازاف کردن پیوست</string>
<string name="menu_leave_group">رئڌن ز جرگه</string>
<string name="menu_delete_chat">پاک کردن گوفت ۉ لوفت</string>
<string name="menu_leave_channel">رئڌن ز تورگه</string>
<string name="menu_delete_chat">پاک کردن گوفت وو لوفت</string>
<!-- Command to delete all messages in a chat. The chat itself will not be deleted but will be empty afterwards, so make sure to be different from "Delete Chat" here. "Clear" is a verb here, "Empty Chat" would also be fine (eg. in German "Chat leeren") -->
<string name="clear_chat">روفتن ویرگار گوفت ۉ لوفت</string>
<string name="clear_chat">روفتن ویرگار گوفت وو لوفت</string>
<string name="delete_contact">پاک کردن هومدنگ</string>
<string name="menu_delete_location">ای جاگه پاک بۊ؟</string>
<string name="menu_message_details">جۊزیات پیوم</string>
<string name="menu_copy_to_clipboard">لف گیری من ویرگه</string>
<string name="share_invite_link">یک رسۊوی لینگ موکش</string>
<string name="invite_friends">موکش رفؽقووݩ</string>
<string name="menu_copy_selection_to_clipboard">لف گیری ز پسند بیڌه</string>
<string name="menu_copy_link_to_clipboard">لف گیری لینک</string>
<string name="menu_copy_text_to_clipboard">لف گیری هؽل</string>
<string name="menu_copy_image_to_clipboard">لف گیری شؽوات</string>
<string name="menu_copy_email_to_clipboard">لف گیری ایمیل</string>
<string name="paste_from_clipboard">جا وندن ز ویرگه</string>
<string name="menu_forward">فوروارد پیوم</string>
<string name="menu_reply">ولوم ب پیوم</string>
<string name="menu_mute">بؽ دونگ کردن وارسۊویا</string>
<string name="menu_unmute">وا دونگ کردن</string>
<string name="menu_all_media">پوی وارسگرا</string>
<!-- Command to jump to the original message corresponding to a gallery image or document -->
<string name="show_in_chat">نشۉ داڌن من گوفت ۉ لوفت</string>
<string name="show_in_chat">نشووݩ داڌن من گوفت وو لوفت</string>
<string name="menu_share">یک رسۊوی</string>
<string name="accept">قوۊل</string>
<string name="menu_play">پشک</string>
@@ -272,7 +286,7 @@
<string name="menu_scroll_to_top">رئڌن ب روء</string>
<string name="menu_help">هیاری</string>
<string name="menu_select_all">پسند پوی</string>
<string name="select_chat">پسند گوفت ۉ لوفت</string>
<string name="select_chat">پسند گوفت وو لوفت</string>
<string name="select_more">پسند بیشتر</string>
<string name="menu_edit_name">آلشت نوم</string>
<string name="menu_settings">سامووا</string>
@@ -287,7 +301,7 @@
<string name="device_talk_subtitle">پیوما منی</string>
<string name="edit_contact">آلشت هومدنگ</string>
<!-- Verb "to pin", making something sticky, not a noun or abbreviation for "pin number". -->
<string name="pin_chat">دیسنیڌن گوفت ۉ لوفت</string>
<string name="pin_chat">دیسنیڌن گوفت وو لوفت</string>
<!-- Verb "to pin", making something sticky, not a noun or abbreviation for "pin number". -->
<string name="pin">دیسنیڌن</string>
<string name="source_code">کود بونچک</string>
@@ -305,23 +319,21 @@
<string name="file_saved_to">فایل من «%1$s» زفت وابی.</string>
<string name="videochat">گوفت ۉ لوفت ویدیویی</string>
<string name="videochat_invitation">موکش و گوفت ۉ لوفت ویدیویی</string>
<string name="videochat">گوفت وو لوفت ویدیویی</string>
<string name="videochat_invitation">موکش و گوفت وو لوفت ویدیویی</string>
<!-- Used for the deletion of Device messages -->
<plurals name="ask_delete_messages_simple">
<item quantity="one">êxuy %d payom pāk bu?</item>
<item quantity="other">اخۊی %d پیوم پاک بۊ؟</item>
</plurals>
<string name="ask_start_chat_with">گوفت ۉ لوفت وا %1$s؟</string>
<string name="ask_start_chat_with">گوفت وو لوفت وا %1$s؟</string>
<!-- %1$s is replaced by a comma-separated list of names -->
<string name="ask_remove_members">پاک کردن %1$s ز جرگه؟</string>
<!-- deprecated -->
<string name="ask_remove_from_broadcast">%1$s ز نومگه Broadcast پاک بۊ؟</string>
<string name="open_url_confirmation">اخۊی ای لینگن بۊگۊشی؟</string>
<!-- contact list -->
<string name="contacts_title">هومدنگۉݩ</string>
<string name="contacts_title">هومدنگووݩ</string>
<string name="contacts_empty_hint">بؽ هومدنگ.</string>
@@ -347,7 +359,7 @@
<string name="send_file_to">فشناڌن \"%1$s\" و...</string>
<!-- title shown above a list contacts where one should be selected (eg. when a webxdc attempts to send a message to a chat) -->
<string name="send_message_to">فشناڌن پیوم و...</string>
<string name="show_location_traces">نشۉ داڌن جا پا</string>
<string name="show_location_traces">نشووݩ داڌن جا پا</string>
<!-- search -->
<string name="search">پیتینیڌن</string>
<!-- create/edit groups, contact/group profile -->
@@ -363,8 +375,9 @@
<string name="tab_docs">سندا</string>
<string name="tab_links">لینگا</string>
<string name="tab_map">نقشه</string>
<string name="media_preview">نشۉ داڌن وارسگر</string>
<string name="media_preview">نشووݩ داڌن وارسگر</string>
<string name="send_message">فشناڌن پیوم</string>
<!-- Headline in the "Connectivity" view. Placeholder will be replaced by a domain -->
<string name="storage_on_domain">زفت کردن من %1$s</string>
<!-- Shown in the title bar if the app is "Connecting"; prefer short strings. The ellipsis is a single character (…), not three (...) -->
@@ -378,9 +391,13 @@
<string name="messages">پیوما</string>
<string name="sync_all">ورۊ کردن پوی</string>
<string name="pref_your_name">نوم ایسا</string>
<string name="pref_notifications_show">نشۉ داڌن</string>
<string name="pref_language">زۉݩ</string>
<string name="pref_chats">گوفت ۉ لوفتا</string>
<string name="pref_notifications_show">نشووݩ داڌن</string>
<string name="pref_dark_theme">تاریک</string>
<string name="pref_appearance">شؽوات</string>
<string name="pref_theme">تم</string>
<string name="pref_language">زووݩ</string>
<string name="pref_use_system_ui_font">فونت سیستوم ن و کار بگر</string>
<string name="pref_chats">گوفت وو لوفتا</string>
<string name="pref_log_header">گوزارش</string>
<string name="pref_background_btn_gallery">پسند ز شؽوات مال</string>
<string name="pref_show_emails_all">پوی</string>
@@ -425,13 +442,14 @@
<string name="notify_name_and_message">نوم وو پیوم</string>
<string name="notify_name_only">تینا نوم</string>
<string name="notify_no_name_or_message">بؽ نوم یا پیوم</string>
<string name="global_menu_preferences_language_desktop">زۉݩ</string>
<string name="global_menu_preferences_language_desktop">زووݩ</string>
<string name="global_menu_file_desktop">فایل</string>
<string name="global_menu_file_quit_desktop">و در زیڌن</string>
<string name="global_menu_edit_desktop">آلشت</string>
<string name="global_menu_edit_cut_desktop">جا گورو</string>
<string name="global_menu_edit_copy_desktop">لف گیری</string>
<string name="global_menu_edit_paste_desktop">جا وندن</string>
<string name="global_menu_help_about_desktop">زبار دلتا چت</string>
<string name="global_menu_file_open_desktop">گۊشیڌن دلتا چت</string>
<string name="delete_message_desktop">پاک کردن پیوم</string>
<string name="remove_desktop">پاک کردن</string>
@@ -440,7 +458,7 @@
<string name="select_group_image_desktop">پسند شؽوات جرگه</string>
<string name="message_detail_sent_desktop">فشناڌ</string>
<string name="message_detail_received_desktop">گرؽڌه وابی</string>
<string name="show_window">نشۉ داڌن نیمدری</string>
<string name="show_window">نشووݩ داڌن نیمدری</string>
<string name="open_settings">گۊشیڌن سامووا</string>
</resources>
+61 -55
View File
@@ -28,12 +28,14 @@
<string name="join">Uneix-te</string>
<string name="rejoin">Torna a unir-te</string>
<string name="delete">Esborra</string>
<string name="delete_for_me">Esborra per a Mi</string>
<string name="delete_for_everyone">Esborra per a Tothom</string>
<string name="delete_for_me">Elimina per a mi</string>
<string name="delete_for_everyone">Esborra per a tothom</string>
<string name="info">Info</string>
<string name="update">Actualitza</string>
<string name="emoji">Emojis</string>
<string name="attachment">Adjunt</string>
<!-- the placeholder will be replaced by the name of the image or file to attach -->
<string name="ask_attach">Voleu adjuntar \"%1$s\"?</string>
<string name="back">Enrere</string>
<string name="close">Tanca</string>
<string name="close_window">Tanca la finestra</string>
@@ -52,11 +54,14 @@
<string name="mute">Silencia</string>
<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 usin Delta Chat. Tot i això, poden copiar, desar o reenviar missatges o utilitzar altres clients de correu.</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>
<string name="save">Desa</string>
<string name="chat">Xat</string>
<string name="media">Multimèdia</string>
<string name="apps_and_media">Apps &amp; Multimèdia</string>
<string name="profile">Perfil</string>
<string name="all_profiles">Tots els Perfils</string>
<string name="current_profile">Perfil Actual</string>
<string name="main_menu">Menú principal</string>
<string name="start_chat">Inicia un xat</string>
<string name="show_full_message">Mostra el missatge sencer...</string>
@@ -91,6 +96,8 @@
<string name="no_app_to_handle_data">No sé trobar cap aplicació per a gestionar aquest tipus de dades.</string>
<string name="no_browser_installed">No hi ha instal·lat cap navegador.</string>
<string name="file_not_found">No s\'ha trobat %1$s.</string>
<!-- the placeholder will be replaced by the name of a file -->
<string name="cannot_save_file">No es pot desar %1$s.</string>
<string name="copied_to_clipboard">Copiat al porta-retalls.</string>
<string name="contacts_headline">Contactes</string>
<string name="email_address">Adreça de correu electrònic</string>
@@ -150,6 +157,7 @@
<item quantity="one">%d triat</item>
<item quantity="other">%d seleccionats</item>
</plurals>
<string name="selected_colon">Seleccionat:</string>
<string name="self">Jo</string>
<string name="draft">Esborrany</string>
<string name="image">Imatge</string>
@@ -183,12 +191,17 @@
<string name="images_and_videos">Imatges i vídeos</string>
<string name="file">Fitxer</string>
<string name="files">Fitxers</string>
<string name="files_attach_hint">Envia els fitxers originals i les imatges sense comprimir</string>
<!-- "Files" here means the "Files Selector App" or "Files Manager App" -->
<string name="choose_from_files">Trieu dentre els Fitxers</string>
<string name="choose_from_gallery">Trieu de la Galeria</string>
<!-- "App" is used to present "Webxdc App" (https://webxdc.org) in a user friendly way. Please stay close to the original term and keep it short (it is used in menus with few screen space). -->
<string name="webxdc_app">App</string>
<!-- plural of "App"; used to present "Webxdc App" (https://webxdc.org) in a user friendly way. Please stay close to the original term and keep it short (it is used in menus with few screen space). -->
<string name="webxdc_apps">Apps</string>
<string name="webxdc_store_url">URL del selector d\'aplicacions</string>
<string name="webxdc_store_url_explain">Si està definit, s\'usarà l\'URL com a selector d\'aplicacions en comptes del predeterminat</string>
<string name="webxdc_draft_hint">Prem “Envia” per compartir </string>
<string name="home">Inici</string>
<string name="games">Jocs</string>
<string name="tools">Eines</string>
@@ -220,26 +233,27 @@
<!-- menu labels (or icon, buttons...) -->
<string name="menu_new_contact">Contacte nou</string>
<string name="menu_new_classic_contact">Afegeix un contacte de forma manual</string>
<string name="menu_new_classic_contact">Afegeix un contacte manualment</string>
<string name="new_classic_contact_explain">Els contactes afegits manualment es poden usar per a enviar correus electrònics clàssics i no es garanteix que se xifrin d\'extrem a extrem.</string>
<string name="menu_new_chat">Xat nou</string>
<string name="menu_new_group">Grup nou</string>
<!-- "Chat" is a verb here, "Message to" would also fit. the string might be used in the "New Chat" screen above the contact list -->
<string name="chat_with">Xateja amb…</string>
<string name="clone_chat">Clona el xat</string>
<!-- deprecated -->
<string name="broadcast_list">Llista de difusió</string>
<!-- deprecated -->
<string name="broadcast_lists">Llistes de difusió</string>
<!-- deprecated -->
<string name="new_broadcast_list">Llista de difusió nova</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="channel">Canal</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="channels">Canals</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="new_channel">Nou Canal</string>
<string name="add_recipients">Afegeix destinataris</string>
<!-- deprecated -->
<string name="edit_broadcast_list">Edita la llista de difusió</string>
<!-- deprecated -->
<string name="broadcast_list_name">Nom de la llista de difusió</string>
<!-- deprecated -->
<string name="please_enter_broadcast_list_name">Poseu el nom de la llista de difusió.</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="channel_name">Nom del Canal</string>
<string name="email">Correu electrònic</string>
<!-- "New" as in "Create New E-Mail"; shown together with "New Group" and "New Channel" -->
<string name="new_email">Nou correu electrònic</string>
<!-- the "Subject" of an e-mail, use the term common in classic e-mail apps -->
<string name="subject">Assumpte</string>
<string name="menu_send">Envia</string>
<string name="menu_toggle_keyboard">Canvia al teclat d\'emojis</string>
<string name="menu_edit_group">Edita el grup</string>
@@ -249,6 +263,7 @@
<string name="menu_unarchive_chat">Desarxiva el xat</string>
<string name="menu_add_attachment">Afegeix un adjunt</string>
<string name="menu_leave_group">Deixa el grup</string>
<string name="menu_leave_channel">Sortir del Canal</string>
<string name="menu_delete_chat">Esborra el xat</string>
<!-- Command to delete all messages in a chat. The chat itself will not be deleted but will be empty afterwards, so make sure to be different from "Delete Chat" here. "Clear" is a verb here, "Empty Chat" would also be fine (eg. in German "Chat leeren") -->
<string name="clear_chat">Buida el xat</string>
@@ -260,7 +275,7 @@
<string name="share_invite_link_explain">Qualsevol amb aquest enllaç pot veure el vostre perfil i començar un xat amb vós. Compartiu-lo només amb qui tingueu confiança.</string>
<string name="invite_friends">Convida amics</string>
<!-- %1$s is replaced by the user's invitation link ("https://i.delta.chat/...") -->
<string name="invite_friends_text">Contacta amb mi a Delta Chat:\n%1$s</string>
<string name="invite_friends_text">Contacta\'m a Delta Chat:\n%1$s</string>
<string name="menu_copy_selection_to_clipboard">Copia la selecció</string>
<string name="menu_copy_link_to_clipboard">Copia l\'enllaç</string>
<string name="menu_copy_text_to_clipboard">Copia el text</string>
@@ -275,6 +290,7 @@
<string name="menu_export_attachment">Exporta l\'adjunt</string>
<string name="menu_export_attachments">Exporta els adjunts</string>
<string name="menu_all_media">Tots els multimèdia</string>
<string name="all_apps_and_media">Totes les Apps &amp; Multimèdia</string>
<!-- Command to jump to the original message corresponding to a gallery image or document -->
<string name="show_in_chat">Mostra al xat</string>
<string name="show_app_in_chat">Mostra l\'aplicació al xat</string>
@@ -332,6 +348,8 @@
<!-- Menu item beside an app/chat that adds an icon to the system's home screen. If the user taps that icon, the app/chat is opened directly. -->
<string name="add_to_home_screen">Afegeix a la pantalla d\'inici</string>
<string name="donate">Feu un donatiu</string>
<string name="donate_device_msg">❤️ Sembla que estàs gaudint de Delta Chat!\n\nConsidera fer una donació per ajudar a garantir que Delta Chat segueixi sent gratuït per a tothom.\n\nTot i que Delta Chat és gratuït i de codi obert, el desenvolupament té un cost.\n\nAjudans a mantenir Delta Chat independent i a fer-lo encara millor en el futur.\n\nhttps://delta.chat/donate</string>
<string name="mute_for_one_hour">Silencia 1 hora</string>
<string name="mute_for_eight_hours">Silencia 8 hores</string>
<string name="mute_for_one_day">Silencia 1 dia</string>
@@ -366,6 +384,9 @@
<string name="videochat_invitation">Invitació a un xat de vídeo</string>
<string name="videochat_invitation_body">Us han convidat a un xat de vídeo, feu clic a %1$s per unir-vos-hi.</string>
<!-- get confirmations -->
<!-- confirmation for leaving groups, channels, broadcasts or a mailinglists. If a subject is needed, "Are you sure you want to leave the chat?" would work as well -->
<string name="ask_leave_group">Segur que vols sortir?</string>
<plurals name="ask_delete_chat">
<item quantity="one">¿Esborrar %d xat de tots els teus dispositius?</item>
<item quantity="other">¿Esborrar %d xats de tots els teus dispositius?</item>
@@ -391,8 +412,8 @@
<string name="ask_start_chat_with">Voleu xatejar amb %1$s?</string>
<!-- %1$s is replaced by a comma-separated list of names -->
<string name="ask_remove_members">Voleu eliminar %1$s del grup?</string>
<!-- deprecated -->
<string name="ask_remove_from_broadcast">Voleu eliminar %1$s de la llista de difusió?</string>
<!-- %1$s is replaced by a comma-separated list of names -->
<string name="ask_remove_from_channel">Eliminar %1$s del canal?</string>
<string name="open_url_confirmation">Voleu obrir aquest enllaç?</string>
@@ -422,8 +443,8 @@
</plurals>
<!-- The placeholder will be replaced by the name of the recipient in a one-to-one chat. -->
<string name="chat_new_one_to_one_hint">Envia un missatge a %1$s.</string>
<!-- deprecated -->
<string name="chat_new_broadcast_hint">En una llista de difusió, els destinataris rebran missatges en mode de només lectura en un xat amb tu.</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="chat_new_channel_hint">Els canals són una eina dun a molts per difondre els teus missatges.</string>
<string name="chat_new_group_hint">Els altres membres només veuran aquest grup després que hi envieu el primer missatge.</string>
<string name="chat_record_slide_to_cancel">Llisca per cancel·lar</string>
<string name="chat_record_explain">Mantingues premut per gravar un missatge de veu i deixa anar per enviar-lo</string>
@@ -433,6 +454,7 @@
<string name="chat_input_placeholder">Missatge</string>
<string name="chat_archived_label">Arxivat</string>
<string name="chat_request_label">Sol·licitud</string>
<string name="chat_request_label_explain">Inclou les sol·licituds als comptadors i notificacions</string>
<string name="chat_no_messages">Cap missatge.</string>
<string name="chat_self_talk_subtitle">Missatges enviats a mi mateix/a</string>
<string name="archive_empty_hint">Si arxiveu xats, els voreu aquí.</string>
@@ -531,17 +553,12 @@
<!-- option to show images in the gallery as square (instead of using correct width/height) -->
<string name="square_grid">Graella quadrada</string>
<string name="send_message">Envia el missatge</string>
<!-- deprecated, was stock string -->
<string name="aeap_addr_changed">%1$s ha canviat la seva adreça de %2$s a %3$s</string>
<!-- deprecated -->
<string name="aeap_explanation">Heu canviat la vostra adreça de correu electrònic de %1$s a %2$s.\n\nSi ara envieu un missatge a un grup verificat, els contactes del grup canviaran de forma automàtica l\'adreça antiga per la nova.\n\nUs recomanem que configureu el compte antic a fi que us reenvïi els missatges a l\'adreça nova. Si no ho feu, podríeu perdre missatges de contactes que encara no hagin actualitzat la vostra adreça.</string>
<!-- Multi Device -->
<!-- "Second Device" can also be translated as "Another Device", if that is catchier in the destination language. However, make sure to use the term consistently. -->
<string name="multidevice_title">Afegeix un segon dispositiu</string>
<string name="multidevice_same_network_hint">Assegureu-vos que els dos dispositius estan a la mateixa Wi-Fi o xarxa</string>
<string name="multidevice_this_creates_a_qr_code">Això crea codi QR que es pot escanejar en el segon dispositiu per a copiar el perfil.</string>
<string name="multidevice_this_creates_a_qr_code">Això crea un codi QR que es pot escanejar en un segon dispositiu per a copiar el perfil.</string>
<string name="multidevice_install_dc_on_other_device">Instal·leu Delta Chat en el vostre altre dispositiu (https://get.delta.chat)</string>
<!-- "I Already Have a Profile / Add as Second Device” should be the same text as defined by the keys onboarding_alternative_logins and multidevice_receiver_title -->
<string name="multidevice_tap_scan_on_other_device">Inicieu Delta Chat, toqueu «Ja tinc un perfil / Afegeix un segon dispositiu» i escanegeu el codi que es mostra aquí.</string>
@@ -646,7 +663,7 @@
<string name="proxy_share_link">Comparteix l\'enllaç</string>
<string name="login_info_oauth2_title">Voleu continuar amb la configuració simplificada?</string>
<string name="login_info_oauth2_text">Ladreça electrònica que heu introduït admet una configuració simplificada. (OAuth2).\n\nEn el pas següent, permeteu que Delta Chat actuï com a xat amb l\'aplicació de correu electrònic..\n\nNo hi ha servidors de Delta Chat, les vostres dades romanen al vostre dispositiu!</string>
<string name="login_info_oauth2_text">Ladreça electrònica que heu introduït admet una configuració simplificada. (OAuth2.0).\n\nEn el pas següent, permeteu que Delta Chat actuï com a xat amb l\'aplicació de correu electrònic..\n\nNo hi ha servidors de Delta Chat, les vostres dades romanen al vostre dispositiu!</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>
@@ -654,7 +671,7 @@
<string name="login_error_required_fields">Si us plau poseu una adreça i contrasenya vàlids</string>
<string name="import_backup_title">Importa una còpia de seguretat</string>
<string name="import_backup_ask">S\'ha trobat una còpia de seguretat a \"%1$s\".\n\nVoleu importar-la i fer-ne servir les dades i els paràmetres de configuració?</string>
<string name="import_backup_no_backup_found">No s\'han trobat còpies de seguretat.\n\nCopieu la còpia de seguretat a \"%1$s\" i torneu-ho a provar. Com a alternativa, premeu \"Comença a escriure\" per encetar el procés de configuració habitual.</string>
<string name="import_backup_no_backup_found">No s\'han trobat còpies de seguretat.\n\nCol·loca la còpia de seguretat a \"%1$s\" i torneu-ho a provar. Com a alternativa, premeu \"Comença a escriure\" per continuar amb el procés de configuració habitual.</string>
<!-- %1$s will be replaced by the failing address -->
<string name="login_error_cannot_login">No es pot iniciar la sessió com a \"%1$s\". Comproveu que l\'adreça electrònica i la contrasenya són correctes.</string>
<!-- TLS certificate checks -->
@@ -696,7 +713,7 @@
<string name="pref_blocked_contacts">Contactes bloquejats</string>
<string name="blocked_empty_hint">Els contactes blocats apareixeran aquí.</string>
<string name="pref_password_and_account_settings">Contrasenya i compte</string>
<string name="pref_who_can_see_profile_explain">La vostra imatge de perfil, el vostre nom i també la biografia s\'enviaran amb els missatges que envieu als altres usuaris.</string>
<string name="pref_who_can_see_profile_explain">La vostra imatge de perfil, nom i biografia s\'enviaran amb juntament els missatges que escriviu quant us comuniqueu amb altres usuaris.</string>
<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>
@@ -716,7 +733,7 @@
<string name="pref_notifications_show">Mostra</string>
<string name="pref_notifications_priority">Prioritat</string>
<string name="pref_notifications_explain">Habilita les notificacions del sistema per als missatges nous</string>
<string name="pref_show_notification_content">Mostra el contingut del missatge en la notificació</string>
<string name="pref_show_notification_content">Mostra el contingut del missatge a la notificació</string>
<string name="pref_show_notification_content_explain">Mostra el remitent i les primeres paraules del missatge en les notificacions</string>
<string name="pref_led_color">Color LED</string>
<string name="pref_sound">So</string>
@@ -802,6 +819,7 @@
<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>
<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>
<string name="profile_image_delete">Esborra la imatge del perfil</string>
@@ -841,7 +859,7 @@
<!-- %1$d will be replaced by the number of messages, you can assume plural/lots here. %2$s will be replaced by a timespan option. -->
<string name="autodel_device_ask">Voleu esborrar ara %1$d missatges i tots els missatges \"%2$s\" futurs?\n\n• Això inclou tot el contingut multimèdia\n\n• S\'esborraran els missatges fins i tot si no s\'han llegit\n\n• Els \"Missatges desats\" no s\'esborraran de l\'emmagatzematge local</string>
<!-- %1$d will be replaced by the number of messages, you can assume plural/lots here. %2$s will be replaced by a timespan option. -->
<string name="autodel_server_ask">Voleu esborrar ara %1$d missatges i tots els missatges nous \"%2$s\" futurs?\n\n⚠️ Això inclou els correus, contingut multimèdia i \"Missatges desats\" a totes les carpetes del servidor\n\n⚠️ No utilitzeu aquesta funció si voleu conservar les dades al servidor\n\n⚠️ No utilitzeu aquesta funció si feu servir un client de correu diferent del Delta Chat</string>
<string name="autodel_server_ask">Voleu esborrar ara %1$d missatges i tots els missatges nous \"%2$s\" futurs?\n\n⚠️ Això inclou els correus, contingut multimèdia i \"Missatges desats\" a totes les carpetes del servidor\n\n⚠️ No utilitzeu aquesta funció si voleu conservar les dades al servidor\n\n⚠️ No utilitzeu aquesta funció si feu servir un client de correu diferent a Delta Chat</string>
<!-- shown below enabled autodel_server-option, should be a summary of autodel_server_ask and remind about the impact -->
<string name="autodel_server_enabled_hint">Això inclou els correus electrònics, contingut multimèdia i «Missatges desats» en totes les carpetes del servidor. No useu aquesta funció si voleu conservar dades al servidor o si useu altres clients de correu més enllà de Delta Chat.</string>
<string name="autodel_server_warn_multi_device_title">Activa l\'esborrament immediat</string>
@@ -860,9 +878,9 @@
<string name="autodel_after_1_year">Després d\'1 any</string>
<!-- autocrypt -->
<string name="autocrypt_send_asm_title">Envia el missatge de configuració Autocrypt</string>
<string name="autocrypt_send_asm_title">Envia el missatge de configuració d\'Autocrypt</string>
<string name="autocrypt_send_asm_explain_before">Un \"missatge de configuració Autocrypt\" comparteix de forma segura la vostra configuració d\'extrem a extrem amb altres aplicacions compatibles amb Autocrypt.\n\nLa configuració es xifrarà amb un codi de configuració que es mostrarà aquí i que haureu d\'escriure a laltre dispositiu.</string>
<string name="autocrypt_send_asm_button">Envia el missatge de configuració Autocrypt</string>
<string name="autocrypt_send_asm_button">Envia el missatge de configuració d\'Autocrypt</string>
<string name="autocrypt_send_asm_explain_after">Se us ha enviat la vostra configuració. Canvieu a l\'altre dispositiu i obriu el missatge de configuració. Se us demanarà un codi de configuració. Introduïu els dígits següents a l\'indicador.\n\nUn cop fet, l\'altre dispositiu estarà llest per utilitzar Autocrypt.</string>
<string name="autocrypt_prefer_e2ee">Prefereix l\'encriptació d\'extrem a extrem</string>
@@ -888,6 +906,8 @@
<string name="remove_member_by_you">Heu eliminat el membre %1$s.</string>
<!-- %1$s will be replaced by name and address of the contact removed from the group, %2$s will be replaced by name and address of the contact who did the action -->
<string name="remove_member_by_other">Membre %1$s eliminat per %2$s.</string>
<!-- "left" in the meaning of "exited". This string is added to a chat after groups, channels, broadcasts or mailinglists are left. If a subject is needed, "You left the chat." would work as well. -->
<string name="group_left_by_you">Has sortit.</string>
<!-- "left" in the meaning of "exited"; %1$s will be replaced by name and address of the contact leaving the group -->
<string name="group_left_by_other">%1$sha abandonat el grup</string>
<string name="group_image_deleted_by_you">Heu esborrat la imatge de grup.</string>
@@ -915,6 +935,9 @@
<string name="ephemeral_timer_1_week_by_you">Heu definit el temporitzador dels missatges fonedissos a 1 setmana</string>
<!-- %1$s will be replaced by name and address of the contact -->
<string name="ephemeral_timer_1_week_by_other">%1$s ha definit el temporitzador dels missatges efímers a 1 setmana.</string>
<string name="ephemeral_timer_1_year_by_you">Has establert el temporitzador de missatges efímers a 1 any.</string>
<!-- %1$s will be replaced by name of the contact -->
<string name="ephemeral_timer_1_year_by_other">El temporitzador de missatges efímers sha establert a 1 any per %1$s.</string>
<!-- %1$s will be replaced by the number of minutes (always >1) the timer is set to -->
<string name="ephemeral_timer_minutes_by_you">Heu definit el temporitzador dels missatges fonedissos a %1$s minuts.</string>
<!-- %1$s will be replaced by the number of minutes (always >1) the timer is set to, %2$s will be replaced by name and address of the contact -->
@@ -931,14 +954,8 @@
<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 and address 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>
<!-- deprecated -->
<string name="chat_protection_broken">%1$s ha enviat un missatge des d\'un altre dispositiu.</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>
<!-- deprecated -->
<string name="chat_protection_broken_tap_to_learn_more">%1$s ha enviat un missatge des d\'un altre dispositiu. Toqueu per a obtenir més informació.</string>
<!-- deprecated -->
<string name="chat_protection_broken_explanation">No es pot garantir el xifrat d\'extrem a extrem, probablement perquè %1$s ha reinstal·lat Delta Chat o ha enviat un missatge des d\'un altre dispositiu.\n\n Podeu trobar-vos físicament amb la persona i tornar a escanejar el seu codi QR per tomar-hi a tenir xifrat d\'extrem a extrem.</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>
<string name="invalid_unencrypted_explanation">Per a establir xifratge d\'extrem a extrem, heu de trobar-vos amb els contactes en persona i escanejar el seu codi QR per a poder afegir-los.</string>
<string name="learn_more">Més informació</string>
@@ -955,7 +972,7 @@
<!-- qr code stuff -->
<string name="qr_code">Codi QR</string>
<string name="load_qr_code_as_image">Carrega el codi QR com a imatge</string>
<string name="load_qr_code_as_image">Carrega un codi QR com a imatge</string>
<string name="qrscan_title">Escaneja codi QR</string>
<string name="qrscan_hint">Poseu la càmera davant del codi QR</string>
<string name="qrscan_hint_desktop">Mou el codi QR a la càmera</string>
@@ -997,16 +1014,11 @@
<!-- 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>
<!-- deprecated -->
<string name="secure_join_wait_timeout">Encara no es pot garantir el xifrat punt-a-punt, però ja podeu enviar un missatge.</string>
<string name="contact_verified">%1$s verificat.</string>
<!-- deprecated, was stock string -->
<string name="contact_not_verified">No es pot verificar%1$s</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>
<!-- deprecated, was stock string -->
<string name="contact_setup_changed">Configuració canviada per %1$s</string>
<string name="verified_by_unknown">Afegit</string>
<string name="verified_contact_required_explain">Per a garantir el xifrat extrem a extrem, només podeu afegir a aquest grup contactes que tinguin la marca verda.\n\nPodeu trobar-vos en persona amb els contactes per a escanejar els seus codis QR i poder-los afegir. </string>
<string name="mailto_dialog_header_select_chat">Trieu el xat on enviar el missatge</string>
<!-- first placeholder is the name of the chat -->
@@ -1049,9 +1061,6 @@
<string name="ImageEditorHud_flip">Capgira</string>
<string name="ImageEditorHud_rotate">Gira</string>
<!-- deprecated, use string "E-Mail" for non-encrypted messages instead -->
<string name="encrypted_message">Missatge encriptat</string>
<!-- strings introduced on desktop. we want to share strings between the os, in general, please do not add generic strings here -->
<string name="about_offical_app_desktop">Aquesta és l\'aplicació d\'escriptori oficial de Delta Chat.</string>
<string name="about_licensed_under_desktop">Aquest programari és llicenciat amb la llicència GNU GPL versió 3 i el codi font és disponible a GitHub</string>
@@ -1166,7 +1175,6 @@
<string name="InfoPlist_NSPhotoLibraryAddUsageDescription">Delta Chat vol desar imatges a la biblioteca de fotografies.</string>
<string name="InfoPlist_NSFaceIDUsageDescription">Delta Chat pot usar Face ID per a protegir el vostre perfil local, la creació de còpies de seguretat i la configuració d\'un segon dispositiu.</string>
<!-- android specific strings, developers: please take care to remove strings that are no longer used! -->
<string name="pref_instant_delivery">Entrega immediata</string>
<string name="pref_background_notifications">Usa la connexió en segon pla</string>
@@ -1179,8 +1187,6 @@
<string name="perm_enable_bg_reminder_title">Prem aquí per rebre missatges mentre Delta Chat és en segon pla.</string>
<string name="perm_enable_bg_already_done">Ja heu autoritzat Delta Chat a rebre missatges en segon pla.\n\nSi encara no us arriben els missatge, reviseu els paràmetres de sistema.</string>
<!-- deprecated -->
<string name="update_1_50_android">Què hi ha de nou?\n\n❤️‍🔥 Un selector d\'emojis nou amb més emojis\n\n🎮 Aplicacions al xat millorades: rebeu notificacions i obriu les aplicacions suportades segons el context. P. ex. obriu una entrada afegida al calendari directament.\n\n👍 Rebeu notificacions de les reaccions dels vostres missatges\n\n... 🛠️ Correccions i moltes coses més a %1$s</string>
<!-- deprecated -->
<string name="update_switch_profile_placement">️ S\'ha mogut l\'opció «Canvia el perfil»: toqueu en la vostra imatge de perfil en la cantonada superior de la finestra principal per a afegir o canviar perfils 💡</string>
<!-- device messages for updates -->
<string name="update_2_0">Què hi ha de nou?\n\n💯 Lencriptació de punta a punta és fiable i permanent. Els candaus 🔒 han desaparegut!\n\n✉️ El correu electrònic clàssic sense encriptació de punta a punta es mostra amb el símbol d\'un sobre.\n\n😻 Nova pantalla de perfil millorada per a tots els teus contactes.\n\n🔲 Nou botó per accedir ràpidament a les aplicacions utilitzades als xats.\n\n❤️ Si us plau, fes una donació per ajudar-nos a continuar independents i seguir aportant millores: %1$s</string>
</resources>
+1 -7
View File
@@ -319,6 +319,7 @@
<string name="tab_docs_empty_hint">بەڵگە و مۆسیقا و هەموو پەڕگە هاوبەشکراوەکانی نێو ئەم وتووێژە، لێرەدا پیشان دەدرێن.</string>
<string name="media_preview">پێشدیتنی ڕەنگاڵە</string>
<string name="send_message">پەیام بنێرە</string>
<string name="login_title">بچۆ ژوورەوە</string>
<string name="login_header">بچۆ ژوورەوە بۆ سێرڤێرەکەی خۆت</string>
<string name="login_explain">بچۆ ژوورەوە بە ناونیشانی ئیمەیلێکەوە</string>
@@ -495,10 +496,6 @@
<string name="qraccount_ask_create_and_login">دەتەوێت ناونیشانی ئیمەیلێکی نوێ لەسەر \"%1$s\" درووست بکەیت و لەوێ بچیتە ژوورەوە؟</string>
<string name="qraccount_qr_code_cannot_be_used">ناتوانی لە کۆدی QRە سکان کراوەکە کەڵک بگریت بۆ پێکهێنانی هەژمارێکی نوێ.</string>
<string name="contact_verified">%1$s پشتڕاست کرایەوە</string>
<!-- deprecated, was stock string -->
<string name="contact_not_verified">%1$s پشتڕاست ناکرێتەوە.</string>
<!-- deprecated, was stock string -->
<string name="contact_setup_changed">ڕێکخستن بۆ %1$s گۆڕا.</string>
<!-- notifications -->
<string name="notify_reply_button">وەڵام دانەوە</string>
<string name="notify_new_message">پەیامی نوێ</string>
@@ -516,9 +513,6 @@
<string name="perm_explain_access_to_mic_denied">بۆ هەناردنی پەیامی دەنگی، بچۆ بەشی ڕێکخستنەکانی نەرمامێرەکە، لە بەشی \"ڕێگەپێدان\"دا، \"مایک\" چالاک بکەوە.</string>
<string name="perm_explain_access_to_storage_denied">بۆ هەناردن و وەرگرتنی پەڕگەکان، بچۆ بەشی ڕێکخستنەکانی نەرمامێرەکە، لە بەشی \"ڕێگەپێدان\"دا، \"بیرگە\" چالاک بکەوە.</string>
<string name="perm_explain_access_to_location_denied">بۆ لکاندنی شوێنێک، بچۆ بەشی ڕێکخستنەکانی نەرمامێرەکە، لە بەشی \"ڕێگەپێدان\"دا، \"شوێن\" چالاک بکەوە.</string>
<!-- deprecated, use string "E-Mail" for non-encrypted messages instead -->
<string name="encrypted_message">پەیامی شفرەکراو</string>
<!-- strings introduced on desktop. we want to share strings between the os, in general, please do not add generic strings here -->
<string name="about_offical_app_desktop">ئەمە نەرمامێری فەرمی دێڵتا چاتە بۆ کۆمپووتەری ڕوومێز.</string>
<string name="welcome_desktop">بەخێر بێن بۆ دێڵتا چات</string>
+1 -44
View File
@@ -251,19 +251,7 @@
<!-- "Chat" is a verb here, "Message to" would also fit. the string might be used in the "New Chat" screen above the contact list -->
<string name="chat_with">Chatovat s...</string>
<string name="clone_chat">Klonovat chat</string>
<!-- deprecated -->
<string name="broadcast_list">Seznam vysílání</string>
<!-- deprecated -->
<string name="broadcast_lists">Seznamy vysílání</string>
<!-- deprecated -->
<string name="new_broadcast_list">Nový seznam vysílání</string>
<string name="add_recipients">Přidat příjemce</string>
<!-- deprecated -->
<string name="edit_broadcast_list">Upravit seznam vysílání</string>
<!-- deprecated -->
<string name="broadcast_list_name">Název seznamu vysílání</string>
<!-- deprecated -->
<string name="please_enter_broadcast_list_name">Prosím, zadejte název seznamu vysílání.</string>
<string name="menu_send">Odeslat</string>
<string name="menu_toggle_keyboard">Přepnout emoji klávesnici</string>
<string name="menu_edit_group">Upravit skupinu</string>
@@ -424,8 +412,6 @@
<string name="ask_start_chat_with">Přejete si chatovat s uživatelem %1$s?</string>
<!-- %1$s is replaced by a comma-separated list of names -->
<string name="ask_remove_members">Přejete si odstranit uživatele %1$s ze skupiny?</string>
<!-- deprecated -->
<string name="ask_remove_from_broadcast">Přejete si odstranit uživatele %1$s ze seznamu vysílání?</string>
<string name="open_url_confirmation">Přejete si otevřít tento odkaz?</string>
@@ -461,8 +447,6 @@
</plurals>
<!-- The placeholder will be replaced by the name of the recipient in a one-to-one chat. -->
<string name="chat_new_one_to_one_hint">Poslat zprávu uživateli %1$s.</string>
<!-- deprecated -->
<string name="chat_new_broadcast_hint">Seznam vysílání slouží k posílání zpráv příjemcům v chatu, který je pro ně pouze pro čtení.</string>
<string name="chat_new_group_hint">Ostatní tuto skupinu uvidí až poté, co odešlete první zprávu.</string>
<string name="chat_record_slide_to_cancel">Zrušit tahem</string>
<string name="chat_record_explain">Pro nahrání hlasové zprávy stiskněte a držte ikonu mikrofonu; uvolněte pro odeslání.</string>
@@ -570,11 +554,6 @@
<!-- option to show images in the gallery as square (instead of using correct width/height) -->
<string name="square_grid">Mřížka čtverců</string>
<string name="send_message">Odeslat zprávu</string>
<!-- deprecated, was stock string -->
<string name="aeap_addr_changed">Uživatel %1$s změnil svou adresu z %2$s na %3$s</string>
<!-- deprecated -->
<string name="aeap_explanation">Změnili jste svou e-mailovou adresu z %1$s na %2$s.\n\nPokud nyní pošlete zprávu ověřené skupině, všem kontaktům v ní se vaše adresa aktualizuje na novou.\n\nJe důrazně doporučeno, abyste u svého starého poskytovatele nastavili přesměrování všech e-mailů na novou adresu, jinak byste mohli přijít o zprávy od kontaktů, které vaši novou adresu dosud nedostaly. </string>
<!-- Multi Device -->
<!-- "Second Device" can also be translated as "Another Device", if that is catchier in the destination language. However, make sure to use the term consistently. -->
@@ -970,14 +949,8 @@
<string name="ephemeral_timer_weeks_by_you">Samomazací zprávy vyprší po %1$s týdnech ode mě.</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 and address of the contact -->
<string name="ephemeral_timer_weeks_by_other">Uživatel %2$s změnil časovač mizejících zpráv na %1$s týdnů.</string>
<!-- deprecated -->
<string name="chat_protection_broken">Uživatel %1$s odeslal zprávu z jiného zařízení.</string>
<string name="chat_protection_enabled_tap_to_learn_more">Nyní je garantováno koncové šifrování vašich zpráv. Stiskněte pro více informací.</string>
<string name="chat_protection_enabled_explanation">Nyní je garantováno, že všechny zprávy v tomto chatu jsou koncově šifrovány.\n\nKoncové šifrování zajišťuje soukromí mezi vámi a vašimi kontakty. Nedokáže je přečíst ani váš poskytovatel e-mailu.</string>
<!-- deprecated -->
<string name="chat_protection_broken_tap_to_learn_more">Uživatel %1$s odeslal zprávu z jiného zařízení. Stiskněte pro více informací.</string>
<!-- deprecated -->
<string name="chat_protection_broken_explanation">Koncové šifrování již nemůže být garantováno, pravděpodobně protože uživatel %1$s přeinstaloval Delta Chat nebo odeslal zprávu z jiného zařízení.\n\nPro obnovení garantovaného koncového šifrování se sejděte osobně a naskenujte si své QR kódy.</string>
<string name="invalid_unencrypted_tap_to_learn_more">⚠️%1$s vyžaduje koncové šifrování, které pro tento chat ještě není nastaveno. Stiskněte pro více informací. </string>
<string name="invalid_unencrypted_explanation">Pro navázání koncově šifrovaného spojení se můžete se svými kontakty setkat osobně a naskenovat jejich QR kódy.</string>
<string name="learn_more">Další informace</string>
@@ -1036,18 +1009,10 @@
<!-- placeholder will be replaced by the name of the inviter. -->
<string name="secure_join_replies">Uživatel %1$s odpověděl, čekání na přidání do skupiny...</string>
<string name="secure_join_wait">Navazování koncově šifrovaného spojení, prosím, čekejte...</string>
<!-- deprecated -->
<string name="secure_join_wait_timeout">Zatím nebylo navázáno koncově šifrované spojení, ale již nyní můžete posílat zprávy.</string>
<!-- deprecated, was stock string -->
<string name="secure_join_takes_longer">Vypadá to, že to trvá trochu déle. Možná jste vy nebo váš kontakt offline.\n\nProces však pokračuje na pozadí. Zatím můžete dělat něco jiného...</string>
<string name="contact_verified">Uživatel %1$s ověřen.</string>
<!-- deprecated, was stock string -->
<string name="contact_not_verified">Navázání garantovaného koncově šifrovaného spojení s uživatelem %1$s selhalo.</string>
<!-- Shown in contact profile. The placeholder will be replaced by the name of the contact that introduced the contact. -->
<string name="verified_by">Ověřeni uživatelem %1$s.</string>
<string name="verified_by_you">Ověřeni mnou</string>
<!-- deprecated, was stock string -->
<string name="contact_setup_changed">Nastavení pro uživatele %1$s změněno.</string>
<string name="verified_contact_required_explain">Aby mohlo být garantováno koncové šifrování, můžete do této skupiny přidávat pouze uživatele se zeleným zaškrtávátkem.\n\nSe svými kontakty se můžete setkat osobně a naskenovat jejich QR kód, abyste je ověřili.</string>
<string name="mailto_dialog_header_select_chat">Vyberte chat, do kterého chcete zprávu poslat</string>
<!-- first placeholder is the name of the chat -->
@@ -1090,9 +1055,6 @@
<string name="ImageEditorHud_flip">Zrcadlení</string>
<string name="ImageEditorHud_rotate">Otočení</string>
<!-- deprecated, use string "E-Mail" for non-encrypted messages instead -->
<string name="encrypted_message">Šifrovaná zpráva</string>
<!-- strings introduced on desktop. we want to share strings between the os, in general, please do not add generic strings here -->
<string name="about_offical_app_desktop">Toto je oficiální aplikace Delta Chat Desktop.</string>
<string name="about_licensed_under_desktop">Tento software je distribuován pod licencí GNU GPL verze 3 a jeho zdrojový kód je k dispozici na GitHubu.</string>
@@ -1207,7 +1169,6 @@
<string name="InfoPlist_NSPhotoLibraryAddUsageDescription">Delta Chat ukládá obrázky do vaší galerie.</string>
<string name="InfoPlist_NSFaceIDUsageDescription">Delta Chat umí využívat Face ID k ochraně vašeho lokálního profilu, vytváření záloh a přidávání dalších zařízení.</string>
<!-- android specific strings, developers: please take care to remove strings that are no longer used! -->
<string name="pref_instant_delivery">Okamžité doručení</string>
<string name="pref_background_notifications">Používat připojení na pozadí</string>
@@ -1220,8 +1181,4 @@
<string name="perm_enable_bg_reminder_title">Stiskem nastavíte přijímání zpráv, když je aplikace Delta Chat na pozadí.</string>
<string name="perm_enable_bg_already_done">Přijímání zpráv na pozadí jste již aplikaci Delta Chat povolili.\n\nV případě, že vám zprávy na pozadí stále nechodí, prosím zkontrolujte svá systémová nastavení.</string>
<!-- deprecated -->
<string name="update_1_50_android">Co je nového?\n\n❤️‍🔥 Nová nabídka emoji s více druhy emoji\n\n🎮 Vylepšené aplikace v chatu: Oznámení a podpůrné aplikace přímo v kontextu chatu, např. odkaz na novou nové položky z kalendáři\n\n👍 Oznámení o reakcích na zprávy\n\n... 🛠️ OPRAVY a DALŠÍ na %1$s</string>
<!-- deprecated -->
<string name="update_switch_profile_placement">️ Volba \"Přepnout profil\" byla přesunuta: Pro přidávání či přepínání profilů stiskněte svůj profilový obrázek v horním rohu hlavní obrazovky.</string>
</resources>
</resources>
+1 -7
View File
@@ -343,6 +343,7 @@
<string name="tab_docs_empty_hint">Dokumenter, musik og andre filer delt i denne samtale vises her.</string>
<string name="media_preview">Medie forhåndsvisning</string>
<string name="send_message">Send besked</string>
<!-- Connectivity -->
<!-- Headline for the "Inbox" eg. in the "Connectivity" view -->
<string name="incoming_messages">Indkommende beskeder</string>
@@ -631,10 +632,6 @@
<string name="qraccount_ask_create_and_login_another">Opret ny e-mail adresse på \"%1$s\" og log ind der?\n\nDin eksisterende konto bliver ikke slettet. Brug \"Skift konto\" for at skifte imellem dine konti.</string>
<string name="qraccount_qr_code_cannot_be_used">Den skannede QR-kode kan ikke bruges til at opsætte en ny konto.</string>
<string name="contact_verified">%1$s bekræftet.</string>
<!-- deprecated, was stock string -->
<string name="contact_not_verified">Kan ikke bekræfte %1$s</string>
<!-- deprecated, was stock string -->
<string name="contact_setup_changed">Ændret opsætning for %1$s</string>
<!-- notifications -->
<string name="notify_reply_button">Svar</string>
<string name="notify_new_message">Ny besked</string>
@@ -656,9 +653,6 @@
<string name="ImageEditorHud_draw_anywhere_to_blur">Tegn hvor som helst for at sløre</string>
<string name="ImageEditorHud_rotate">Roter</string>
<!-- deprecated, use string "E-Mail" for non-encrypted messages instead -->
<string name="encrypted_message">Krypteret besked</string>
<!-- strings introduced on desktop. we want to share strings between the os, in general, please do not add generic strings here -->
<string name="about_offical_app_desktop">Dette er den officielle Delta Chat Desktop app.</string>
<string name="about_licensed_under_desktop">Denne software er licenseret under GNU GPL version 3 og kildekoden er tilgængelig på GitHub</string>

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