diff --git a/CHANGELOG.md b/CHANGELOG.md index d64e76c2803a52e9f82b408e40487fe8fe078d13..b5d72e162f7f1b7103e1eedf4cdfb0dd140484f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Version 2.16.7 + +* Add timeout to call initiation + ### Version 2.16.6 * Offer higher automatic file accept values diff --git a/build.gradle b/build.gradle index a55012141809016ee9496d5479e0917ea77f31ff..8871955ec30b1f6cf2aa9918537913a5f0511da3 100644 --- a/build.gradle +++ b/build.gradle @@ -48,15 +48,14 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' implementation "androidx.core:core:1.10.1" - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2' implementation project(':libs:annotation') annotationProcessor project(':libs:annotation-processor') - implementation 'androidx.viewpager:viewpager:1.0.0' - playstoreImplementation('com.google.firebase:firebase-messaging:24.0.0') { + playstoreImplementation('com.google.firebase:firebase-messaging:24.0.1') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -72,11 +71,12 @@ dependencies { implementation 'androidx.cardview:cardview:1.0.0' implementation "androidx.preference:preference:1.2.1" implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'com.google.android.material:material:1.12.0' - implementation 'androidx.work:work-runtime:2.9.0' + implementation 'com.google.android.material:material:1.13.0-alpha06' + implementation 'androidx.work:work-runtime:2.9.1' - implementation "androidx.emoji2:emoji2:1.4.0" - freeImplementation "androidx.emoji2:emoji2-bundled:1.4.0" + implementation "androidx.emoji2:emoji2:1.5.0" + freeImplementation "androidx.emoji2:emoji2-bundled:1.5.0" + implementation "androidx.emoji2:emoji2-emojipicker:1.5.0" implementation 'org.bouncycastle:bcmail-jdk18on:1.78.1' implementation 'com.google.zxing:core:3.5.3' @@ -105,7 +105,7 @@ dependencies { implementation 'com.google.guava:guava:32.1.3-android' implementation 'io.michaelrocks:libphonenumber-android:8.13.35' - implementation 'im.conversations.webrtc:webrtc-android:119.0.1' + implementation 'im.conversations.webrtc:webrtc-android:129.0.0' implementation 'io.github.nishkarsh:android-permissions:2.1.6' implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.documentfile:documentfile:1.0.1' @@ -259,11 +259,6 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' ndk.debugSymbolLevel = 'full' } - debug { - shrinkResources true - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } } diff --git a/fastlane/metadata/android/de-DE/changelogs/4211804.txt b/fastlane/metadata/android/de-DE/changelogs/4211804.txt new file mode 100644 index 0000000000000000000000000000000000000000..ea2b6a9f2ca70567fd295fae7d532e9bce8ad6ad --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4211804.txt @@ -0,0 +1 @@ +* Zeitüberschreitung beim Anrufaufbau hinzugefügt diff --git a/fastlane/metadata/android/en-US/changelogs/4211804.txt b/fastlane/metadata/android/en-US/changelogs/4211804.txt new file mode 100644 index 0000000000000000000000000000000000000000..96986442d5be591179014a447b36eba54d696060 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4211804.txt @@ -0,0 +1 @@ +* Add timeout to call initiation diff --git a/fastlane/metadata/android/gl-ES/changelogs/4211804.txt b/fastlane/metadata/android/gl-ES/changelogs/4211804.txt new file mode 100644 index 0000000000000000000000000000000000000000..fd5430efeb7190cf9d575e88a6b5c23f887d154b --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4211804.txt @@ -0,0 +1 @@ +* Engadido límite para iniciar unha chamada diff --git a/fastlane/metadata/android/pl-PL/changelogs/4211804.txt b/fastlane/metadata/android/pl-PL/changelogs/4211804.txt new file mode 100644 index 0000000000000000000000000000000000000000..94ac5e6d4c764cf6dfd92ca76e7fe3f89245fc38 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4211804.txt @@ -0,0 +1 @@ +* Dodanie limitu czasu zestawiania połączenia diff --git a/fastlane/metadata/android/sq/changelogs/4211704.txt b/fastlane/metadata/android/sq/changelogs/4211704.txt new file mode 100644 index 0000000000000000000000000000000000000000..c52c0bb4447cdcbc34cc6a7a6723a8730be611d8 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4211704.txt @@ -0,0 +1,3 @@ +* Ofro vlera më të larta pranimi automatik kartelash +* Dhënie e më tepër hollësish te “Hollësi shërbyesi” +* Ndreqje të metash të ndryshme diff --git a/fastlane/metadata/android/sq/changelogs/4211804.txt b/fastlane/metadata/android/sq/changelogs/4211804.txt new file mode 100644 index 0000000000000000000000000000000000000000..f3c67744f1a646dfd4725b1f76a011f6ef394648 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4211804.txt @@ -0,0 +1 @@ +* Shtim mbarimi kohe te gatitje thirrjeje diff --git a/fastlane/metadata/android/sv-SE/changelogs/349.txt b/fastlane/metadata/android/sv-SE/changelogs/349.txt new file mode 100644 index 0000000000000000000000000000000000000000..d0ea39a04778395f09367413a5da68714bda18eb --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/349.txt @@ -0,0 +1,4 @@ +* Introducera expert-inställning för att utföra kanal-upptäckt i den lokala servern istället för search.jabber.network +* Aktivera leverans-kontrollmarkeringar som standard och ta bort inställning +* Aktivera 'Skicka-knappen inidikerar status' som standard och ta bort inställning +* Flytta inställningar för Säkerhetskopiering och Bakgrundstjänst till huvud-skärmen diff --git a/fastlane/metadata/android/sv-SE/changelogs/405.txt b/fastlane/metadata/android/sv-SE/changelogs/405.txt new file mode 100644 index 0000000000000000000000000000000000000000..71ae2e97333e1b44dcdc253e9dc272dda21a16aa --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/405.txt @@ -0,0 +1 @@ +* Quicksy: Ta automatiskt emot bekräftelse-SMS diff --git a/fastlane/metadata/android/sv-SE/changelogs/42015.txt b/fastlane/metadata/android/sv-SE/changelogs/42015.txt new file mode 100644 index 0000000000000000000000000000000000000000..c8cc57ce3c28efb718a5a44976c44e66137ad9b0 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/42015.txt @@ -0,0 +1 @@ +* mindre A/V-förbättringar diff --git a/fastlane/metadata/android/sv-SE/changelogs/42065.txt b/fastlane/metadata/android/sv-SE/changelogs/42065.txt new file mode 100644 index 0000000000000000000000000000000000000000..396a873527b32654a2fb0cbdf6a23440ee575f1a --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/42065.txt @@ -0,0 +1 @@ +* Introducera nytt fil-format för säkerhetskopiering diff --git a/fastlane/metadata/android/sv-SE/changelogs/4211604.txt b/fastlane/metadata/android/sv-SE/changelogs/4211604.txt new file mode 100644 index 0000000000000000000000000000000000000000..2c4a3e81254c44aa44b91bece79d45ab6ea0785f --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/4211604.txt @@ -0,0 +1 @@ +* Mindre buggfixar diff --git a/fastlane/metadata/android/uk/changelogs/4211804.txt b/fastlane/metadata/android/uk/changelogs/4211804.txt new file mode 100644 index 0000000000000000000000000000000000000000..c3490793c919058ab1c1a700cb0fde390e090862 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4211804.txt @@ -0,0 +1 @@ +* Додано таймаут для ініціювання виклику diff --git a/fastlane/metadata/android/zh-CN/changelogs/4211704.txt b/fastlane/metadata/android/zh-CN/changelogs/4211704.txt new file mode 100644 index 0000000000000000000000000000000000000000..8499115544947f042f1f944c1a06cddb46cb8448 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4211704.txt @@ -0,0 +1,3 @@ +* 提供更大的自动下载文件的大小 +* 在“服务器信息”中提供更多信息 +* 各种错误修复 diff --git a/fastlane/metadata/android/zh-CN/changelogs/4211804.txt b/fastlane/metadata/android/zh-CN/changelogs/4211804.txt new file mode 100644 index 0000000000000000000000000000000000000000..371baba7bfbc1e84b7c5f1fdcf7fc7cee6cc1096 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4211804.txt @@ -0,0 +1 @@ +* 添加超时到通话发起 diff --git a/src/cheogram/java/com/cheogram/android/TagEditorView.java b/src/cheogram/java/com/cheogram/android/TagEditorView.java index ed3b5242264c6dc8a8c7e838aa21dccfa3e9ef84..43dd551b3505c6698e360156899839a733008692 100644 --- a/src/cheogram/java/com/cheogram/android/TagEditorView.java +++ b/src/cheogram/java/com/cheogram/android/TagEditorView.java @@ -37,7 +37,7 @@ public class TagEditorView extends TokenCompleteTextView { @Override protected View getViewForObject(ListItem.Tag tag) { LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Activity.LAYOUT_INFLATER_SERVICE); - final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, (ViewGroup) getParent(), false); + final TextView tv = (TextView) inflater.inflate(R.layout.item_tag, (ViewGroup) getParent(), false); tv.setText(tag.getName()); tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(getContext(),XEP0392Helper.rgbFromNick(tag.getName())))); return tv; diff --git a/src/conversations/fastlane/metadata/android/de-DE/full_description.txt b/src/conversations/fastlane/metadata/android/de-DE/full_description.txt index d6b5d4b1ec2b42d2e38fdf952857969004b39384..755334362c87d92d3e93b24ec2e19e3b51cc1de0 100644 --- a/src/conversations/fastlane/metadata/android/de-DE/full_description.txt +++ b/src/conversations/fastlane/metadata/android/de-DE/full_description.txt @@ -28,7 +28,7 @@ Conversations funktioniert mit jedem XMPP-Server. XMPP ist jedoch ein erweiterba Diese XEPs sind es derzeit: -* XEP-0065: SOCKS5 Bytestreams (oder mod_proxy65). Wird für die Übertragung von Dateien verwendet, wenn sich beide Parteien hinter einer Firewall (NAT) befinden. +* XEP-0065: SOCKS5 Bytestreams (oder mod_proxy65). Wird für die Übertragung von Dateien verwendet, wenn sich beide Parteien hinter einer Firewall oder NAT befinden. * XEP-0163: Personal Eventing Protocol für Profilbilder * XEP-0191: Mit dem Blockierungsbefehl kannst du Spammer auf eine schwarze Liste setzen oder Kontakte blockieren, ohne sie aus deiner Liste zu entfernen. * XEP-0198: Stream Management ermöglicht es XMPP, kleinere Netzwerkausfälle und Änderungen der zugrunde liegenden TCP-Verbindung zu überstehen. diff --git a/src/conversations/fastlane/metadata/android/gl-ES/full_description.txt b/src/conversations/fastlane/metadata/android/gl-ES/full_description.txt index 9bdcf9042de371fcd868b9cf9e7d5ad8bc155e57..7b17cefd23514a819a846ba14be545c4c1097b0b 100644 --- a/src/conversations/fastlane/metadata/android/gl-ES/full_description.txt +++ b/src/conversations/fastlane/metadata/android/gl-ES/full_description.txt @@ -29,7 +29,7 @@ Conversations da soporte a un par delas que axudan a mellorar a experiencia de u Estes XEPs son - neste intre: -* XEP-0065: SOCKS5 Bytestreams (ou mod_proxy65). Usado para a transferencia de ficheiros se as dúas partes están detrás dun cortalumes (NAT). +* XEP-0065: SOCKS5 Bytestreams (ou mod_proxy65). Usado para a transferencia de ficheiros se as dúas partes están detrás dun cortalumes ou NAT. * XEP-0163: Personal Eventing Protocol para os avatares * XEP-0191: O bloqueo de ordes permiteche bloquear spammer ou contactos sen eliminalos das túas listaxes. * XEP-0198: Stream Management permite que XMPP sobreviva a caídas da rede e cambios na conexión TCP. diff --git a/src/conversations/fastlane/metadata/android/nl-NL/full_description.txt b/src/conversations/fastlane/metadata/android/nl-NL/full_description.txt index 3d12812f9cb3e361709c6a42382032b4e8752877..9261da0288eddb7f5029d38b9a927fb6986dc2c5 100644 --- a/src/conversations/fastlane/metadata/android/nl-NL/full_description.txt +++ b/src/conversations/fastlane/metadata/android/nl-NL/full_description.txt @@ -28,7 +28,7 @@ Conversations werkt met elke bestaande XMPP-server. XMPP is echter een uitbreidb Deze XEP's zijn - vanaf nu: -* XEP-0065: SOCKS5 Bytestreams (or mod_proxy65). Wordt gebruikt om bestanden over te dragen als beide partijen achter een firewall zitten (NAT). +* XEP-0065: SOCKS5 Bytestreams (or mod_proxy65). Wordt gebruikt om bestanden over te dragen als beide partijen achter een firewall of NAT zitten. * XEP-0163: Personal Eventing Protocol for avatars * XEP-0191: Blocking command laat je spammers op de zwarte lijst zetten of contacten blokkeren zonder ze uit je selectie te verwijderen. * XEP-0198: Stream Management stelt XMPP in staat om kleine netwerkuitval en veranderingen van de onderliggende TCP-verbinding te overleven. diff --git a/src/conversations/fastlane/metadata/android/pl-PL/full_description.txt b/src/conversations/fastlane/metadata/android/pl-PL/full_description.txt index 442e1e8263a4a57ba82f80f6b310bdb36ed53cef..e3635e96bf5574195a5a3e46560bd55896f127e2 100644 --- a/src/conversations/fastlane/metadata/android/pl-PL/full_description.txt +++ b/src/conversations/fastlane/metadata/android/pl-PL/full_description.txt @@ -28,7 +28,7 @@ Conversations działa z każdym dostępnym serwerem XMPP, jednak XMPP to rozsze Obecnie są obsługiwane następujące rozszerzenia: -* XEP-0065: SOCKS5 Bytestreams (lub mod_proxy65). Będzie używany do przesyłania plików jeżeli obie strony znajdują się za zaporą (NAT); +* XEP-0065: SOCKS5 Bytestreams (lub mod_proxy65). Będzie używany do przesyłania plików jeżeli obie strony znajdują się za zaporą lub NAT; * XEP-0163: Personal Eventing Protocol dla awatarów; * XEP-0191: Blocking Command umożliwia ochronę przed spamerami lub blokowanie bez usuwanie ich z rostera; * XEP-0198: Stream Management pozwala na przetrwanie krótkich braków połączenia z siecią oraz zmian używanego połączenia TCP; diff --git a/src/conversations/fastlane/metadata/android/pt-BR/full_description.txt b/src/conversations/fastlane/metadata/android/pt-BR/full_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..e4a6318952a5a5f7d1d5cdf5c06852233cbd6659 --- /dev/null +++ b/src/conversations/fastlane/metadata/android/pt-BR/full_description.txt @@ -0,0 +1,39 @@ +Fácil de usar, estável, e bem otimizado. Com suporte à imagens, conversas em grupo, e criptografia de ponta-a-ponta. + +Base do design: + +* Ser o mais bonito e fácil de usar possível sem sacrificar privacidade ou segurança +* Depender de protocolos existentes e bem estabelecidos +* Não precisar de uma conta Google ou especificamente do Google Cloud Messaging (GCM) +* Usar o mínimo de permissões possíveis + +Recursos: + +* Criptografia de ponta-a-ponta com OMEMO ou OpenPGP. +* Envio e recebimento de imagens +* Chamadas de áudio e vídeo criptografadas (DTLS-SRTP) +* UI intuitiva que segue as regras do Android Design +* Imagens / Avatares para seus Contatos +* Sincroniza com o cliente desktop +* Conferências (com suporte a favoritos) +* Integração com livro de endereços +* Múltiplas contas / caixa de entrada unificada +* Baixo uso da bateria + +O Conversations faz com que seja fácil criar uma conta no servidor conversations.im. Mesmo assim, o Conversations funcionará com qualquer outro servidor XMPP. Muitos servidores XMPP são mantidos por voluntários e são gratuitos. + +Recursos XMPP: + +O Conversations funciona com qualquer servidor XMPP por aí. Porém, o XMPP é um protocolo extensível. Estas extensões são padronizadas, nos chamados XEPs. O Conversations suporta alguns desses para tornar a experiência do usuário melhor. Existe uma chance que seu servidor XMPP atual não suporta estas extensões. Por causa disso, para tirar melhor proveito do Conversations, você pode considerar alterar para um servidor XMPP que as suporta - ou melhor ainda - manter seu próprio servidor XMPP para você e seus amigos. + +Estas XEPs são - atualmente: + +* XEP-0065: Bytestreams SOCKS5 (ou mod_proxy65). Será usado para transferir arquivos caso ambas partes estão atrás de um firewall ou NAT. +* XEP-0163: Protocolo de Eventos Pessoais para avatares +* XEP-0191: O comando de bloquear permite que bloqueie spam ou contatos sem removê-los de sua lista. +* XEP-0198: Gerenciamento de Streams permite que o XMPP sobreviva a pequenas interrupções ou mudanças na conexão TCP. +* XEP-0280: Carbonos de Mensagem que automaticamente sincroniza as mensagens que envia no computador e permite que você troque de cliente facilmente do seu celular para seu computador e ao contrário em uma conversa. +* XEP-0237: Controle de Versões da Lista principalmente para economizar banda larga em conexões móveis ruins +* XEP-0313: Gerenciamento do Histórico de Mensagens para sincronizar o histórico de mensagens com o servidor. Recupere mensagens enviadas enquanto o Conversations estava off-line. +* XEP-0352: Indicação do Estado do Cliente permite que o servidor saiba quando o Conversations está ou não em segundo plano. Permite que o servidor economize banda larga. +* XEP-0363: Envio de Arquivos HTTP permite que compartilhe arquivos em conferências e com contatos off-line. Requer um componente adicional em seu servidor. diff --git a/src/conversations/fastlane/metadata/android/pt-BR/short_description.txt b/src/conversations/fastlane/metadata/android/pt-BR/short_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..043bfde950bfe4461a13e3fa8789aa987483037b --- /dev/null +++ b/src/conversations/fastlane/metadata/android/pt-BR/short_description.txt @@ -0,0 +1 @@ +Mensageiro XMPP criptografado, fácil de usar, para o seu dispositivo móvel diff --git a/src/conversations/fastlane/metadata/android/ro/full_description.txt b/src/conversations/fastlane/metadata/android/ro/full_description.txt index 2d4ae419dc8ea47e99de4ce83565b29e7d393a14..789b45210039a6baa15881ad4a46d3aa11018a28 100644 --- a/src/conversations/fastlane/metadata/android/ro/full_description.txt +++ b/src/conversations/fastlane/metadata/android/ro/full_description.txt @@ -27,7 +27,7 @@ Caracteristici XMPP: Conversations funcționează cu orice server XMPP existent. Cu toate acestea, XMPP este un protocol extensibil. Aceste extensii sunt, de asemenea, standardizate în așa-numitele XEP-uri. Conversations suportă câteva dintre acestea pentru a îmbunătăți experiența generală a utilizatorului. Există o șansă ca serverul XMPP actual să nu suporte aceste extensii. Prin urmare, pentru a profita la maximum de Conversations, ar trebui să luați în considerare fie trecerea la un server XMPP care să suporte aceste extensii, fie - și mai bine - să rulați propriul server XMPP pentru dumneavoastră și prietenii dumneavoastră. Aceste XEP-uri sunt - deocamdată: -* XEP-0065: SOCKS5 Bytestreams (sau mod_proxy65). Va fi utilizat pentru a transfera fișiere dacă ambele părți se află în spatele unui firewall (NAT). +* XEP-0065: SOCKS5 Bytestreams (sau mod_proxy65). Va fi utilizat pentru a transfera fișiere dacă ambele părți se află în spatele unui firewall sau NAT. * XEP-0163: Protocol de evenimente personale pentru avatare. * XEP-0191: Comanda de blocare vă permite să puneți pe lista neagră spamerii sau să blocați contactele fără a le elimina din listă. * XEP-0198: Stream Management permite XMPP să supraviețuiască unor mici întreruperi de rețea și schimbărilor conexiunii TCP de bază. diff --git a/src/conversations/fastlane/metadata/android/sr/full_description.txt b/src/conversations/fastlane/metadata/android/sr/full_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..10f602f71998e6e17c4b4cbfa9d271cc2d285c98 --- /dev/null +++ b/src/conversations/fastlane/metadata/android/sr/full_description.txt @@ -0,0 +1,39 @@ +Лагана за коришћење, поуздана, са малом потрошњом батерије. Има уграђену подршку за слике, групне преписке и e2e шифровање. + +Приципи дизајна: + +* Леп изглед и једноставност коришћења, без компромитовања безбедности и приватности +* Ослања се на постојеће, добро установљене протоколе +* Не захтева Google налог нити конкретно Google Cloud Messaging (GCM) +* Захтева минималан број дозвола + +Способности: + +* E2E шифровање уз OMEMO или OpenPGP +* Слање и примање слика +* Шифровани аудио и видео позиви (DTLS-SRTP) +* Интуитивно корисничко сучеље које прати смернице Android дизајна +* Слике / аватари за твоје контакте +* Синхронизација са десктоп клијентима +* Конференције (уз подршку за обележивачe) +* Интеграција са имеником +* Више истовремених налога / обједињени inbox +* Веома мали утицај на потрошњу батерије + +Conversations омогућава веома лако отварање налога на бесплатном серверу conversations.im. Поред тога, Conversations може да ради и уз било који други XMPP сервер. Многе XMPP сервере одржавају добровољци и бесплатни су за коришћење. + +XMPP способности: + +Conversations ради уз сваки XMPP сервер. Међутим XMPP је проширив протокол. Ова проширења су такође стандардизована у такозваним XEP-овима. Conversations има подршку за неколико тих проширења ради побољшања свеопштег корисничког искуства. Постоји шанса да твој тренутни XMPP сервер не подржава ова проширења. Да би максимално искористио/ла Conversations требало би да размислиш или о преласку на XMPP сервер који подржава ова проширења или - још боље - да успоставиш сопствени XMPP сервер за тебе и твоје пријатеље. + +Ови XEP-ови су - за сада: + +* XEP-0065: SOCKS5 Bytestreams (или mod_proxy65). Користи се за пребацивање фајлова ако су обе стране иза firewall-а (NAT). +* XEP-0163: Personal Eventing Protocol за аватаре +* XEP-0191: Blocking command омогућава blacklist-овање спамера или блокирање контаката без њиховог уклањања из твог списка. +* XEP-0198: Stream Management омогућава да XMPP преживи мање прекиде на мрежи и промене у TCP веза. +* XEP-0280: Message Carbons који аутоматски синхронизује поруке које шаљеш на свој десктоп клијент и са тим ти омогућава да се лако пребациш са мобилног клијента на десктоп клијент и обратно у оквиру исте преписке. +* XEP-0237: Roster Versioning углавном за уштеду количине пренетих података на непоузданим мобилним везама +* XEP-0313: Message Archive Management синхронизује историју порука са сервером. Усаглашава поруке које су послате док Conversations није био на вези. +* XEP-0352: Client State Indication обавештава сервер да ли је Conversations у позадини или не. Омогућава серверу да уштеди количину пренетих података спречавањем слања непотребних пакета. +* XEP-0363: HTTP File Upload омогућава дељење фајлова у конференцијама и са контактима који нису на вези. Захтева додатну компоненту на твом серверу. diff --git a/src/conversations/fastlane/metadata/android/sr/short_description.txt b/src/conversations/fastlane/metadata/android/sr/short_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..e9096599feeda36e38005a2131515f8785213f3b --- /dev/null +++ b/src/conversations/fastlane/metadata/android/sr/short_description.txt @@ -0,0 +1 @@ +Шифрована и лагана XMPP апликација за инстант поруке за твој мобилни телефон diff --git a/src/conversations/fastlane/metadata/android/uk/full_description.txt b/src/conversations/fastlane/metadata/android/uk/full_description.txt index 39971ed5571be157835f2e2b959d96f74a5c7f55..80f67afb1e8f0fba01ed571258a43ef1a9a8caa0 100644 --- a/src/conversations/fastlane/metadata/android/uk/full_description.txt +++ b/src/conversations/fastlane/metadata/android/uk/full_description.txt @@ -28,7 +28,7 @@ Conversations працює з будь-яким сервером XMPP. Прот На даний час підтримуються такі XEP: -* XEP-0065: SOCKS5 Bytestreams (або mod_proxy65). Використовується для передачі файлів, якщо обидві сторони знаходяться за брандмауером (NAT). +* XEP-0065: SOCKS5 Bytestreams (або mod_proxy65). Використовується для передачі файлів, якщо обидві сторони знаходяться за брандмауером або NAT. * XEP-0163: персональний протокол подій для аватарів * XEP-0191: команда блокування дозволяє Вам заносити спамерів у чорний список або блокувати контакти, не видаляючи їх зі свого списку. * XEP-0198: керування потоками дозволяє XMPP витримувати невеликі перебої в мережі та зміни основного TCP-з'єднання. diff --git a/src/conversations/fastlane/metadata/android/zh-CN/full_description.txt b/src/conversations/fastlane/metadata/android/zh-CN/full_description.txt index 87d4ecea0ae5c85b1e7cf7e9a3bac6aed32590e4..96cdff57cae47da73f7643f6506dd5c325c8e16e 100644 --- a/src/conversations/fastlane/metadata/android/zh-CN/full_description.txt +++ b/src/conversations/fastlane/metadata/android/zh-CN/full_description.txt @@ -20,7 +20,7 @@ * 多账号/统一消息栏 * 对电池寿命的影响非常小 -Conversations 使在免费的 conversations.im 服务器上创建账号变得非常简单。不过,Conversations 也适用于任何其他 XMPP 服务器。许多 XMPP 服务器都是由志愿者免费运行的。 +您可以很容易地使用 Conversations 在免费的 conversations.im 服务器上创建账号。不过,Conversations 也适用于任何其他 XMPP 服务器。许多 XMPP 服务器由志愿者免费运行。 XMPP 功能: @@ -28,7 +28,7 @@ Conversations 适用于所有 XMPP 服务器。然而,XMPP 是一种可扩展 到目前为止,这些 XMPP 扩展协议是: -* XEP-0065:SOCKS5 字节流(或 mod_proxy65)。如果双方都在防火墙(NAT)后面,将用于传输文件。 +* XEP-0065:SOCKS5 字节流(或 mod_proxy65)。如果双方都在防火墙或 NAT 后面,将用于传输文件。 * XEP-0163:个人事件协议(头像) * XEP-0191:屏蔽命令可让您将垃圾消息发送者列入黑名单或屏蔽的联系人中,而不会将其从花名册中删除。 * XEP-0198:流管理允许 XMPP 在小规模网络中断和底层 TCP 连接发生变化时继续运行。 diff --git a/src/conversations/res/values-et/strings.xml b/src/conversations/res/values-et/strings.xml index a6b3daec9354f9ae75cdf8d94a67446c6227dd96..da46388b7fa294e1f802c1251859994014a8bf19 100644 --- a/src/conversations/res/values-et/strings.xml +++ b/src/conversations/res/values-et/strings.xml @@ -1,2 +1,20 @@ - \ No newline at end of file + + Vali oma XMPP-teenusepakkuja + Loo uus kasutajakonto + Kas sul juba on XMPP-konto olemas? Kui sa oled varem kasutanud Conversationsit või mõnda muud XMPP-klienti, siis see tõesti võib nii olla. Kui aga mitte, siis võid XMPP-konto kohe luua. +\nVihje: Ka mitmed e-postiteenuse pakkujad pakuvad ka XMPP-teenuseid. + XMPP on teenusepakkujast sõltumatu sõnumivõrk. Sa võid kasutada seda rakendust ükspuha missuguse XMPP-serveriga. +\nAga mugavuse nimel oleme teinud lihtsaks kasutajakonto loomise conversations.im serverisse, mis on Conversationsiga hästi sobituv teenusepakkuja. + Sa oled saanud kutse kasutama serverit %1$s. Järgnevaga aitame sind kasutajakonto loomise kõikides sammudes. +\nKui valid serveriks %1$s, siis jagades oma XMPP-aadressi saad suhelda kõikide teiste XMPP kasutajatega. + Sa oled saanud kutse kasutama serverit %1$s. Kasutajanimi on juba sulle valitud. Järgnevaga aitame sind kasutajakonto loomise kõikides sammudes. +\nJagades oma XMPP-aadressi saad suhelda kõikide teiste XMPP kasutajatega kõikides teistes serverites. + Kasuta conversations.im teenust + Sinu kutse serveri kasutajaks + Vigaselt vormindatud eeltäidetud kutse kood + Klõpsi „Jaga“ nuppu ja saada oma kontaktile kutse kasutamaks serverit %1$s. + Kui teine osapool on lähedal, siis ta saab kutse vastuvõtmiseks skaneerida järgnevat koodi. + Liitu serveriga %1$s ja vestle minuga: %2$s + Jaga kutset… + \ No newline at end of file diff --git a/src/conversations/res/values-ru/strings.xml b/src/conversations/res/values-ru/strings.xml index a62334be82bb18a69532ec396c31788539923397..27f98bae9d8133fde9b9ca8268be0d5cef6bbe5b 100644 --- a/src/conversations/res/values-ru/strings.xml +++ b/src/conversations/res/values-ru/strings.xml @@ -3,8 +3,8 @@ Выберите своего XMPP-провайдера Использовать conversations.im Создать новый аккаунт - У вас есть аккаунт XMPP\? Если вы использовали Conversations или другой XMPP-клиент в прошлом, то скорее всего, он у вас есть. Если у вас нет аккаунта, вы можете создать его прямо сейчас. -\nПодсказка: Некоторые провайдеры электронной почты также регистрируют аккаунты XMPP. + У вас есть аккаунт XMPP? Если вы использовали Conversations или другой XMPP-клиент в прошлом, то скорее всего, он у вас есть. Если у вас нет аккаунта, вы можете создать его прямо сейчас. +\nПодсказка: некоторые провайдеры электронной почты также регистрируют аккаунты XMPP. XMPP - это независимая сеть обмена сообщениями. Это приложение позволяет подключиться к любому XMPP-серверу на ваш выбор. \nЕсли у вас нет сервера, предлагаем вам зарегистрировать аккаунт на conversations.im, сервере, специально предназначенном для работы с Conversations. Вас пригласили на %1$s. Мы проведём вас через процесс создания аккаунта. @@ -14,7 +14,7 @@ Ваше приглашение Неправильный формат кода Нажмите кнопку «Поделиться», чтобы отправить вашему контакту приглашение в %1$s. - Если ваш контакт находится поблизости, он также может отсканировать приведенный ниже код, чтобы принять ваше приглашение. + Если ваш контакт находится поблизости, он также может отсканировать приведённый ниже код, чтобы принять ваше приглашение. Присоединяйтесь к %1$s и пообщайтесь со мной: %2$s Поделиться приглашением с… \ No newline at end of file diff --git a/src/conversations/res/values-sr/strings.xml b/src/conversations/res/values-sr/strings.xml index 3bbef8725fd0c659f4d3d9056e2d2e46f5d9acfb..f6ad6f27c894d2982308e3440b0f80fb25768734 100644 --- a/src/conversations/res/values-sr/strings.xml +++ b/src/conversations/res/values-sr/strings.xml @@ -1,10 +1,20 @@ - Одаберите вашег ИксМПП провајдера - Користи conversations.im + Одабери свог XMPP провајдера + Одабери conversations.im Направи нови налог - Да ли већ имате ИксМПП налог? Извесно је да га имате ако користите неки ИксМПП клијент или сте раније користили Конверзацију. Ако немате, сада можете направити нови ИксМПП налог.\nСавет: неки поштански провајдери такође омогућавају и ИксМПП налоге. - ИксМПП је мрежа брзих порука, независна од провајдера. Овај клијент можете користити уз било који сервер по вашем избору. -\nДа бисмо вам олакшали, омогућили смо креирање налога на conversations.im; провајдеру специјално прилаг.ођеном за коришћење уз Конверзацију - Ваша серверска позивница + Да ли већ имаш XMPP налог? Могуће је да имаш ако већ користиш неки други XMPP клијент или ако си раније користио/ла Conversations. Ако немаш, можеш сада да направиш нови XMPP налог. +\nУспут: поједини имејл провајдери пружају и XMPP налоге. + XMPP је мрежа за инстант поруке, независна од провајдера. Ову апликацију можеш да користиш уз било који XMPP сервер по свом избору. +\nРади погодности, омогућили смо лако креирање налога на conversations.im - провајдер посебно прилагођен за коришћење уз Conversations. + Твоја позивница на сервер + Позван/а си на %1$s. Спровешћемо те кроз поступак прављења налога. +\nАко одабереш %1$s као провајдера можеш да комуницираш са корисницима других провајдера тако што им проследиш своју пуну XMPP адресу. + Неисправно форматиран провизиони кôд + Притисни дугме за дељење како би свом контакту послао/ла позивницу на %1$s. + Ако је твој контакт у близини, може да скенира кôд у наставку како би прихватио твоју позивницу. + Пријави се на %1$s и разговарај са мном: %2$s + Подели позивницу са… + Позван/а си на %1$s. Корисничко име је већ одабрано за тебе. Спровешћемо те кроз поступак прављења налога. +\nМожеш да комуницираш са корисницима других провајдера тако што им проследиш своју пуну XMPP адресу. \ No newline at end of file diff --git a/src/free/java/eu/siacs/conversations/services/EmojiInitializationService.java b/src/free/java/eu/siacs/conversations/services/EmojiInitializationService.java index 2618d380964a0b075dc17c7c99fcf20fb30fb5b3..aae5e64094970c7aace6fa99b6a049c3c7b0ba33 100644 --- a/src/free/java/eu/siacs/conversations/services/EmojiInitializationService.java +++ b/src/free/java/eu/siacs/conversations/services/EmojiInitializationService.java @@ -1,14 +1,31 @@ package eu.siacs.conversations.services; import android.content.Context; +import android.util.Log; +import androidx.annotation.Nullable; import androidx.emoji2.bundled.BundledEmojiCompatConfig; import androidx.emoji2.text.EmojiCompat; +import eu.siacs.conversations.Config; + public class EmojiInitializationService { public static void execute(final Context context) { - EmojiCompat.init(new BundledEmojiCompatConfig(context).setReplaceAll(true)); - } + EmojiCompat.init(new BundledEmojiCompatConfig(context).setReplaceAll(true)) + .registerInitCallback( + new EmojiCompat.InitCallback() { + @Override + public void onInitialized() { + Log.d(Config.LOGTAG, "initialized EmojiCompat"); + super.onInitialized(); + } + @Override + public void onFailed(@Nullable Throwable throwable) { + Log.e(Config.LOGTAG, "failed to initialize EmojiCompat", throwable); + super.onFailed(throwable); + } + }); + } } diff --git a/src/main/java/eu/siacs/conversations/Conversations.java b/src/main/java/eu/siacs/conversations/Conversations.java index cc46f9c640e211a4486734c448583c333a5f7526..d790b65670a76d3e4a1a47fea25c84eed20af6c8 100644 --- a/src/main/java/eu/siacs/conversations/Conversations.java +++ b/src/main/java/eu/siacs/conversations/Conversations.java @@ -11,6 +11,7 @@ import androidx.appcompat.app.AppCompatDelegate; import com.google.android.material.color.DynamicColors; import com.google.android.material.color.DynamicColorsOptions; +import eu.siacs.conversations.services.EmojiInitializationService; import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.utils.ThemeHelper; @@ -27,6 +28,7 @@ public class Conversations extends Application { public void onCreate() { super.onCreate(); CONTEXT = this.getApplicationContext(); + EmojiInitializationService.execute(getApplicationContext()); ExceptionHelper.init(getApplicationContext()); applyThemeSettings(); } diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index 7f9bf8593f072064881b567008de07a8e1fa1b78..11523dae5777818a509b2cd32e9e9ef92e3241d2 100644 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -61,11 +61,11 @@ public class Bookmark extends Element implements ListItem { return bookmarks; } - public static Map parseFromPubsub(Element pubsub, Account account) { - if (pubsub == null) { + public static Map parseFromPubSub(final Element pubSub, final Account account) { + if (pubSub == null) { return Collections.emptyMap(); } - final Element items = pubsub.findChild("items"); + final Element items = pubSub.findChild("items"); if (items != null && Namespace.BOOKMARKS2.equals(items.getAttribute("node"))) { final Map bookmarks = new HashMap<>(); for(Element item : items.getChildren()) { @@ -99,6 +99,7 @@ public class Bookmark extends Element implements ListItem { } final Bookmark bookmark = new Bookmark(account); bookmark.jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("id")); + // TODO verify that we only use bare jids and ignore full jids if (bookmark.jid == null) { return null; } @@ -128,14 +129,14 @@ public class Bookmark extends Element implements ListItem { } public void setGroups(List groups) { - final List children = new ArrayList<>(getChildren()); + final List children = ImmutableList.copyOf(getChildren()); for (final Element el : children) { if (el.getName().equals("group")) { removeChild(el); } } - final List extChildren = new ArrayList<>(extensions.getChildren()); + final List extChildren = ImmutableList.copyOf(extensions.getChildren()); for (final Element el : extChildren) { if (el.getName().equals("group")) { extensions.removeChild(el); diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 8cb4f62c58398a536082a112ac155e65b80a16e2..dc5ef217d186c3c29579ef16f9e1d88f18bc5456 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -519,6 +519,17 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return null; } + public Message findMessageWithUuidOrRemoteId(final String id) { + synchronized (this.messages) { + for (final Message message : this.messages) { + if (id.equals(message.getUuid()) || id.equals(message.getRemoteMsgId())) { + return message; + } + } + } + return null; + } + public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart) { synchronized (this.messages) { for (int i = this.messages.size() - 1; i >= 0; --i) { @@ -602,7 +613,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl if (reactor != null && message.getCounterpart() == null) continue; if (reactor != null && !(message.getCounterpart().equals(reactor) || message.getCounterpart().asBareJid().equals(reactor))) continue; - final Element r = message.getReactions(); + final Element r = message.getReactionsEl(); if (r != null && r.getAttribute("id") != null && id.equals(r.getAttribute("id"))) { return message; } @@ -628,7 +639,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public Set findReactionsTo(String id, Jid reactor) { Set reactionEmoji = new HashSet<>(); Message reactM = findMessageReactingTo(id, reactor); - Element reactions = reactM == null ? null : reactM.getReactions(); + Element reactions = reactM == null ? null : reactM.getReactionsEl(); if (reactions != null) { for (Element el : reactions.getChildren()) { if (el.getName().equals("reaction") && el.getNamespace().equals("urn:xmpp:reactions:0")) { @@ -696,7 +707,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } else if (getLockThread() && mthread != null) { Element reply = m.getReply(); if (reply != null && reply.getAttribute("id") != null) extraIds.add(reply.getAttribute("id")); - Element reactions = m.getReactions(); + Element reactions = m.getReactionsEl(); if (reactions != null && reactions.getAttribute("id") != null) extraIds.add(reactions.getAttribute("id")); } } diff --git a/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java b/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java index 00d4086ae244e6cb76cc33b68f01e7e53bdab210..c0b327575dc2cd15cce484a98a428663adaa5407 100644 --- a/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java +++ b/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java @@ -31,6 +31,7 @@ package eu.siacs.conversations.entities; import android.database.Cursor; +import java.util.Collection; import java.util.Set; import eu.siacs.conversations.ui.adapter.MessageAdapter; @@ -43,8 +44,8 @@ public class IndividualMessage extends Message { super(conversation); } - private IndividualMessage(Conversational conversation, String uuid, String conversationUUid, Jid counterpart, Jid trueCounterpart, String body, long timeSent, int encryption, int status, int type, boolean carbon, String remoteMsgId, String relativeFilePath, String serverMsgId, String fingerprint, boolean read, String edited, boolean oob, String errorMessage, Set readByMarkers, boolean markable, boolean deleted, String bodyLanguage) { - super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, edited, oob, errorMessage, readByMarkers, markable, deleted, bodyLanguage, timeSent, null, null, null); + private IndividualMessage(Conversational conversation, String uuid, String conversationUUid, Jid counterpart, Jid trueCounterpart, String body, long timeSent, int encryption, int status, int type, boolean carbon, String remoteMsgId, String relativeFilePath, String serverMsgId, String fingerprint, boolean read, String edited, boolean oob, String errorMessage, Set readByMarkers, boolean markable, boolean deleted, String bodyLanguage, String occupantId, Collection reactions) { + super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, edited, oob, errorMessage, readByMarkers, markable, deleted, bodyLanguage, occupantId, reactions, timeSent, null, null, null); } @Override @@ -73,7 +74,7 @@ public class IndividualMessage extends Message { public static Message fromCursor(Cursor cursor, Conversational conversation) { Jid jid; try { - String value = cursor.getString(cursor.getColumnIndex(COUNTERPART)); + String value = cursor.getString(cursor.getColumnIndexOrThrow(COUNTERPART)); if (value != null) { jid = Jid.of(value); } else { @@ -86,7 +87,7 @@ public class IndividualMessage extends Message { } Jid trueCounterpart; try { - String value = cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART)); + String value = cursor.getString(cursor.getColumnIndexOrThrow(TRUE_COUNTERPART)); if (value != null) { trueCounterpart = Jid.of(value); } else { @@ -96,28 +97,30 @@ public class IndividualMessage extends Message { trueCounterpart = null; } return new IndividualMessage(conversation, - cursor.getString(cursor.getColumnIndex(UUID)), - cursor.getString(cursor.getColumnIndex(CONVERSATION)), + cursor.getString(cursor.getColumnIndexOrThrow(UUID)), + cursor.getString(cursor.getColumnIndexOrThrow(CONVERSATION)), jid, trueCounterpart, - cursor.getString(cursor.getColumnIndex(BODY)), - cursor.getLong(cursor.getColumnIndex(TIME_SENT)), - cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), - cursor.getInt(cursor.getColumnIndex(STATUS)), - cursor.getInt(cursor.getColumnIndex(TYPE)), - cursor.getInt(cursor.getColumnIndex(CARBON)) > 0, - cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)), - cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), - cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)), - cursor.getString(cursor.getColumnIndex(FINGERPRINT)), - cursor.getInt(cursor.getColumnIndex(READ)) > 0, - cursor.getString(cursor.getColumnIndex(EDITED)), - cursor.getInt(cursor.getColumnIndex(OOB)) > 0, - cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)), - ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))), - cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0, - cursor.getInt(cursor.getColumnIndex(DELETED)) > 0, - cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)) + cursor.getString(cursor.getColumnIndexOrThrow(BODY)), + cursor.getLong(cursor.getColumnIndexOrThrow(TIME_SENT)), + cursor.getInt(cursor.getColumnIndexOrThrow(ENCRYPTION)), + cursor.getInt(cursor.getColumnIndexOrThrow(STATUS)), + cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)), + cursor.getInt(cursor.getColumnIndexOrThrow(CARBON)) > 0, + cursor.getString(cursor.getColumnIndexOrThrow(REMOTE_MSG_ID)), + cursor.getString(cursor.getColumnIndexOrThrow(RELATIVE_FILE_PATH)), + cursor.getString(cursor.getColumnIndexOrThrow(SERVER_MSG_ID)), + cursor.getString(cursor.getColumnIndexOrThrow(FINGERPRINT)), + cursor.getInt(cursor.getColumnIndexOrThrow(READ)) > 0, + cursor.getString(cursor.getColumnIndexOrThrow(EDITED)), + cursor.getInt(cursor.getColumnIndexOrThrow(OOB)) > 0, + cursor.getString(cursor.getColumnIndexOrThrow(ERROR_MESSAGE)), + ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndexOrThrow(READ_BY_MARKERS))), + cursor.getInt(cursor.getColumnIndexOrThrow(MARKABLE)) > 0, + cursor.getInt(cursor.getColumnIndexOrThrow(DELETED)) > 0, + cursor.getString(cursor.getColumnIndexOrThrow(BODY_LANGUAGE)), + cursor.getString(cursor.getColumnIndexOrThrow(OCCUPANT_ID)), + Reaction.fromString(cursor.getString(cursor.getColumnIndexOrThrow(REACTIONS))) ); } } diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 9ef0a55e1f74b314d3cbeaeeac735421eadd89d3..d39b5f45ad29eb4781d03563befe99e96bc8494d 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -28,20 +28,22 @@ import com.google.common.primitives.Longs; import org.json.JSONException; -import java.lang.ref.WeakReference; import java.io.IOException; +import java.lang.ref.WeakReference; import java.net.URI; import java.net.URISyntaxException; -import java.time.Duration; import java.security.NoSuchAlgorithmException; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.stream.Collectors; import io.ipfs.cid.Cid; @@ -119,6 +121,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public static final String READ_BY_MARKERS = "readByMarkers"; public static final String MARKABLE = "markable"; public static final String DELETED = "deleted"; + public static final String OCCUPANT_ID = "occupantId"; + public static final String REACTIONS = "reactions"; public static final String ME_COMMAND = "/me "; public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled"; @@ -156,6 +160,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable private String errorMessage = null; private Set readByMarkers = new CopyOnWriteArraySet<>(); protected Message mInReplyTo = null; + private Collection reactions = Collections.emptyList(); private Boolean isGeoUri = null; private Boolean isEmojisOnly = null; @@ -195,6 +200,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable false, false, null, + null, + Collections.emptyList(), System.currentTimeMillis(), null, null, @@ -224,6 +231,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable false, false, null, + null, + Collections.emptyList(), System.currentTimeMillis(), null, null, @@ -236,7 +245,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable final String remoteMsgId, final String relativeFilePath, final String serverMsgId, final String fingerprint, final boolean read, final String edited, final boolean oob, final String errorMessage, final Set readByMarkers, - final boolean markable, final boolean deleted, final String bodyLanguage, final long timeReceived, final String subject, final String fileParams, final List payloads) { + final boolean markable, final boolean deleted, final String bodyLanguage, final String occupantId, final Collection reactions, final long timeReceived, final String subject, final String fileParams, final List payloads) { this.conversation = conversation; this.uuid = uuid; this.conversationUuid = conversationUUid; @@ -260,6 +269,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable this.markable = markable; this.deleted = deleted; this.bodyLanguage = bodyLanguage; + this.occupantId = occupantId; + this.reactions = reactions; this.timeReceived = timeReceived; this.subject = subject; if (payloads != null) this.payloads = payloads; @@ -305,12 +316,15 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0, cursor.getInt(cursor.getColumnIndex(DELETED)) > 0, cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)), + cursor.getString(cursor.getColumnIndexOrThrow(OCCUPANT_ID)), + Reaction.fromString(cursor.getString(cursor.getColumnIndexOrThrow(REACTIONS))), cursor.getLong(cursor.getColumnIndex(cursor.isNull(cursor.getColumnIndex("timeReceived")) ? TIME_SENT : "timeReceived")), cursor.getString(cursor.getColumnIndex("subject")), cursor.getString(cursor.getColumnIndex("fileParams")), payloads ); - m.setOccupantId(cursor.getString(cursor.getColumnIndex("occupant_id"))); + final var legacyOccupant = cursor.getString(cursor.getColumnIndex("occupant_id")); + if (legacyOccupant != null) m.setOccupantId(legacyOccupant); return m; } @@ -361,7 +375,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable @Override public ContentValues getContentValues() { - ContentValues values = new ContentValues(); + final var values = new ContentValues(); values.put(UUID, uuid); values.put(CONVERSATION, conversationUuid); if (counterpart == null) { @@ -396,6 +410,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable values.put(MARKABLE, markable ? 1 : 0); values.put(DELETED, deleted ? 1 : 0); values.put(BODY_LANGUAGE, bodyLanguage); + values.put(OCCUPANT_ID, occupantId); + values.put(REACTIONS, Reaction.toString(this.reactions)); return values; } @@ -415,7 +431,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public void clearReplyReact() { - this.payloads.remove(getReactions()); + this.payloads.remove(getReactionsEl()); this.payloads.remove(getReply()); clearFallbacks("urn:xmpp:reply:0", "urn:xmpp:reactions:0"); } @@ -446,7 +462,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public Message react(String emoji) { final var m = reply(); - if (getReactions() == null) { + if (getReactionsEl() == null) { m.updateReaction(this, emoji); } else if (mInReplyTo != null) { // Try to send react-to-reaction to parent @@ -475,25 +491,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable addPayload(reactions); } - public void setReactions(Element reactions) { - if (this.payloads != null) { - this.payloads.remove(getReactions()); - } - addPayload(reactions); - } - - public Element getReactions() { - if (this.payloads == null) return null; - - for (Element el : this.payloads) { - if (el.getName().equals("reactions") && el.getNamespace().equals("urn:xmpp:reactions:0")) { - return el; - } - } - - return null; - } - public Element getReply() { if (this.payloads == null) return null; @@ -1017,7 +1014,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public boolean mergeable(final Message message) { - return false; // Merrgine messages messes up reply, so disable for now + return false; // Merging messages messes up reply, so disable for now } private static boolean isStatusMergeable(int a, int b) { @@ -1062,6 +1059,41 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable return oob || getFileParams().url != null; } + public Collection getReactions() { + return this.reactions; + } + + public void setReactions(Element reactions) { + if (this.payloads != null) { + this.payloads.remove(getReactionsEl()); + } + addPayload(reactions); + } + + public Element getReactionsEl() { + if (this.payloads == null) return null; + + for (Element el : this.payloads) { + if (el.getName().equals("reactions") && el.getNamespace().equals("urn:xmpp:reactions:0")) { + return el; + } + } + + return null; + } + + public boolean isReactionsEmpty() { + return this.reactions.isEmpty(); + } + + public Reaction.Aggregated getAggregatedReactions() { + return Reaction.aggregated(this.reactions); + } + + public void setReactions(final Collection reactions) { + this.reactions = reactions; + } + public static class MergeSeparator { } diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 324c71c71e8f5739880620f8f8565c16a9a6b841..19d0658320d13e061629d32365a34f37a1114139 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -22,6 +22,7 @@ import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.utils.JidHelper; import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.forms.Data; @@ -240,6 +241,11 @@ public class MucOptions { return getFeatures().contains("http://jabber.org/protocol/muc#stable_id"); } + public boolean occupantId() { + final var features = getFeatures(); + return features.contains(Namespace.OCCUPANT_ID); + } + public User deleteUser(Jid jid) { User user = findUserByFullJid(jid); if (user != null) { @@ -1120,5 +1126,9 @@ public class MucOptions { public String getAvatarName() { return getConversation().getName().toString(); } + + public void setOccupantId(final String occupantId) { + this.occupantId = occupantId; + } } } diff --git a/src/main/java/eu/siacs/conversations/entities/Reaction.java b/src/main/java/eu/siacs/conversations/entities/Reaction.java new file mode 100644 index 0000000000000000000000000000000000000000..eff7be74b096c1acc7c6020a992d452e025009b4 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/Reaction.java @@ -0,0 +1,178 @@ +package eu.siacs.conversations.entities; + +import androidx.annotation.NonNull; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Strings; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimaps; +import com.google.common.collect.Ordering; +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +import eu.siacs.conversations.xmpp.Jid; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class Reaction { + + public static final List SUGGESTIONS = + Arrays.asList( + "\u2764\uFE0F", + "\uD83D\uDC4D", + "\uD83D\uDC4E", + "\uD83D\uDE02", + "\uD83D\uDE2E", + "\uD83D\uDE22"); + + private static final Gson GSON; + + static { + GSON = new GsonBuilder().registerTypeAdapter(Jid.class, new JidTypeAdapter()).create(); + } + + public final String reaction; + public final boolean received; + public final Jid from; + public final Jid trueJid; + public final String occupantId; + + public Reaction( + final String reaction, + boolean received, + final Jid from, + final Jid trueJid, + final String occupantId) { + this.reaction = reaction; + this.received = received; + this.from = from; + this.trueJid = trueJid; + this.occupantId = occupantId; + } + + public static String toString(final Collection reactions) { + return (reactions == null || reactions.isEmpty()) ? null : GSON.toJson(reactions); + } + + public static Collection fromString(final String asString) { + if (Strings.isNullOrEmpty(asString)) { + return Collections.emptyList(); + } + try { + return GSON.fromJson(asString, new TypeToken>() {}.getType()); + } catch (final JsonSyntaxException e) { + return Collections.emptyList(); + } + } + + public static Collection withOccupantId( + final Collection existing, + final Collection reactions, + final boolean received, + final Jid from, + final Jid trueJid, + final String occupantId) { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + builder.addAll(Collections2.filter(existing, e -> !occupantId.equals(e.occupantId))); + builder.addAll( + Collections2.transform( + reactions, r -> new Reaction(r, received, from, trueJid, occupantId))); + return builder.build(); + } + + @NonNull + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("reaction", reaction) + .add("received", received) + .add("from", from) + .add("trueJid", trueJid) + .add("occupantId", occupantId) + .toString(); + } + + public static Collection withFrom( + final Collection existing, + final Collection reactions, + final boolean received, + final Jid from) { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + builder.addAll( + Collections2.filter(existing, e -> !from.asBareJid().equals(e.from.asBareJid()))); + builder.addAll( + Collections2.transform( + reactions, r -> new Reaction(r, received, from, null, null))); + return builder.build(); + } + + private static class JidTypeAdapter extends TypeAdapter { + @Override + public void write(final JsonWriter out, final Jid value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.toEscapedString()); + } + } + + @Override + public Jid read(final JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } else if (in.peek() == JsonToken.STRING) { + final String value = in.nextString(); + return Jid.ofEscaped(value); + } + throw new IOException("Unexpected token"); + } + } + + public static Aggregated aggregated(final Collection reactions) { + final Map aggregatedReactions = + Maps.transformValues( + Multimaps.index(reactions, r -> r.reaction).asMap(), Collection::size); + final List> sortedList = + Ordering.from( + Comparator.comparingInt( + (Map.Entry o) -> o.getValue())) + .reverse() + .immutableSortedCopy(aggregatedReactions.entrySet()); + return new Aggregated( + sortedList, + ImmutableSet.copyOf( + Collections2.transform( + Collections2.filter(reactions, r -> !r.received), + r -> r.reaction))); + } + + public static final class Aggregated { + + public final List> reactions; + public final Set ourReactions; + + private Aggregated( + final List> reactions, Set ourReactions) { + this.reactions = reactions; + this.ourReactions = ourReactions; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 463bad4e3599188220377bcddf1548a97a46e5f5..259d5decf81bc2b8b38b5fe3bd64e71989a83e17 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.generator; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Collection; import java.util.Date; import java.util.Locale; import java.util.TimeZone; @@ -23,6 +24,8 @@ import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; +import im.conversations.android.xmpp.model.reactions.Reaction; +import im.conversations.android.xmpp.model.reactions.Reactions; public class MessageGenerator extends AbstractGenerator { private static final String OMEMO_FALLBACK_MESSAGE = "I sent you an OMEMO encrypted message but your client doesn’t seem to support that. Find more information on https://conversations.im/omemo"; @@ -197,6 +200,21 @@ public class MessageGenerator extends AbstractGenerator { return packet; } + public im.conversations.android.xmpp.model.stanza.Message reaction(final Conversational conversation, final String reactingTo, final Collection ourReactions) { + final boolean groupChat = conversation.getMode() == Conversational.MODE_MULTI; + final Jid to = conversation.getJid().asBareJid(); + final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(groupChat ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); + packet.setTo(to); + final var reactions = packet.addExtension(new Reactions()); + reactions.setId(reactingTo); + for(final String ourReaction : ourReactions) { + reactions.addExtension(new Reaction(ourReaction)); + } + packet.addChild("store", "urn:xmpp:hints"); + return packet; + } + public im.conversations.android.xmpp.model.stanza.Message conferenceSubject(Conversation conversation, String subject) { im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT); diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index 63fd084f4cb1aafc879941345b76da4c9bb87dd0..3530097886f485c99a553082de826917abe8a6e0 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.parser; - import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -139,7 +138,7 @@ public abstract class AbstractParser { return parseItem(conference,item,null,null,null,new Element("hats", "urn:xmpp:hats:0")); } - public static MucOptions.User parseItem(Conversation conference, Element item, Jid fullJid, final Element occupantId, final String nicknameIn, final Element hatsEl) { + public static MucOptions.User parseItem(final Conversation conference, Element item, Jid fullJid, final Element occupantId, final String nicknameIn, final Element hatsEl) { final String local = conference.getJid().getLocal(); final String domain = conference.getJid().getDomain().toEscapedString(); String affiliation = item.getAttribute("affiliation"); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 27c8f95b710ded82c99c5830796f5bc2f8c0231c..825d7a61c87c8c20419e5bce2fab7386f932a4bd 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -42,6 +42,7 @@ import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.entities.Reaction; import eu.siacs.conversations.entities.ReadByMarker; import eu.siacs.conversations.entities.ReceiptRequest; import eu.siacs.conversations.entities.RtpSessionStatus; @@ -65,6 +66,8 @@ import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.carbons.Received; import im.conversations.android.xmpp.model.carbons.Sent; import im.conversations.android.xmpp.model.forward.Forwarded; +import im.conversations.android.xmpp.model.occupant.OccupantId; +import im.conversations.android.xmpp.model.reactions.Reactions; public class MessageParser extends AbstractParser implements Consumer { @@ -445,7 +448,8 @@ public class MessageParser extends AbstractParser implements Consumer f; f = getForwardedMessagePacket(original, Received.class); f = f == null ? getForwardedMessagePacket(original, Sent.class) : f; @@ -462,6 +466,7 @@ public class MessageParser extends AbstractParser implements Consumer codes = getStatusCodes(x); if (type == null) { @@ -79,8 +80,15 @@ public class PresenceParser extends AbstractParser implements Consumer= 52) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.OCCUPANT_ID + " TEXT"); + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.REACTIONS + " TEXT"); + } } private void canonicalizeJids(SQLiteDatabase db) { @@ -1397,28 +1403,29 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public boolean updateAccount(Account account) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {account.getUuid()}; - final int rows = db.update(Account.TABLENAME, account.getContentValues(), Account.UUID + "=?", args); + final var db = this.getWritableDatabase(); + final String[] args = {account.getUuid()}; + final int rows = + db.update(Account.TABLENAME, account.getContentValues(), Account.UUID + "=?", args); return rows == 1; } - public boolean deleteAccount(Account account) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {account.getUuid()}; + public boolean deleteAccount(final Account account) { + final var db = this.getWritableDatabase(); + final String[] args = {account.getUuid()}; final int rows = db.delete(Account.TABLENAME, Account.UUID + "=?", args); return rows == 1; } - public boolean updateMessage(Message message, boolean includeBody) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {message.getUuid()}; - ContentValues contentValues = message.getContentValues(); + public boolean updateMessage(final Message message, final boolean includeBody) { + final var db = this.getWritableDatabase(); + final String[] args = {message.getUuid()}; + final var contentValues = message.getContentValues(); contentValues.remove(Message.UUID); if (!includeBody) { contentValues.remove(Message.BODY); } - return db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + "=?", args) == 1 && + return db.update(Message.TABLENAME, contentValues, Message.UUID + "=?", args) == 1 && db.update("cheogram." + Message.TABLENAME, message.getCheogramContentValues(), Message.UUID + "=?", args) == 1; } diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index 5b23d1533fa8440bebc2a332f4f3ad779d6ab796..853901c0dbc83a9a1dd3fdc84c45e20882248bd3 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -16,6 +16,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; @@ -30,6 +31,7 @@ import eu.siacs.conversations.xmpp.jingle.Media; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -37,17 +39,28 @@ import java.util.concurrent.atomic.AtomicBoolean; public class CallIntegration extends Connection { /** - * OnePlus 6 (Android 8.1-11) Device is buggy and always starts the OS call screen even though - * we want to be self managed - * - *

Samsung Galaxy Tab A claims to have FEATURE_CONNECTION_SERVICE but then throws + * Samsung Galaxy Tab A claims to have FEATURE_CONNECTION_SERVICE but then throws * SecurityException when invoking placeCall(). Both Stock and LineageOS have this problem. * *

Lenovo Yoga Smart Tab YT-X705F claims to have FEATURE_CONNECTION_SERVICE but throws * SecurityException */ private static final List BROKEN_DEVICE_MODELS = - Arrays.asList("OnePlus6", "gtaxlwifi", "YT-X705F"); + Arrays.asList("gtaxlwifi", "a5y17lte", "YT-X705F"); + + /** + * all Realme devices at least up to and including Android 11 are broken + * + *

we are relatively sure that old Oppo devices are broken too. We get reports of 'number not + * sent' from Oppo R15x (Android 10) + * + *

OnePlus 6 (Android 8.1-11) Device is buggy and always starts the OS call screen even + * though we want to be self managed + * + *

a bunch of OnePlus devices are broken in other ways + */ + private static final List BROKEN_MANUFACTURES_UP_TO_11 = + Arrays.asList("realme", "oppo", "oneplus"); public static final int DEFAULT_TONE_VOLUME = 60; private static final int DEFAULT_MEDIA_PLAYER_VOLUME = 90; @@ -534,25 +547,18 @@ public class CallIntegration extends Connection { } private static boolean isDeviceModelSupported() { + final var manufacturer = Strings.nullToEmpty(Build.MANUFACTURER).toLowerCase(Locale.ROOT); if (BROKEN_DEVICE_MODELS.contains(Build.DEVICE)) { return false; } - // all Realme devices at least up to and including Android 11 are broken - if ("realme".equalsIgnoreCase(Build.MANUFACTURER) - && Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { - return false; - } - // we are relatively sure that old Oppo devices are broken too. We get reports of 'number - // not sent' from Oppo R15x (Android 10) - if ("OPPO".equalsIgnoreCase(Build.MANUFACTURER) + if (BROKEN_MANUFACTURES_UP_TO_11.contains(manufacturer) && Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { return false; } // we only know of one Umidigi device (BISON_GT2_5G) that doesn't work (audio is not being // routed properly) However with those devices being extremely rare it's impossible to gauge // how many might be effected and no Naomi Wu around to clarify with the company directly - if ("umidigi".equalsIgnoreCase(Build.MANUFACTURER) - && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) { + if ("umidigi".equals(manufacturer) && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) { return false; } return true; diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index ef5241194974936b01661516b21b1627e62ea60f..e4e44a4a13b599696faf081a3901f71fdf95a3bf 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -136,6 +136,7 @@ import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.OnRenameListener; import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.entities.PresenceTemplate; +import eu.siacs.conversations.entities.Reaction; import eu.siacs.conversations.entities.Roster; import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.generator.AbstractGenerator; @@ -181,7 +182,6 @@ import eu.siacs.conversations.xml.LocalizedContent; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnBindListener; import eu.siacs.conversations.xmpp.OnContactStatusChanged; import eu.siacs.conversations.xmpp.OnGatewayResult; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; @@ -1513,7 +1513,11 @@ public class XmppConnectionService extends Service { } final PowerManager powerManager = getSystemService(PowerManager.class); - this.wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Conversations:Service"); + if (powerManager != null) { + this.wakeLock = + powerManager.newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, "Conversations:Service"); + } toggleForegroundService(); updateUnreadCountBadge(); @@ -2258,7 +2262,7 @@ public class XmppConnectionService extends Service { sendIqPacket(account, retrieve, (response) -> { if (response.getType() == Iq.Type.RESULT) { final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB); - final Map bookmarks = Bookmark.parseFromPubsub(pubsub, account); + final Map bookmarks = Bookmark.parseFromPubSub(pubsub, account); processBookmarksInitial(account, bookmarks, true); } }); @@ -5258,6 +5262,53 @@ public class XmppConnectionService extends Service { PublishOptions.persistentWhitelistAccessMaxItems()); } + public boolean sendReactions(final Message message, final Collection reactions) { + if (message.getConversation() instanceof Conversation conversation) { + final String reactToId; + final Collection combinedReactions; + if (conversation.getMode() == Conversational.MODE_MULTI) { + final var self = conversation.getMucOptions().getSelf(); + final String occupantId = self.getOccupantId(); + if (Strings.isNullOrEmpty(occupantId)) { + Log.d(Config.LOGTAG, "occupant id not found for reaction in MUC"); + return false; + } + reactToId = message.getServerMsgId(); + combinedReactions = + Reaction.withOccupantId( + message.getReactions(), + reactions, + false, + self.getFullJid(), + conversation.getAccount().getJid(), + occupantId); + } else { + if (message.isCarbon() || message.getStatus() == Message.STATUS_RECEIVED) { + reactToId = message.getRemoteMsgId(); + } else { + reactToId = message.getUuid(); + } + combinedReactions = + Reaction.withFrom( + message.getReactions(), + reactions, + false, + conversation.getAccount().getJid()); + } + if (Strings.isNullOrEmpty(reactToId)) { + return false; + } + final var reactionMessage = + mMessageGenerator.reaction(conversation, reactToId, reactions); + sendMessagePacket(conversation.getAccount(), reactionMessage); + message.setReactions(combinedReactions); + updateMessage(message, false); + return true; + } else { + return false; + } + } + public MemorizingTrustManager getMemorizingTrustManager() { return this.mMemorizingTrustManager; } diff --git a/src/main/java/eu/siacs/conversations/ui/BindingAdapters.java b/src/main/java/eu/siacs/conversations/ui/BindingAdapters.java new file mode 100644 index 0000000000000000000000000000000000000000..6f6b4e8bebba341dbc61ca9c81ec7cb747167b95 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/BindingAdapters.java @@ -0,0 +1,117 @@ +package eu.siacs.conversations.ui; + +import android.view.View; + +import com.google.android.material.chip.Chip; +import com.google.android.material.chip.ChipGroup; +import com.google.android.material.color.MaterialColors; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableSet; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Reaction; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Consumer; + +public class BindingAdapters { + + public static void setReactionsOnReceived( + final ChipGroup chipGroup, + final Reaction.Aggregated reactions, + final Consumer> onModifiedReactions, + final Runnable addReaction) { + setReactions(chipGroup, reactions, true, onModifiedReactions, addReaction); + } + + public static void setReactionsOnSent( + final ChipGroup chipGroup, + final Reaction.Aggregated reactions, + final Consumer> onModifiedReactions) { + setReactions(chipGroup, reactions, false, onModifiedReactions, null); + } + + private static void setReactions( + final ChipGroup chipGroup, + final Reaction.Aggregated aggregated, + final boolean onReceived, + final Consumer> onModifiedReactions, + final Runnable addReaction) { + final var context = chipGroup.getContext(); + final List> reactions = aggregated.reactions; + if (reactions == null || reactions.isEmpty()) { + chipGroup.setVisibility(View.GONE); + } else { + chipGroup.removeAllViews(); + chipGroup.setVisibility(View.VISIBLE); + for (final Map.Entry reaction : reactions) { + final var emoji = reaction.getKey(); + final var count = reaction.getValue(); + final Chip chip = new Chip(chipGroup.getContext()); + chip.setEnsureMinTouchTargetSize(false); + chip.setChipStartPadding(0.0f); + chip.setChipEndPadding(0.0f); + if (count == 1) { + chip.setText(emoji); + } else { + chip.setText(String.format(Locale.ENGLISH, "%s %d", emoji, count)); + } + final boolean oneOfOurs = aggregated.ourReactions.contains(emoji); + // received = surface; sent = surface high matches bubbles + if (oneOfOurs) { + chip.setChipBackgroundColor( + MaterialColors.getColorStateListOrNull( + context, + com.google.android.material.R.attr + .colorSurfaceContainerHighest)); + } else { + chip.setChipBackgroundColor( + MaterialColors.getColorStateListOrNull( + context, + com.google.android.material.R.attr.colorSurfaceContainerLow)); + } + chip.setOnClickListener( + v -> { + if (oneOfOurs) { + onModifiedReactions.accept( + ImmutableSet.copyOf( + Collections2.filter( + aggregated.ourReactions, + r -> !r.equals(emoji)))); + } else { + onModifiedReactions.accept( + new ImmutableSet.Builder() + .addAll(aggregated.ourReactions) + .add(emoji) + .build()); + } + }); + chipGroup.addView(chip); + } + if (onReceived) { + final Chip chip = new Chip(chipGroup.getContext()); + chip.setChipIconResource(R.drawable.ic_add_reaction_24dp); + chip.setChipStrokeColor( + MaterialColors.getColorStateListOrNull( + chipGroup.getContext(), + com.google.android.material.R.attr.colorTertiary)); + chip.setChipBackgroundColor( + MaterialColors.getColorStateListOrNull( + chipGroup.getContext(), + com.google.android.material.R.attr.colorTertiaryContainer)); + chip.setChipIconTint( + MaterialColors.getColorStateListOrNull( + chipGroup.getContext(), + com.google.android.material.R.attr.colorOnTertiaryContainer)); + chip.setEnsureMinTouchTargetSize(false); + chip.setTextEndPadding(0.0f); + chip.setTextStartPadding(0.0f); + chip.setOnClickListener(v -> addReaction.run()); + chipGroup.addView(chip); + } + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 6525fbf5d32a8d27e31efd75c2ca3032588f2242..863d7e8da7087f61084001d3ba31dd4b0df27e75 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -704,7 +704,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers final ImmutableList.Builder viewIdBuilder = new ImmutableList.Builder<>(); for (final ListItem.Tag tag : tagList) { final String name = tag.getName(); - final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, binding.tags, false); + final TextView tv = (TextView) inflater.inflate(R.layout.item_tag, binding.tags, false); tv.setText(name); tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(this,XEP0392Helper.rgbFromNick(name)))); final int id = ViewCompat.generateViewId(); diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 8b26118bd429eb46ea06f7359426ce3c07dff739..47d23173b11773aca1efd4555239e56e24ac7c35 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -35,7 +35,6 @@ import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.core.view.ViewCompat; import androidx.databinding.DataBindingUtil; @@ -610,7 +609,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp final ImmutableList.Builder viewIdBuilder = new ImmutableList.Builder<>(); for (final ListItem.Tag tag : tagList) { final String name = tag.getName(); - final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, binding.tags, false); + final TextView tv = (TextView) inflater.inflate(R.layout.item_tag, binding.tags, false); tv.setText(name); tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(this,XEP0392Helper.rgbFromNick(name)))); final int id = ViewCompat.generateViewId(); @@ -622,7 +621,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp final TextView tv = (TextView) inflater.inflate( - R.layout.list_item_tag, binding.tags, false); + R.layout.item_tag, binding.tags, false); tv.setText(R.string.blocked); tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(tv.getContext(), ContextCompat.getColor(tv.getContext(),R.color.gray_800)))); final int id = ViewCompat.generateViewId(); @@ -635,7 +634,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp final TextView tv = (TextView) inflater.inflate( - R.layout.list_item_tag, binding.tags, false); + R.layout.item_tag, binding.tags, false); UIHelper.setStatus(tv, status); final int id = ViewCompat.generateViewId(); tv.setId(id); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 8b6c5ef90c0744194be8fc601c4c853df79556d1..63aebf4b9d0ae5b102198cb182f545333b964e79 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1803,6 +1803,7 @@ public class ConversationFragment extends XmppFragment || t instanceof HttpDownloadConnection); activity.getMenuInflater().inflate(R.menu.message_context, menu); final MenuItem reportAndBlock = menu.findItem(R.id.action_report_and_block); + final MenuItem addReaction = menu.findItem(R.id.action_add_reaction); MenuItem openWith = menu.findItem(R.id.open_with); MenuItem copyMessage = menu.findItem(R.id.copy_message); MenuItem quoteMessage = menu.findItem(R.id.quote_message); @@ -1838,6 +1839,14 @@ public class ConversationFragment extends XmppFragment } } if (!encrypted && !m.getBody().equals("")) { + addReaction.setVisible(!showError && !m.isDeleted()); + } + if (!m.isFileOrImage() + && !encrypted + && !m.isGeoUri() + && !m.treatAsDownloadable() + && !unInitiatedButKnownSize + && t == null) { copyMessage.setVisible(true); } quoteMessage.setVisible(!encrypted && !showError); @@ -1945,10 +1954,10 @@ public class ConversationFragment extends XmppFragment activity.xmppConnectionService.deleteMessage(message); return; } - Element reactions = message.getReactions(); + Element reactions = message.getReactionsEl(); if (reactions != null) { final Message previousReaction = conversation.findMessageReactingTo(reactions.getAttribute("id"), null); - if (previousReaction != null) reactions = previousReaction.getReactions(); + if (previousReaction != null) reactions = previousReaction.getReactionsEl(); for (Element el : reactions.getChildren()) { if (message.getRawBody().endsWith(el.getContent())) { reactions.removeChild(el); @@ -2053,6 +2062,9 @@ public class ConversationFragment extends XmppFragment case R.id.action_report_and_block: reportMessage(selectedMessage); return true; + case R.id.action_add_reaction: + addReaction(selectedMessage); + return true; default: return onOptionsItemSelected(item); } @@ -2875,6 +2887,10 @@ public class ConversationFragment extends XmppFragment } } + private void addReaction(final Message message) { + activity.addReaction(message, reactions -> activity.xmppConnectionService.sendReactions(message, reactions)); + } + private void reportMessage(final Message message) { BlockContactDialog.show(activity, conversation.getContact(), message.getServerMsgId()); } diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index e1dc13e417b7f8212090699bbb8bfddf03619db4..a88c6dd4e73b9466448bb853bf385f5959b3fb42 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -1832,7 +1832,7 @@ public class StartConversationActivity extends XmppActivity @Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { - View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.list_item_tag, null); + View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_tag, null); return new ViewHolder(view); } diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 01b0d1d29b383d300485199dedfd34b9b1ac2b12..e1c64764f455cae95407e8cbe99e65ce92c60d61 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -60,6 +60,7 @@ import com.google.android.material.color.MaterialColors; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.common.base.Strings; import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableSet; import java.io.IOException; import java.lang.ref.WeakReference; @@ -75,15 +76,16 @@ import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.databinding.DialogAddReactionBinding; import eu.siacs.conversations.databinding.DialogQuickeditBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.entities.Reaction; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.BarcodeProvider; -import eu.siacs.conversations.services.EmojiInitializationService; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; @@ -103,8 +105,11 @@ import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.concurrent.RejectedExecutionException; +import java.util.function.Consumer; public abstract class XmppActivity extends ActionBarActivity { @@ -199,9 +204,11 @@ public abstract class XmppActivity extends ActionBarActivity { } protected void hideToast() { - if (mToast != null) { - mToast.cancel(); + final var toast = this.mToast; + if (toast == null) { + return; } + toast.cancel(); } protected void replaceToast(String msg) { @@ -286,29 +293,66 @@ public abstract class XmppActivity extends ActionBarActivity { XmppConnectionService.class)); finish(); }); - builder.setPositiveButton(getString(R.string.install), + builder.setPositiveButton( + getString(R.string.install), (dialog, which) -> { - Uri uri = Uri - .parse("market://details?id=org.sufficientlysecure.keychain"); - Intent marketIntent = new Intent(Intent.ACTION_VIEW, - uri); - PackageManager manager = getApplicationContext() - .getPackageManager(); - List infos = manager - .queryIntentActivities(marketIntent, 0); - if (infos.size() > 0) { - startActivity(marketIntent); + final Uri uri = + Uri.parse("market://details?id=org.sufficientlysecure.keychain"); + Intent marketIntent = new Intent(Intent.ACTION_VIEW, uri); + PackageManager manager = getApplicationContext().getPackageManager(); + final var infos = manager.queryIntentActivities(marketIntent, 0); + if (infos.isEmpty()) { + final var website = Uri.parse("http://www.openkeychain.org/"); + final Intent browserIntent = new Intent(Intent.ACTION_VIEW, website); + try { + startActivity(browserIntent); + } catch (final ActivityNotFoundException e) { + Toast.makeText( + this, + R.string.application_found_to_open_website, + Toast.LENGTH_LONG) + .show(); + } } else { - uri = Uri.parse("http://www.openkeychain.org/"); - Intent browserIntent = new Intent( - Intent.ACTION_VIEW, uri); - startActivity(browserIntent); + startActivity(marketIntent); } finish(); }); builder.create().show(); } + public void addReaction(final Message message, Consumer> callback) { + final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); + final var layoutInflater = this.getLayoutInflater(); + final DialogAddReactionBinding viewBinding = + DataBindingUtil.inflate(layoutInflater, R.layout.dialog_add_reaction, null, false); + builder.setView(viewBinding.getRoot()); + final var dialog = builder.create(); + for (final String emoji : Reaction.SUGGESTIONS) { + final Button button = + (Button) + layoutInflater.inflate( + R.layout.item_emoji_button, viewBinding.emojis, false); + viewBinding.emojis.addView(button); + button.setText(emoji); + button.setOnClickListener( + v -> { + final var aggregated = message.getAggregatedReactions(); + if (aggregated.ourReactions.contains(emoji)) { + callback.accept(aggregated.ourReactions); + } else { + final ImmutableSet.Builder reactionBuilder = + new ImmutableSet.Builder<>(); + reactionBuilder.addAll(aggregated.ourReactions); + reactionBuilder.add(emoji); + callback.accept(reactionBuilder.build()); + } + dialog.dismiss(); + }); + } + dialog.show(); + } + protected void deleteAccount(final Account account) { this.deleteAccount(account, null); } @@ -508,7 +552,6 @@ public abstract class XmppActivity extends ActionBarActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); metrics = getResources().getDisplayMetrics(); - EmojiInitializationService.execute(this); this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); this.mCustomColors = ThemeHelper.applyCustomColors(this); } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java index feef424734d3c30850674454df1853df286f01a7..efdfa82db3bb4c4a3ee25792d9468e9bd625c672 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -100,7 +100,7 @@ public class ListItemAdapter extends ArrayAdapter { final ImmutableList.Builder viewIdBuilder = new ImmutableList.Builder<>(); for (final ListItem.Tag tag : tags) { final String name = tag.getName(); - final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, viewHolder.tags, false); + final TextView tv = (TextView) inflater.inflate(R.layout.item_tag, viewHolder.tags, false); tv.setText(name); tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(getContext(),XEP0392Helper.rgbFromNick(name)))); tv.setOnClickListener(this.onTagTvClick); @@ -114,7 +114,7 @@ public class ListItemAdapter extends ArrayAdapter { final TextView tv = (TextView) inflater.inflate( - R.layout.list_item_tag, viewHolder.tags, false); + R.layout.item_tag, viewHolder.tags, false); tv.setText(R.string.blocked); tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(tv.getContext(),ContextCompat.getColor(tv.getContext(),R.color.gray_800)))); final int id = ViewCompat.generateViewId(); @@ -127,7 +127,7 @@ public class ListItemAdapter extends ArrayAdapter { final TextView tv = (TextView) inflater.inflate( - R.layout.list_item_tag, viewHolder.tags, false); + R.layout.item_tag, viewHolder.tags, false); UIHelper.setStatus(tv, status); final int id = ViewCompat.generateViewId(); tv.setId(id); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index e17625dd5f511d5952cf175b271f026e57c9c4b5..e64e16d444036aee9c26b2955db9c570242e55b6 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -30,10 +30,12 @@ import android.view.accessibility.AccessibilityEvent; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; +import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.ArrayAdapter; +import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListAdapter; @@ -64,11 +66,17 @@ import com.cheogram.android.Util; import com.cheogram.android.WebxdcPage; import com.cheogram.android.WebxdcUpdate; +import androidx.emoji2.emojipicker.EmojiViewItem; +import androidx.emoji2.emojipicker.RecentEmojiProvider; + import com.google.android.material.button.MaterialButton; +import com.google.android.material.chip.ChipGroup; import com.google.android.material.color.MaterialColors; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.lelloman.identicon.view.GithubIdenticonView; @@ -94,6 +102,7 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; import eu.siacs.conversations.databinding.LinkDescriptionBinding; +import eu.siacs.conversations.databinding.DialogAddReactionBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -109,6 +118,7 @@ import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.NotificationService; import eu.siacs.conversations.ui.Activities; +import eu.siacs.conversations.ui.BindingAdapters; import eu.siacs.conversations.ui.ConversationFragment; import eu.siacs.conversations.ui.ConversationsActivity; import eu.siacs.conversations.ui.XmppActivity; @@ -132,12 +142,14 @@ import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.mam.MamReference; import eu.siacs.conversations.xml.Element; +import kotlin.coroutines.Continuation; import java.net.URI; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Locale; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -1126,6 +1138,7 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.audioPlayer = view.findViewById(R.id.audio_player); viewHolder.link_descriptions = view.findViewById(R.id.link_descriptions); viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon); + viewHolder.reactions = view.findViewById(R.id.reactions); break; case RECEIVED: view = activity.getLayoutInflater().inflate(R.layout.item_message_received, parent, false); @@ -1149,6 +1162,7 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.commands_list = view.findViewById(R.id.commands_list); viewHolder.link_descriptions = view.findViewById(R.id.link_descriptions); viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon); + viewHolder.reactions = view.findViewById(R.id.reactions); break; case STATUS: view = @@ -1518,6 +1532,16 @@ public class MessageAdapter extends ArrayAdapter { CryptoHelper.encryptionTypeToText(message.getEncryption())); } } + BindingAdapters.setReactionsOnReceived( + viewHolder.reactions, + message.getAggregatedReactions(), + reactions -> sendReactions(message, reactions), + () -> addReaction(message)); + } else if (type == SENT) { + BindingAdapters.setReactionsOnSent( + viewHolder.reactions, + message.getAggregatedReactions(), + reactions -> sendReactions(message, reactions)); } if (type == RECEIVED || type == SENT) { @@ -1585,6 +1609,17 @@ public class MessageAdapter extends ArrayAdapter { return view; } + private void sendReactions(final Message message, final Collection reactions) { + if (activity.xmppConnectionService.sendReactions(message, reactions)) { + return; + } + Toast.makeText(activity, R.string.could_not_add_reaction, Toast.LENGTH_LONG).show(); + } + + private void addReaction(final Message message) { + activity.addReaction(message, reactions -> activity.xmppConnectionService.sendReactions(message,reactions)); + } + private void promptOpenKeychainInstall(View view) { activity.showInstallPgpDialog(); } @@ -1792,6 +1827,7 @@ public class MessageAdapter extends ArrayAdapter { protected ListView commands_list; protected ListView link_descriptions; protected GithubIdenticonView thread_identicon; + protected ChipGroup reactions; } class ThumbnailTask extends AsyncTask { diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java index bc8af1145ddf3dc9c502be0b47297292c62bb11e..c68f8c8c0bea2e1e36e3a7d0746d705660515bea 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java @@ -149,7 +149,7 @@ public class UserAdapter extends ListAdapter viewIdBuilder = new ImmutableList.Builder<>(); for (MucOptions.Hat hat : user.getPseudoHats(viewHolder.binding.getRoot().getContext())) { final String tag = hat.toString(); - final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, viewHolder.binding.tags, false); + final TextView tv = (TextView) inflater.inflate(R.layout.item_tag, viewHolder.binding.tags, false); tv.setText(tag); tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(context,hat.getColor()))); final int id = ViewCompat.generateViewId(); @@ -159,7 +159,7 @@ public class UserAdapter extends ListAdapter + + + + diff --git a/src/main/res/layout/dialog_add_reaction.xml b/src/main/res/layout/dialog_add_reaction.xml new file mode 100644 index 0000000000000000000000000000000000000000..4eda335e28bb81040dc78443985edd032887e113 --- /dev/null +++ b/src/main/res/layout/dialog_add_reaction.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/item_emoji_button.xml b/src/main/res/layout/item_emoji_button.xml new file mode 100644 index 0000000000000000000000000000000000000000..0e1711f7c1af7bb335450f5921d117dcb15bad2b --- /dev/null +++ b/src/main/res/layout/item_emoji_button.xml @@ -0,0 +1,8 @@ + +