diff options
-rw-r--r-- | dino.doap | 308 | ||||
-rw-r--r-- | dino.doap.in | 202 | ||||
-rw-r--r-- | libdino/src/service/avatar_manager.vala | 10 | ||||
-rw-r--r-- | libdino/src/service/avatar_storage.vala | 2 | ||||
-rw-r--r-- | main/CMakeLists.txt | 5 | ||||
-rw-r--r-- | main/data/emojichooser.ui | 410 | ||||
-rw-r--r-- | main/data/im.dino.Dino.appdata.xml | 15 | ||||
-rw-r--r-- | main/src/emojichooser.c | 820 | ||||
-rw-r--r-- | main/src/emojichooser.h | 36 | ||||
-rw-r--r-- | main/src/ui/avatar_image.vala | 4 | ||||
-rw-r--r-- | main/vapi/emojichooser.vapi | 7 | ||||
-rw-r--r-- | qlite/src/delete_builder.vala | 2 | ||||
-rw-r--r-- | qlite/src/insert_builder.vala | 2 | ||||
-rw-r--r-- | qlite/src/update_builder.vala | 2 | ||||
-rw-r--r-- | qlite/src/upsert_builder.vala | 2 | ||||
-rw-r--r-- | xmpp-vala/src/core/stanza_node.vala | 3 | ||||
-rw-r--r-- | xmpp-vala/src/core/stanza_reader.vala | 56 |
17 files changed, 1840 insertions, 46 deletions
@@ -1,21 +1,299 @@ <?xml version="1.0" encoding="UTF-8"?> -<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" - xmlns:foaf="http://xmlns.com/foaf/0.1/" - xmlns="http://usefulinc.com/ns/doap#"> - +<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" xmlns:foaf="http://xmlns.com/foaf/0.1/" xmlns="http://usefulinc.com/ns/doap#"> <name>Dino</name> <short-name>dino</short-name> - <shortdesc xml:lang="en">Modern Jabber/XMPP Client</shortdesc> + <shortdesc xml:lang="en">Modern XMPP Chat Client</shortdesc> + <shortdesc xml:lang="zh-Hans">现代 XMPP 聊天客户端</shortdesc> + <shortdesc xml:lang="ru">Современный чат-клиент на XMPP</shortdesc> + <shortdesc xml:lang="ro">Client XMPP de discuții modern</shortdesc> + <shortdesc xml:lang="pt">Moderno cliente de chat XMPP</shortdesc> + <shortdesc xml:lang="pl">Nowoczesny komunikator XMPP</shortdesc> + <shortdesc xml:lang="nl">Moderne XMPP-chatcliënt</shortdesc> + <shortdesc xml:lang="nb">Moderne XMPP-sludreklient</shortdesc> + <shortdesc xml:lang="lb">Modernen XMPP Chat Client</shortdesc> + <shortdesc xml:lang="ja">現代的な XMPP チャット クライアント</shortdesc> + <shortdesc xml:lang="it">Client di chat moderno per XMPP</shortdesc> + <shortdesc xml:lang="hu">Modern XMPP Üzenetküldő</shortdesc> + <shortdesc xml:lang="gl">Cliente moderno para parolas XMPP</shortdesc> + <shortdesc xml:lang="fr">Client XMPP moderne</shortdesc> + <shortdesc xml:lang="eu">XMPP txat bezero modernoa</shortdesc> + <shortdesc xml:lang="es">Cliente de XMPP moderno</shortdesc> + <shortdesc xml:lang="de">Moderner XMPP Chat Client</shortdesc> + <shortdesc xml:lang="ar">تطبيق حديث للدردشة عبر XMPP</shortdesc> <description xml:lang="en"> - Dino is an instant messaging client for the - Jabber/XMPP network, providing a unique and modern user experience - based on the latest technology from the GNOME project. - </description> - - <homepage rdf:resource="https://dino.im/" /> - <bug-database rdf:resource="https://github.com/dino/dino/issues" /> - <category rdf:resource="http://api.gnome.org/doap-extensions#apps" /> + Dino is a modern open-source chat client for the desktop. It focuses on providing a clean and reliable Jabber/XMPP experience while having your privacy in mind. + It supports end-to-end encryption with OMEMO and OpenPGP and allows configuring privacy-related features such as read receipts and typing notifications. + Dino fetches history from the server and synchronizes messages with other devices. + </description> + <description xml:lang="zh-Hans"> + Dino 是一个现代的开源聊天桌面客户端。它致力于提供一个清爽又可靠的 Jabber/XMPP 体验,同时又保护您的隐私。 + 它支持 OMEMO 和 OpenPGP 端对端加密并允许配置隐私相关的特性比如已读回执和输入提醒。 + Dino 从服务器获取消息并和其他设备同步。 + </description> + <description xml:lang="ru"> + Dino - это современный клиент для чатов с открытым исходным кодом, направленный на надёжное и приватное использование Jabber/XMPP на персональных компьютерах. + Он поддерживает сквозное шифрование через OMEMO и OpenPGP и позволяет настраивать такие функции, как уведомления о прочтении и наборе сообщений. + Dino загружает историю с сервера и синхронизирует сообщения с другими устройствами. + </description> + <description xml:lang="ro"> + Dino este un client de chat modern, cu sursă deschisă, pentru calculatoare. Se concentrează să furnizeze o experiență Jabber/XMPP clară și fiabilă, ținând cont de confidențialitatea dumneavoastră. + Suportă criptare de la un capăt la altul prin intermediul OMEMO și OpenPGP, și permite configurarea caracteristicilor legate de confidențialitate precum trimiterea notificărilor de primire și tastare. + Dino preia istoricul discuțiilor de pe server și sincronizează mesajele cu celelalte dispozitive. + </description> + <description xml:lang="pt"> + Dino é um moderno chat de código aberto para desktop. Ele é focado em prover uma transparente e confiável experiência Jabber/XMPP, tendo em mente a sua privacidade. + Suporte criptografia ponta a ponta com OMEMO e OpenPGP e permite configurar privacidade—características relacionadas às notificações de leitura, recebimento e escrita. + Dino obtém o histórico do servidor e sincroniza mensagens com outros dispositivos. + </description> + <description xml:lang="pl"> + Dino jest nowoczesnym, otwartym komunikatorem. Skupia się na prostej obsłudze sieci Jabber/XMPP dbając o twoją prywatność. + Dino obsługuje szyfrowanie end-to-end za pomocą OMEMO i OpenPGP, a także daje kontrolę nad funkcjami wpływającymi na prywatność, takimi jak powiadomienia o pisaniu czy odczytaniu wiadomości. + Dino pobiera historię rozmów z serwera i synchronizuje wiadomości z innymi urządzeniami. + </description> + <description xml:lang="nl"> + Dino is een moderne, vrije chattoepassing voor uw bureaublad. Ze biedt een eenvoudige en betrouwbare Jabber/XMPP-ervaring, met uw privacy in het achterhoofd. + Ze ondersteunt eind-tot-eind-versleuteling met OMEMO en OpenPGP, en staat u toe privacy-gerelateerde functies, zoals leesbevestigingen en typmeldingen, in te stellen. + Dino haalt de geschiedenis op van de server en synchroniseert berichten met andere apparaten. + </description> + <description xml:lang="nb"> + Dino er en moderne friporg-sludringsklient for skrivebordet. Det fokuserer på rask og pålitelig XMPP-opplevelse, samtidig som det hegner om personvernet. + Det støtter ende-til-ende -kryptering med OMEMO og OpenPGP, og tillater oppsett av personvernsrelaterte funksjoner som meldingskvitteringer og skrivevarsling. + Dino henter historikk fra tjeneren og synkroniserer meldinger med andre enheter. + </description> + <description xml:lang="lb"> + Dino ass e modernen, quell-offene Chat Client fir den Desktop. Hien biet eng opgeraumt a robust Jabber/XMPP Erfarung a leet ee Schwéierpunkt op Privatsphär. + Hien ënnerstëtz Enn-zu-Enn Verschlësselung mat OMEMO an OpenPGP an enthält Privatsphäre-Astellungen zu Liesbestätegungen an Tipp-Benoriichtegungen. + Dino rifft Gespréichverläf vum Server of a synchroniséiert Noriichte mat anere Geräter. + </description> + <description xml:lang="it"> + Dino è un client di chat per il desktop, moderno e open-source. Si concentra nel fornire un'esperienza Jabber/XMPP pulita e affidabile tenendo presente la tua privacy. + Support la crittografia end-to-end tramite OMEMO e OpenPGP e permette di configurare le funzioni relative alla privacy come le ricevute di lettura e le notifiche di digitazione. + Dino recupera la cronologia dal server e sincronizza i messaggi con gli altri dispositivi. + </description> + <description xml:lang="hu"> + A Dino egy modern, nyílt forráskódú üzenetküldő alkalmazás asztali rendszerekre, ami a hangsúlyt a letisztult és megbízható Jabber/XMPP élményre helyezi, miközben a magánszféra megőrzését is fontosnak tartja. + Támogatja a végponttól-végpontig titkosítást az OMEMO és az OpenPGP által, és magánszférához kötődő beállítási lehetőségeket is biztosít, mint például a kézbesítési, vagy gépelési értesítések küldése. + A Dino lekéri a chat előzményeket a szerverről, és szinkronizálja az üzeneteket a többi eszközzel. + </description> + <description xml:lang="gl"> + Dino é un cliente moderno e de código aberto para o escritorio. Orientado a fornecer unha experiencia Jabber/XMPP limpa e fiábel tendo a privacidade e seguranza presentes. + Suporta o cifrado de punto-a-punto con OMEMO e OpenPGP e permite configurar trazos orientados á privacidade tales coma confirmación de lectura e notificacións de escritura. + Dino obtén o histórico dende o servidor e sincroniza as mensaxes con outros dispositivos. + </description> + <description xml:lang="fr"> + Dino est un client de chat libre et moderne pour le bureau. Il tente de fournir une expérience Jabber/XMPP simple et fiable tout en ayant toujours à l’esprit votre vie privée. + Il prend en charge le chiffrement de bout en bout avec OMEMO et OpenPGP et permet de configurer les fonctions liées à la vie privée telles que l’accusé de réception et les notifications de frappe. + Dino récupère l’historique du serveur et synchronise les messages avec d'autres appareils. + </description> + <description xml:lang="fi"> + Dino on nykyaikainen avoimen lähdekoodin jutteluohjelma työpöydälle. Se keskittyy tarjoamaan selkeän ja luotettavan Jabber/XMPP-kokemuksen unohtamatta yksityisyyttäsi. + Se tukee päästä päähän -salausta OMEMO:n ja OpenPGP:n avulla ja mahdollistaa yksityisyyteen liittyvien ominaisuuksien, kuten lukukuittausten ja kirjoitusilmoitusten asetusten määrittämisen. + Dino hakee historian palvelimelta ja synkronisoi viestit muiden laitteiden kanssa. + </description> + <description xml:lang="eu"> + Dino mahaigainerako iturburu irekiko txat bezero moderno bat da. Jabber/XMPP esperientzia garbi eta fidagarri bat ematen du zure pribatutasuna kontuan hartzeaz gain. + Amaieratik amaierarako enkriptazioa onartzen du OMEMO eta OpenPGPrekin eta pribatutasun ezaugarriak konfiguratzea baimentzen du irakurtze markak eta idazketa jakinarazpenak bezala. + Dinok zerbitzaritik hartzen du historia eta beste gailuekin mezuak sinkronizatzen ditu. + </description> + <description xml:lang="es"> + Dino es un cliente de mensajería moderno y libre para escritorio. Está enfocado en proveer una experiencia Jabber/XMPP limpia y confiable teniendo tu privacidad en mente. + Soporta cifrado de extremo a extremo a través de OMEMO y OpenPGP y permite configurar las características relacionadas con la privacidad, como confirmaciones de lectura y notificaciones de escritura. + Dino recupera los mensajes desde el servidor y sincroniza los mensajes con otros dispositivos. + </description> + <description xml:lang="de"> + Dino ist ein moderner, quelloffener Chat Client. Er bietet eine aufgeräumte und robuste Jabber/XMPP Erfahrung und legt einen Schwerpunkt auf Privatsphäre. + Er unterstützt Ende-zu-Ende Verschlüsselung mit OMEMO und OpenPGP und enthält Privatsphäre-Einstellungen zu Lesebestätigungen und Tippbenachrichtigungen. + Dino ruft Gesprächsverläufe vom Server ab und synchronisiert Nachrichten mit anderen Geräten. + </description> + <description xml:lang="ca"> + Dino és un client de xat lliure i modern per a l'escriptori. Està centrat en proveir una experiència neta i fiable de Jabber/XMPP, sempre tenint en compte la vostra privacitat. + Implementa xifratge punt a punt amb OMEMO i OpenPGP, i permet configurar funcionalitats relacionades amb la privacitat com per exemple rebuts de lectura i notificacions d'escriptura. + Dino recupera l'historial del servidor i sincronitza els missatges amb altres dispositius. + </description> + <description xml:lang="ar"> + دينو برنامج حديث ومفتوح المصدر للدردشة صُمّم لسطح المكتب. ويُركّز علي تقديم تجربة نظيفة وموثوق منها لجابر/XMPP مع أخذ خصوصيتكم بعين الإعتبار. + وهو يدعم التشفير بواسطة OMEMO و OpenPGP يسمح بإعداد ميزات الخصوصية كالإيصالات المقروءة والإخطارات عند الكتابة. + يقوم دينو بجلب السِجلّ مِن السيرفر ثم يُزامِن الرسائل مع الأجهزة الأخرى. + </description> + <homepage rdf:resource="https://dino.im/"/> + <wiki rdf:resource="https://github.com/dino/dino/wiki"/> + <bug-database rdf:resource="https://github.com/dino/dino/issues"/> + <category rdf:resource="http://api.gnome.org/doap-extensions#apps"/> + <license rdf:resource="http://usefulinc.com/doap/licenses/gplv3"/> <programming-language>Vala</programming-language> - + <os>Linux</os> + <os>FreeBSD</os> + <repository> + <GitRepository> + <location rdf:resource="https://github.com/dino/dino.git"/> + <browse rdf:resource="https://github.com/dino/dino/"/> + </GitRepository> + </repository> + <implements rdf:resource="https://xmpp.org/rfcs/rfc6120.html"/> + <implements rdf:resource="https://xmpp.org/rfcs/rfc6121.html"/> + <implements rdf:resource="https://xmpp.org/rfcs/rfc6122.html"/> + <implements rdf:resource="https://xmpp.org/rfcs/rfc7590.html"/> + <software xmlns="https://linkmauve.fr/ns/xmpp-doap#"> + <Client> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0004.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0027.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0030.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0045.html"/> + <status>partial</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0049.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0054.html"/> + <status>partial</status> + <note>Only for viewing avatars</note> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0060.html"/> + <status>partial</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0066.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0077.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0082.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0084.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0085.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0115.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0163.html"/> + <status>partial</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0184.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0191.html"/> + <status>partial</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0198.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0199.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0203.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0280.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0313.html"/> + <status>partial</status> + <note>Not for MUCs</note> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0333.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0363.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0368.html"/> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0380.html"/> + <status>partial</status> + <note>Only for outgoing messages</note> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0384.html"/> + <status>complete</status> + </SupportedXep> + </supports> + </Client> + </software> </Project> diff --git a/dino.doap.in b/dino.doap.in new file mode 100644 index 00000000..b391091e --- /dev/null +++ b/dino.doap.in @@ -0,0 +1,202 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" + xmlns:foaf="http://xmlns.com/foaf/0.1/" + xmlns="http://usefulinc.com/ns/doap#"> + + <name>Dino</name> + <short-name>dino</short-name> + + <shortdesc xml:lang="en">Modern XMPP Chat Client</shortdesc> + <description xml:lang="en"> + Dino is a modern open-source chat client for the desktop. It focuses on providing a clean and reliable Jabber/XMPP experience while having your privacy in mind. + It supports end-to-end encryption with OMEMO and OpenPGP and allows configuring privacy-related features such as read receipts and typing notifications. + Dino fetches history from the server and synchronizes messages with other devices. + </description> + + <homepage rdf:resource="https://dino.im/" /> + <wiki rdf:resource="https://github.com/dino/dino/wiki" /> + <bug-database rdf:resource="https://github.com/dino/dino/issues" /> + <category rdf:resource="http://api.gnome.org/doap-extensions#apps" /> + <license rdf:resource="http://usefulinc.com/doap/licenses/gplv3" /> + + <programming-language>Vala</programming-language> + <os>Linux</os> + <os>FreeBSD</os> + + <repository> + <GitRepository> + <location rdf:resource="https://github.com/dino/dino.git" /> + <browse rdf:resource="https://github.com/dino/dino/" /> + </GitRepository> + </repository> + + <implements rdf:resource="https://xmpp.org/rfcs/rfc6120.html" /> + <implements rdf:resource="https://xmpp.org/rfcs/rfc6121.html" /> + <implements rdf:resource="https://xmpp.org/rfcs/rfc6122.html" /> + <implements rdf:resource="https://xmpp.org/rfcs/rfc7590.html" /> + + <software xmlns="https://linkmauve.fr/ns/xmpp-doap#"> + <Client> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0004.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0027.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0030.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0045.html" /> + <status>partial</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0049.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0054.html" /> + <status>partial</status> + <note>Only for viewing avatars</note> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0060.html" /> + <status>partial</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0066.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0077.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0082.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0084.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0085.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0115.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0163.html" /> + <status>partial</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0184.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0191.html" /> + <status>partial</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0198.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0199.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0203.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0280.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0313.html" /> + <status>partial</status> + <note>Not for MUCs</note> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0333.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0363.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0368.html" /> + <status>complete</status> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0380.html" /> + <status>partial</status> + <note>Only for outgoing messages</note> + </SupportedXep> + </supports> + <supports> + <SupportedXep> + <xep rdf:resource="https://xmpp.org/extensions/xep-0384.html" /> + <status>complete</status> + </SupportedXep> + </supports> + </Client> + </software> +</Project> diff --git a/libdino/src/service/avatar_manager.vala b/libdino/src/service/avatar_manager.vala index c9c078ab..a47273ee 100644 --- a/libdino/src/service/avatar_manager.vala +++ b/libdino/src/service/avatar_manager.vala @@ -23,6 +23,7 @@ public class AvatarManager : StreamInteractionModule, Object { private HashMap<Jid, string> vcard_avatars = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func); private AvatarStorage avatar_storage = new AvatarStorage(get_storage_dir()); private HashMap<string, Pixbuf> cached_pixbuf = new HashMap<string, Pixbuf>(); + private HashMap<string, Gee.List<SourceFuncWrapper>> pending_pixbuf = new HashMap<string, Gee.List<SourceFuncWrapper>>(); private const int MAX_PIXEL = 192; public static void start(StreamInteractor stream_interactor, Database db) { @@ -50,12 +51,21 @@ public class AvatarManager : StreamInteractionModule, Object { if (cached_pixbuf.has_key(hash)) { return cached_pixbuf[hash]; } + if (pending_pixbuf.has_key(hash)) { + pending_pixbuf[hash].add(new SourceFuncWrapper(get_avatar_by_hash.callback)); + yield; + return cached_pixbuf[hash]; + } + pending_pixbuf[hash] = new ArrayList<SourceFuncWrapper>(); Pixbuf? image = yield avatar_storage.get_image(hash); if (image != null) { cached_pixbuf[hash] = image; } else { db.avatar.delete().with(db.avatar.hash, "=", hash).perform(); } + foreach (SourceFuncWrapper sfw in pending_pixbuf[hash]) { + sfw.sfun(); + } return image; } diff --git a/libdino/src/service/avatar_storage.vala b/libdino/src/service/avatar_storage.vala index d6bdd30e..26c98e12 100644 --- a/libdino/src/service/avatar_storage.vala +++ b/libdino/src/service/avatar_storage.vala @@ -33,7 +33,7 @@ public class AvatarStorage : Xep.PixbufStorage, Object { File file = File.new_for_path(Path.build_filename(folder, id)); FileInputStream stream = yield file.read_async(); - uint8 fbuf[100]; + uint8 fbuf[1024]; size_t size; Checksum checksum = new Checksum (ChecksumType.SHA1); diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index dead5348..278f3360 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -33,6 +33,7 @@ set(RESOURCE_LIST contact_details_dialog.ui conversation_list_titlebar.ui conversation_list_titlebar_csd.ui + emojichooser.ui global_search.ui conversation_selector/chat_row_tooltip.ui conversation_selector/conversation_row.ui @@ -134,6 +135,7 @@ CUSTOM_VAPIS ${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi ${CMAKE_BINARY_DIR}/exports/qlite.vapi ${CMAKE_BINARY_DIR}/exports/dino_internal.vapi + vapi/emojichooser.vapi PACKAGES ${MAIN_PACKAGES} ${MAIN_EXTRA_PACKAGES} @@ -144,8 +146,9 @@ OPTIONS ) add_definitions(${VALA_CFLAGS} -DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\" -DLOCALE_INSTALL_DIR=\"${LOCALE_INSTALL_DIR}\") -add_executable(dino ${MAIN_VALA_C} ${MAIN_GRESOURCES_TARGET}) +add_executable(dino ${MAIN_VALA_C} ${MAIN_GRESOURCES_TARGET} src/emojichooser.c) add_dependencies(dino ${GETTEXT_PACKAGE}-translations) +target_include_directories(dino PRIVATE src) target_link_libraries(dino libdino ${MAIN_PACKAGES}) if(WIN32) diff --git a/main/data/emojichooser.ui b/main/data/emojichooser.ui new file mode 100644 index 00000000..d47a2c22 --- /dev/null +++ b/main/data/emojichooser.ui @@ -0,0 +1,410 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface domain="gtk30"> + <template class="DinoEmojiChooser" parent="GtkPopover"> + <property name="modal">1</property> + <style> + <class name="emoji-picker"/> + </style> + <child> + <object class="GtkBox" id="box"> + <property name="orientation">vertical</property> + <property name="visible">1</property> + <child> + <object class="GtkSearchEntry" id="search_entry"> + <property name="visible">1</property> + <property name="input-hints">no-emoji</property> + <signal name="search-changed" handler="search_changed"/> + </object> + </child> + <child> + <object class="GtkStack" id="stack"> + <property name="visible">1</property> + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkScrolledWindow" id="scrolled_window"> + <property name="visible">1</property> + <property name="vexpand">1</property> + <property name="hscrollbar-policy">never</property> + <property name="min-content-height">250</property> + <style> + <class name="view"/> + </style> + <child> + <object class="GtkBox" id="emoji_box"> + <property name="visible">1</property> + <property name="orientation">vertical</property> + <property name="margin">6</property> + <property name="spacing">6</property> + <!-- Remember to keep headings here in sync with button tooltips below --> + <child> + <object class="GtkFlowBox" id="recent.box"> + <property name="visible">1</property> + <property name="homogeneous">1</property> + <property name="selection-mode">none</property> + <signal name="child-activated" handler="emoji_activated"/> + </object> + </child> + <child> + <object class="GtkLabel" id="people.heading"> + <property name="visible">1</property> + <property name="label" translatable="yes">Smileys & People</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkFlowBox" id="people.box"> + <property name="visible">1</property> + <property name="homogeneous">1</property> + <property name="selection-mode">none</property> + <signal name="child-activated" handler="emoji_activated"/> + </object> + </child> + <child> + <object class="GtkLabel" id="body.heading"> + <property name="visible">1</property> + <property name="label" translatable="yes">Body & Clothing</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkFlowBox" id="body.box"> + <property name="visible">1</property> + <property name="homogeneous">1</property> + <property name="selection-mode">none</property> + <signal name="child-activated" handler="emoji_activated"/> + </object> + </child> + <child> + <object class="GtkLabel" id="nature.heading"> + <property name="visible">1</property> + <property name="label" translatable="yes">Animals & Nature</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkFlowBox" id="nature.box"> + <property name="visible">1</property> + <property name="homogeneous">1</property> + <property name="selection-mode">none</property> + <signal name="child-activated" handler="emoji_activated"/> + </object> + </child> + <child> + <object class="GtkLabel" id="food.heading"> + <property name="visible">1</property> + <property name="label" translatable="yes">Food & Drink</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkFlowBox" id="food.box"> + <property name="visible">1</property> + <property name="homogeneous">1</property> + <property name="selection-mode">none</property> + <signal name="child-activated" handler="emoji_activated"/> + </object> + </child> + <child> + <object class="GtkLabel" id="travel.heading"> + <property name="visible">1</property> + <property name="label" translatable="yes">Travel & Places</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkFlowBox" id="travel.box"> + <property name="visible">1</property> + <property name="homogeneous">1</property> + <property name="selection-mode">none</property> + <signal name="child-activated" handler="emoji_activated"/> + </object> + </child> + <child> + <object class="GtkLabel" id="activities.heading"> + <property name="visible">1</property> + <property name="label" translatable="yes">Activities</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkFlowBox" id="activities.box"> + <property name="visible">1</property> + <property name="homogeneous">1</property> + <property name="selection-mode">none</property> + <signal name="child-activated" handler="emoji_activated"/> + </object> + </child> + <child> + <object class="GtkLabel" id="objects.heading"> + <property name="visible">1</property> + <property name="label" translatable="yes" context="emoji category">Objects</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkFlowBox" id="objects.box"> + <property name="visible">1</property> + <property name="homogeneous">1</property> + <property name="selection-mode">none</property> + <signal name="child-activated" handler="emoji_activated"/> + </object> + </child> + <child> + <object class="GtkLabel" id="symbols.heading"> + <property name="visible">1</property> + <property name="label" translatable="yes">Symbols</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkFlowBox" id="symbols.box"> + <property name="visible">1</property> + <property name="homogeneous">1</property> + <property name="selection-mode">none</property> + <signal name="child-activated" handler="emoji_activated"/> + </object> + </child> + <child> + <object class="GtkLabel" id="flags.heading"> + <property name="visible">1</property> + <property name="label" translatable="yes">Flags</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkFlowBox" id="flags.box"> + <property name="visible">1</property> + <property name="homogeneous">1</property> + <property name="selection-mode">none</property> + <signal name="child-activated" handler="emoji_activated"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="visible">1</property> + <!-- Remember to keep tooltips here in sync with section headings above --> + <child> + <object class="GtkButton" id="recent.button"> + <property name="visible">1</property> + <property name="relief">none</property> + <property name="tooltip-text" translatable="yes">Recent</property> + <style> + <class name="emoji-section"/> + </style> + <child> + <object class="GtkImage" id="recent.icon"> + <property name="visible">1</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkButton" id="people.button"> + <property name="visible">1</property> + <property name="relief">none</property> + <property name="tooltip-text" translatable="yes">Smileys & People</property> + <style> + <class name="emoji-section"/> + </style> + <child> + <object class="GtkImage" id="people.icon"> + <property name="visible">1</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkButton" id="body.button"> + <property name="visible">1</property> + <property name="relief">none</property> + <property name="tooltip-text" translatable="yes">Body & Clothing</property> + <style> + <class name="emoji-section"/> + </style> + <child> + <object class="GtkImage" id="body.icon"> + <property name="visible">1</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkButton" id="nature.button"> + <property name="visible">1</property> + <property name="relief">none</property> + <property name="tooltip-text" translatable="yes">Animals & Nature</property> + <style> + <class name="emoji-section"/> + </style> + <child> + <object class="GtkImage" id="nature.icon"> + <property name="visible">1</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkButton" id="food.button"> + <property name="visible">1</property> + <property name="relief">none</property> + <property name="tooltip-text" translatable="yes">Food & Drink</property> + <style> + <class name="emoji-section"/> + </style> + <child> + <object class="GtkImage" id="food.icon"> + <property name="visible">1</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkButton" id="travel.button"> + <property name="visible">1</property> + <property name="relief">none</property> + <property name="tooltip-text" translatable="yes">Travel & Places</property> + <style> + <class name="emoji-section"/> + </style> + <child> + <object class="GtkImage" id="travel.icon"> + <property name="visible">1</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkButton" id="activities.button"> + <property name="visible">1</property> + <property name="relief">none</property> + <property name="tooltip-text" translatable="yes">Activities</property> + <style> + <class name="emoji-section"/> + </style> + <child> + <object class="GtkImage" id="activities.icon"> + <property name="visible">1</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkButton" id="objects.button"> + <property name="visible">1</property> + <property name="relief">none</property> + <property name="tooltip-text" translatable="yes" context="emoji category">Objects</property> + <style> + <class name="emoji-section"/> + </style> + <child> + <object class="GtkImage" id="objects.icon"> + <property name="visible">1</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkButton" id="symbols.button"> + <property name="visible">1</property> + <property name="relief">none</property> + <property name="tooltip-text" translatable="yes">Symbols</property> + <style> + <class name="emoji-section"/> + </style> + <child> + <object class="GtkImage" id="symbols.icon"> + <property name="visible">1</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkButton" id="flags.button"> + <property name="visible">1</property> + <property name="relief">none</property> + <property name="tooltip-text" translatable="yes">Flags</property> + <style> + <class name="emoji-section"/> + </style> + <child> + <object class="GtkImage" id="flags.icon"> + <property name="visible">1</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">list</property> + </packing> + </child> + <child> + <object class="GtkGrid"> + <property name="visible">1</property> + <property name="row-spacing">12</property> + <property name="halign">center</property> + <property name="valign">center</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkImage"> + <property name="visible">1</property> + <property name="icon-name">edit-find-symbolic</property> + <property name="pixel-size">72</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">1</property> + <property name="label" translatable="yes">No Results Found</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="1.44"/> + </attributes> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">1</property> + <property name="label" translatable="yes">Try a different search</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">2</property> + </packing> + </child> + </object> + <packing> + <property name="name">empty</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/main/data/im.dino.Dino.appdata.xml b/main/data/im.dino.Dino.appdata.xml index ba138e6c..0828f4c9 100644 --- a/main/data/im.dino.Dino.appdata.xml +++ b/main/data/im.dino.Dino.appdata.xml @@ -7,13 +7,15 @@ <name>Dino</name> <summary>Modern XMPP Chat Client</summary> <summary xml:lang="zh_Hans">现代 XMPP 聊天客户端</summary> + <summary xml:lang="ru">Современный чат-клиент на XMPP</summary> <summary xml:lang="ro">Client XMPP de discuții modern</summary> <summary xml:lang="pt_BR">Moderno cliente de chat XMPP</summary> - <summary xml:lang="pl">Nowoczesny klient Chatu XMPP</summary> + <summary xml:lang="pl">Nowoczesny komunikator XMPP</summary> <summary xml:lang="nl_BE">Modernen XMPP-chatcliënt</summary> <summary xml:lang="nl">Moderne XMPP-chatcliënt</summary> <summary xml:lang="nb">Moderne XMPP-sludreklient</summary> <summary xml:lang="lb">Modernen XMPP Chat Client</summary> + <summary xml:lang="ja">現代的な XMPP チャット クライアント</summary> <summary xml:lang="it">Client di chat moderno per XMPP</summary> <summary xml:lang="hu">Modern XMPP Üzenetküldő</summary> <summary xml:lang="gl">Cliente moderno para parolas XMPP</summary> @@ -28,7 +30,7 @@ <p xml:lang="ru">Dino - это современный клиент для чатов с открытым исходным кодом, направленный на надёжное и приватное использование Jabber/XMPP на персональных компьютерах.</p> <p xml:lang="ro">Dino este un client de chat modern, cu sursă deschisă, pentru calculatoare. Se concentrează să furnizeze o experiență Jabber/XMPP clară și fiabilă, ținând cont de confidențialitatea dumneavoastră.</p> <p xml:lang="pt_BR">Dino é um moderno chat de código aberto para desktop. Ele é focado em prover uma transparente e confiável experiência Jabber/XMPP, tendo em mente a sua privacidade.</p> - <p xml:lang="pl">Dino jest nowoczesnym klientem chat z otwartym źródłem w wersji na komputery. Skupia się na prostej obsłudze Jabber/XMPP dbając o twoją prywatność.</p> + <p xml:lang="pl">Dino jest nowoczesnym, otwartym komunikatorem. Skupia się na prostej obsłudze sieci Jabber/XMPP dbając o twoją prywatność.</p> <p xml:lang="nl_BE">Dino is een moderne, vrije chattoepassing voor uw bureaublad. Ze biedt een eenvoudige en betrouwbare Jabber/XMPP-ervaring, met uw privacy in het achterhoofd.</p> <p xml:lang="nl">Dino is een moderne, vrije chattoepassing voor uw bureaublad. Ze biedt een eenvoudige en betrouwbare Jabber/XMPP-ervaring, met uw privacy in het achterhoofd.</p> <p xml:lang="nb">Dino er en moderne friporg-sludringsklient for skrivebordet. Det fokuserer på rask og pålitelig XMPP-opplevelse, samtidig som det hegner om personvernet.</p> @@ -48,6 +50,7 @@ <p xml:lang="ru">Он поддерживает сквозное шифрование через OMEMO и OpenPGP и позволяет настраивать такие функции, как уведомления о прочтении и наборе сообщений.</p> <p xml:lang="ro">Suportă criptare de la un capăt la altul prin intermediul OMEMO și OpenPGP, și permite configurarea caracteristicilor legate de confidențialitate precum trimiterea notificărilor de primire și tastare.</p> <p xml:lang="pt_BR">Suporte criptografia ponta a ponta com OMEMO e OpenPGP e permite configurar privacidade—características relacionadas às notificações de leitura, recebimento e escrita.</p> + <p xml:lang="pl">Dino obsługuje szyfrowanie end-to-end za pomocą OMEMO i OpenPGP, a także daje kontrolę nad funkcjami wpływającymi na prywatność, takimi jak powiadomienia o pisaniu czy odczytaniu wiadomości.</p> <p xml:lang="nl_BE">Ze ondersteunt eind-tot-eind-versleuteling met OMEMO en OpenPGP, en laat u toe privacygerelateerde functies, gelijk leesbevestigingen en typmeldingen, in te stellen.</p> <p xml:lang="nl">Ze ondersteunt eind-tot-eind-versleuteling met OMEMO en OpenPGP, en staat u toe privacy-gerelateerde functies, zoals leesbevestigingen en typmeldingen, in te stellen.</p> <p xml:lang="nb">Det støtter ende-til-ende -kryptering med OMEMO og OpenPGP, og tillater oppsett av personvernsrelaterte funksjoner som meldingskvitteringer og skrivevarsling.</p> @@ -67,10 +70,12 @@ <p xml:lang="ru">Dino загружает историю с сервера и синхронизирует сообщения с другими устройствами.</p> <p xml:lang="ro">Dino preia istoricul discuțiilor de pe server și sincronizează mesajele cu celelalte dispozitive.</p> <p xml:lang="pt_BR">Dino obtém o histórico do servidor e sincroniza mensagens com outros dispositivos.</p> + <p xml:lang="pl">Dino pobiera historię rozmów z serwera i synchronizuje wiadomości z innymi urządzeniami.</p> <p xml:lang="nl_BE">Dino haalt de geschiedenis op van de server en synchroniseert berichten met andere apparaten.</p> <p xml:lang="nl">Dino haalt de geschiedenis op van de server en synchroniseert berichten met andere apparaten.</p> <p xml:lang="nb">Dino henter historikk fra tjeneren og synkroniserer meldinger med andre enheter.</p> <p xml:lang="lb">Dino rifft Gespréichverläf vum Server of a synchroniséiert Noriichte mat anere Geräter.</p> + <p xml:lang="ja">Dino はサーバーから履歴を取得し、ほかのデバイスとメッセージを同期します。</p> <p xml:lang="it">Dino recupera la cronologia dal server e sincronizza i messaggi con gli altri dispositivi.</p> <p xml:lang="hu">A Dino lekéri a chat előzményeket a szerverről, és szinkronizálja az üzeneteket a többi eszközzel.</p> <p xml:lang="gl">Dino obtén o histórico dende o servidor e sincroniza as mensaxes con outros dispositivos.</p> @@ -94,6 +99,7 @@ <caption xml:lang="nl">Hoofdvenster met gesprekken</caption> <caption xml:lang="nb">Hovedvindu med samtaler</caption> <caption xml:lang="lb">Haaptfënster mat den Conversatiounen</caption> + <caption xml:lang="ja">会話中のメインウィンドウ</caption> <caption xml:lang="it">La finestra principale con le conversazioni</caption> <caption xml:lang="hu">A fő ablak a beszélgetésekkel</caption> <caption xml:lang="gl">Lapela principal con parolas</caption> @@ -117,9 +123,10 @@ <caption xml:lang="nl">Gesprek beginnen</caption> <caption xml:lang="nb">Start samtale</caption> <caption xml:lang="lb">Konversatioun starten</caption> + <caption xml:lang="ja">会話を開始</caption> <caption xml:lang="it">Inizia una Conversazione</caption> <caption xml:lang="hu">Csevegés kezdeményezése</caption> - <caption xml:lang="gl">Comezar parola</caption> + <caption xml:lang="gl">Comezar conversa</caption> <caption xml:lang="fr">Commencer une discussion</caption> <caption xml:lang="fi">Aloita keskustelu</caption> <caption xml:lang="eu">Elkarrizketa hasi</caption> @@ -139,7 +146,7 @@ <caption xml:lang="nl">Contactgegevens</caption> <caption xml:lang="nb">Kontaktdetaljer</caption> <caption xml:lang="lb">Kontaktdetailer</caption> - <caption xml:lang="ja">相手先の詳細</caption> + <caption xml:lang="ja">連絡先の詳細</caption> <caption xml:lang="it">Dettagli del contatto</caption> <caption xml:lang="hu">Felhasználó információ</caption> <caption xml:lang="gl">Detalles do contacto</caption> diff --git a/main/src/emojichooser.c b/main/src/emojichooser.c new file mode 100644 index 00000000..f8c0c51c --- /dev/null +++ b/main/src/emojichooser.c @@ -0,0 +1,820 @@ +/* gtkemojichooser.c: An Emoji chooser widget + * Copyright 2017, Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "emojichooser.h" + +#define BOX_SPACE 6 + +typedef struct { + GtkWidget *box; + GtkWidget *heading; + GtkWidget *button; + const char *first; + gunichar label; + gboolean empty; +} EmojiSection; + +struct _DinoEmojiChooser +{ + GtkPopover parent_instance; + + GtkWidget *search_entry; + GtkWidget *stack; + GtkWidget *scrolled_window; + + int emoji_max_width; + + EmojiSection recent; + EmojiSection people; + EmojiSection body; + EmojiSection nature; + EmojiSection food; + EmojiSection travel; + EmojiSection activities; + EmojiSection objects; + EmojiSection symbols; + EmojiSection flags; + + GtkGesture *recent_long_press; + GtkGesture *recent_multi_press; + GtkGesture *people_long_press; + GtkGesture *people_multi_press; + GtkGesture *body_long_press; + GtkGesture *body_multi_press; + + GVariant *data; + GtkWidget *box; + GVariantIter *iter; + guint populate_idle; + + GSettings *settings; +}; + +struct _DinoEmojiChooserClass { + GtkPopoverClass parent_class; + gboolean (* popover_button_release_event) (GtkWidget *widget, + GdkEventButton *event); +}; + +enum { + EMOJI_PICKED, + LAST_SIGNAL +}; + +static int signals[LAST_SIGNAL]; + +G_DEFINE_TYPE (DinoEmojiChooser, dino_emoji_chooser, GTK_TYPE_POPOVER) + +static void +dino_emoji_chooser_finalize (GObject *object) +{ + DinoEmojiChooser *chooser = DINO_EMOJI_CHOOSER (object); + + if (chooser->populate_idle) + g_source_remove (chooser->populate_idle); + + g_variant_unref (chooser->data); + g_object_unref (chooser->settings); + + g_clear_object (&chooser->recent_long_press); + g_clear_object (&chooser->recent_multi_press); + g_clear_object (&chooser->people_long_press); + g_clear_object (&chooser->people_multi_press); + g_clear_object (&chooser->body_long_press); + g_clear_object (&chooser->body_multi_press); + + G_OBJECT_CLASS (dino_emoji_chooser_parent_class)->finalize (object); +} + +static void +scroll_to_section (GtkButton *button, + gpointer data) +{ + EmojiSection *section = data; + DinoEmojiChooser *chooser; + GtkAdjustment *adj; + GtkAllocation alloc = { 0, 0, 0, 0 }; + + chooser = DINO_EMOJI_CHOOSER (gtk_widget_get_ancestor (GTK_WIDGET (button), GTK_TYPE_EMOJI_CHOOSER)); + + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window)); + + if (section->heading) + gtk_widget_get_allocation (section->heading, &alloc); + + gtk_adjustment_set_value (adj, alloc.y - BOX_SPACE); +} + +static void +add_emoji (GtkWidget *box, + gboolean prepend, + GVariant *item, + gunichar modifier, + DinoEmojiChooser *chooser); + +#define MAX_RECENT (7*3) + +static void +populate_recent_section (DinoEmojiChooser *chooser) +{ + GVariant *variant; + GVariant *item; + GVariantIter iter; + gboolean empty = FALSE; + + variant = g_settings_get_value (chooser->settings, "recent-emoji"); + g_variant_iter_init (&iter, variant); + while ((item = g_variant_iter_next_value (&iter))) + { + GVariant *emoji_data; + gunichar modifier; + + emoji_data = g_variant_get_child_value (item, 0); + g_variant_get_child (item, 1, "u", &modifier); + add_emoji (chooser->recent.box, FALSE, emoji_data, modifier, chooser); + g_variant_unref (emoji_data); + g_variant_unref (item); + empty = FALSE; + } + + if (!empty) + { + gtk_widget_show (chooser->recent.box); + gtk_widget_set_sensitive (chooser->recent.button, TRUE); + } + g_variant_unref (variant); +} + +static void +add_recent_item (DinoEmojiChooser *chooser, + GVariant *item, + gunichar modifier) +{ + GList *children, *l; + int i; + GVariantBuilder builder; + + g_variant_ref (item); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a((auss)u)")); + g_variant_builder_add (&builder, "(@(auss)u)", item, modifier); + + children = gtk_container_get_children (GTK_CONTAINER (chooser->recent.box)); + for (l = children, i = 1; l; l = l->next, i++) + { + GVariant *item2 = g_object_get_data (G_OBJECT (l->data), "emoji-data"); + gunichar modifier2 = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (l->data), "modifier")); + + if (modifier == modifier2 && g_variant_equal (item, item2)) + { + gtk_widget_destroy (GTK_WIDGET (l->data)); + i--; + continue; + } + if (i >= MAX_RECENT) + { + gtk_widget_destroy (GTK_WIDGET (l->data)); + continue; + } + + g_variant_builder_add (&builder, "(@(auss)u)", item2, modifier2); + } + g_list_free (children); + + add_emoji (chooser->recent.box, TRUE, item, modifier, chooser); + + /* Enable recent */ + gtk_widget_show (chooser->recent.box); + gtk_widget_set_sensitive (chooser->recent.button, TRUE); + + g_settings_set_value (chooser->settings, "recent-emoji", g_variant_builder_end (&builder)); + + g_variant_unref (item); +} + +static void +emoji_activated (GtkFlowBox *box, + GtkFlowBoxChild *child, + gpointer data) +{ + DinoEmojiChooser *chooser = data; + char *text; + GtkWidget *ebox; + GtkWidget *label; + GVariant *item; + gunichar modifier; + + gtk_popover_popdown (GTK_POPOVER (chooser)); + + ebox = gtk_bin_get_child (GTK_BIN (child)); + label = gtk_bin_get_child (GTK_BIN (ebox)); + text = g_strdup (gtk_label_get_label (GTK_LABEL (label))); + + item = (GVariant*) g_object_get_data (G_OBJECT (child), "emoji-data"); + modifier = (gunichar) GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (child), "modifier")); + add_recent_item (chooser, item, modifier); + + g_signal_emit (data, signals[EMOJI_PICKED], 0, text); + g_free (text); +} + +static gboolean +has_variations (GVariant *emoji_data) +{ + GVariant *codes; + int i; + gboolean has_variations; + + has_variations = FALSE; + codes = g_variant_get_child_value (emoji_data, 0); + for (i = 0; i < g_variant_n_children (codes); i++) + { + gunichar code; + g_variant_get_child (codes, i, "u", &code); + if (code == 0) + { + has_variations = TRUE; + break; + } + } + g_variant_unref (codes); + + return has_variations; +} + +static void +show_variations (DinoEmojiChooser *chooser, + GtkWidget *child) +{ + GtkWidget *popover; + GtkWidget *view; + GtkWidget *box; + GVariant *emoji_data; + GtkWidget *parent_popover; + gunichar modifier; + + if (!child) + return; + + emoji_data = (GVariant*) g_object_get_data (G_OBJECT (child), "emoji-data"); + if (!emoji_data) + return; + + if (!has_variations (emoji_data)) + return; + + parent_popover = gtk_widget_get_ancestor (child, GTK_TYPE_POPOVER); + popover = gtk_popover_new (child); + view = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_style_context_add_class (gtk_widget_get_style_context (view), "view"); + box = gtk_flow_box_new (); + gtk_flow_box_set_homogeneous (GTK_FLOW_BOX (box), TRUE); + gtk_flow_box_set_min_children_per_line (GTK_FLOW_BOX (box), 6); + gtk_flow_box_set_max_children_per_line (GTK_FLOW_BOX (box), 6); + gtk_flow_box_set_activate_on_single_click (GTK_FLOW_BOX (box), TRUE); + gtk_flow_box_set_selection_mode (GTK_FLOW_BOX (box), GTK_SELECTION_NONE); + gtk_container_add (GTK_CONTAINER (popover), view); + gtk_container_add (GTK_CONTAINER (view), box); + + g_signal_connect (box, "child-activated", G_CALLBACK (emoji_activated), parent_popover); + + add_emoji (box, FALSE, emoji_data, 0, chooser); + for (modifier = 0x1f3fb; modifier <= 0x1f3ff; modifier++) + add_emoji (box, FALSE, emoji_data, modifier, chooser); + + gtk_widget_show_all (view); + gtk_popover_popup (GTK_POPOVER (popover)); +} + +static void +update_hover (GtkWidget *widget, + GdkEvent *event, + gpointer data) +{ + if (event->type == GDK_ENTER_NOTIFY) + gtk_widget_set_state_flags (widget, GTK_STATE_FLAG_PRELIGHT, FALSE); + else + gtk_widget_unset_state_flags (widget, GTK_STATE_FLAG_PRELIGHT); +} + +static void +long_pressed_cb (GtkGesture *gesture, + double x, + double y, + gpointer data) +{ + DinoEmojiChooser *chooser = data; + GtkWidget *box; + GtkWidget *child; + + box = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture)); + child = GTK_WIDGET (gtk_flow_box_get_child_at_pos (GTK_FLOW_BOX (box), x, y)); + show_variations (chooser, child); +} + +static void +pressed_cb (GtkGesture *gesture, + int n_press, + double x, + double y, + gpointer data) +{ + DinoEmojiChooser *chooser = data; + GtkWidget *box; + GtkWidget *child; + + box = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture)); + child = GTK_WIDGET (gtk_flow_box_get_child_at_pos (GTK_FLOW_BOX (box), x, y)); + show_variations (chooser, child); +} + +static gboolean +popup_menu (GtkWidget *widget, + gpointer data) +{ + DinoEmojiChooser *chooser = data; + + show_variations (chooser, widget); + return TRUE; +} + +static void +add_emoji (GtkWidget *box, + gboolean prepend, + GVariant *item, + gunichar modifier, + DinoEmojiChooser *chooser) +{ + GtkWidget *child; + GtkWidget *ebox; + GtkWidget *label; + PangoAttrList *attrs; + GVariant *codes; + char text[64]; + char *p = text; + int i; + PangoLayout *layout; + PangoRectangle rect; + + codes = g_variant_get_child_value (item, 0); + for (i = 0; i < g_variant_n_children (codes); i++) + { + gunichar code; + + g_variant_get_child (codes, i, "u", &code); + if (code == 0) + code = modifier; + if (code != 0) + p += g_unichar_to_utf8 (code, p); + } + g_variant_unref (codes); + p += g_unichar_to_utf8 (0xFE0F, p); /* U+FE0F is the Emoji variation selector */ + p[0] = 0; + + label = gtk_label_new (text); + attrs = pango_attr_list_new (); + pango_attr_list_insert (attrs, pango_attr_scale_new (PANGO_SCALE_X_LARGE)); + gtk_label_set_attributes (GTK_LABEL (label), attrs); + pango_attr_list_unref (attrs); + + layout = gtk_label_get_layout (GTK_LABEL (label)); + pango_layout_get_extents (layout, &rect, NULL); + + /* Check for fallback rendering that generates too wide items */ + if (pango_layout_get_unknown_glyphs_count (layout) > 0 || + rect.width >= 1.5 * chooser->emoji_max_width) + { + gtk_widget_destroy (label); + return; + } + + child = gtk_flow_box_child_new (); + gtk_style_context_add_class (gtk_widget_get_style_context (child), "emoji"); + g_object_set_data_full (G_OBJECT (child), "emoji-data", + g_variant_ref (item), + (GDestroyNotify)g_variant_unref); + if (modifier != 0) + g_object_set_data (G_OBJECT (child), "modifier", GUINT_TO_POINTER (modifier)); + + ebox = gtk_event_box_new (); + gtk_widget_add_events (ebox, GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK); + g_signal_connect (ebox, "enter-notify-event", G_CALLBACK (update_hover), FALSE); + g_signal_connect (ebox, "leave-notify-event", G_CALLBACK (update_hover), FALSE); + gtk_container_add (GTK_CONTAINER (child), ebox); + gtk_container_add (GTK_CONTAINER (ebox), label); + gtk_widget_show_all (child); + + if (chooser) + g_signal_connect (child, "popup-menu", G_CALLBACK (popup_menu), chooser); + + gtk_flow_box_insert (GTK_FLOW_BOX (box), child, prepend ? 0 : -1); +} + +static gboolean +populate_emoji_chooser (gpointer data) +{ + DinoEmojiChooser *chooser = data; + GBytes *bytes = NULL; + GVariant *item; + guint64 start, now; + + start = g_get_monotonic_time (); + + if (!chooser->data) + { + bytes = g_resources_lookup_data ("/org/gtk/libgtk/emoji/emoji.data", 0, NULL); + chooser->data = g_variant_ref_sink (g_variant_new_from_bytes (G_VARIANT_TYPE ("a(auss)"), bytes, TRUE)); + } + + if (!chooser->iter) + { + chooser->iter = g_variant_iter_new (chooser->data); + chooser->box = chooser->people.box; + } + while ((item = g_variant_iter_next_value (chooser->iter))) + { + const char *name; + + g_variant_get_child (item, 1, "&s", &name); + + if (strcmp (name, chooser->body.first) == 0) + chooser->box = chooser->body.box; + else if (strcmp (name, chooser->nature.first) == 0) + chooser->box = chooser->nature.box; + else if (strcmp (name, chooser->food.first) == 0) + chooser->box = chooser->food.box; + else if (strcmp (name, chooser->travel.first) == 0) + chooser->box = chooser->travel.box; + else if (strcmp (name, chooser->activities.first) == 0) + chooser->box = chooser->activities.box; + else if (strcmp (name, chooser->objects.first) == 0) + chooser->box = chooser->objects.box; + else if (strcmp (name, chooser->symbols.first) == 0) + chooser->box = chooser->symbols.box; + else if (strcmp (name, chooser->flags.first) == 0) + chooser->box = chooser->flags.box; + + add_emoji (chooser->box, FALSE, item, 0, chooser); + g_variant_unref (item); + + now = g_get_monotonic_time (); + if (now > start + 8000) + return G_SOURCE_CONTINUE; + } + + /* We scroll to the top on show, so check the right button for the 1st time */ + gtk_widget_set_state_flags (chooser->recent.button, GTK_STATE_FLAG_CHECKED, FALSE); + + g_variant_iter_free (chooser->iter); + chooser->iter = NULL; + chooser->box = NULL; + chooser->populate_idle = 0; + + return G_SOURCE_REMOVE; +} + +static void +adj_value_changed (GtkAdjustment *adj, + gpointer data) +{ + DinoEmojiChooser *chooser = data; + double value = gtk_adjustment_get_value (adj); + EmojiSection const *sections[] = { + &chooser->recent, + &chooser->people, + &chooser->body, + &chooser->nature, + &chooser->food, + &chooser->travel, + &chooser->activities, + &chooser->objects, + &chooser->symbols, + &chooser->flags, + }; + EmojiSection const *select_section = sections[0]; + gsize i; + + /* Figure out which section the current scroll position is within */ + for (i = 0; i < G_N_ELEMENTS (sections); ++i) + { + EmojiSection const *section = sections[i]; + GtkAllocation alloc; + + if (section->heading) + gtk_widget_get_allocation (section->heading, &alloc); + else + gtk_widget_get_allocation (section->box, &alloc); + + if (value < alloc.y - BOX_SPACE) + break; + + select_section = section; + } + + /* Un/Check the section buttons accordingly */ + for (i = 0; i < G_N_ELEMENTS (sections); ++i) + { + EmojiSection const *section = sections[i]; + + if (section == select_section) + gtk_widget_set_state_flags (section->button, GTK_STATE_FLAG_CHECKED, FALSE); + else + gtk_widget_unset_state_flags (section->button, GTK_STATE_FLAG_CHECKED); + } +} + +static gboolean +filter_func (GtkFlowBoxChild *child, + gpointer data) +{ + EmojiSection *section = data; + DinoEmojiChooser *chooser; + GVariant *emoji_data; + const char *text; + const char *name; + gboolean res; + + res = TRUE; + + chooser = DINO_EMOJI_CHOOSER (gtk_widget_get_ancestor (GTK_WIDGET (child), GTK_TYPE_EMOJI_CHOOSER)); + text = gtk_entry_get_text (GTK_ENTRY (chooser->search_entry)); + emoji_data = (GVariant *) g_object_get_data (G_OBJECT (child), "emoji-data"); + + if (text[0] == 0) + goto out; + + if (!emoji_data) + goto out; + + g_variant_get_child (emoji_data, 1, "&s", &name); + res = g_str_match_string (text, name, TRUE); + +out: + if (res) + section->empty = FALSE; + + return res; +} + +static void +invalidate_section (EmojiSection *section) +{ + section->empty = TRUE; + gtk_flow_box_invalidate_filter (GTK_FLOW_BOX (section->box)); +} + +static void +update_headings (DinoEmojiChooser *chooser) +{ + gtk_widget_set_visible (chooser->people.heading, !chooser->people.empty); + gtk_widget_set_visible (chooser->people.box, !chooser->people.empty); + gtk_widget_set_visible (chooser->body.heading, !chooser->body.empty); + gtk_widget_set_visible (chooser->body.box, !chooser->body.empty); + gtk_widget_set_visible (chooser->nature.heading, !chooser->nature.empty); + gtk_widget_set_visible (chooser->nature.box, !chooser->nature.empty); + gtk_widget_set_visible (chooser->food.heading, !chooser->food.empty); + gtk_widget_set_visible (chooser->food.box, !chooser->food.empty); + gtk_widget_set_visible (chooser->travel.heading, !chooser->travel.empty); + gtk_widget_set_visible (chooser->travel.box, !chooser->travel.empty); + gtk_widget_set_visible (chooser->activities.heading, !chooser->activities.empty); + gtk_widget_set_visible (chooser->activities.box, !chooser->activities.empty); + gtk_widget_set_visible (chooser->objects.heading, !chooser->objects.empty); + gtk_widget_set_visible (chooser->objects.box, !chooser->objects.empty); + gtk_widget_set_visible (chooser->symbols.heading, !chooser->symbols.empty); + gtk_widget_set_visible (chooser->symbols.box, !chooser->symbols.empty); + gtk_widget_set_visible (chooser->flags.heading, !chooser->flags.empty); + gtk_widget_set_visible (chooser->flags.box, !chooser->flags.empty); + + if (chooser->recent.empty && chooser->people.empty && + chooser->body.empty && chooser->nature.empty && + chooser->food.empty && chooser->travel.empty && + chooser->activities.empty && chooser->objects.empty && + chooser->symbols.empty && chooser->flags.empty) + gtk_stack_set_visible_child_name (GTK_STACK (chooser->stack), "empty"); + else + gtk_stack_set_visible_child_name (GTK_STACK (chooser->stack), "list"); +} + +static void +search_changed (GtkEntry *entry, + gpointer data) +{ + DinoEmojiChooser *chooser = data; + + invalidate_section (&chooser->recent); + invalidate_section (&chooser->people); + invalidate_section (&chooser->body); + invalidate_section (&chooser->nature); + invalidate_section (&chooser->food); + invalidate_section (&chooser->travel); + invalidate_section (&chooser->activities); + invalidate_section (&chooser->objects); + invalidate_section (&chooser->symbols); + invalidate_section (&chooser->flags); + + update_headings (chooser); +} + +static void +setup_section (DinoEmojiChooser *chooser, + EmojiSection *section, + const char *first, + const char *icon) +{ + GtkAdjustment *adj; + GtkWidget *image; + + section->first = first; + + image = gtk_bin_get_child (GTK_BIN (section->button)); + gtk_image_set_from_icon_name (GTK_IMAGE (image), icon, GTK_ICON_SIZE_BUTTON); + + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window)); + + gtk_container_set_focus_vadjustment (GTK_CONTAINER (section->box), adj); + gtk_flow_box_set_filter_func (GTK_FLOW_BOX (section->box), filter_func, section, NULL); + g_signal_connect (section->button, "clicked", G_CALLBACK (scroll_to_section), section); +} + +static void +dino_emoji_chooser_init (DinoEmojiChooser *chooser) +{ + GtkAdjustment *adj; + + chooser->settings = g_settings_new ("org.gtk.Settings.EmojiChooser"); + + gtk_widget_init_template (GTK_WIDGET (chooser)); + + /* Get a reasonable maximum width for an emoji. We do this to + * skip overly wide fallback rendering for certain emojis the + * font does not contain and therefore end up being rendered + * as multiply glyphs. + */ + { + PangoLayout *layout = gtk_widget_create_pango_layout (GTK_WIDGET (chooser), "🙂"); + PangoAttrList *attrs; + PangoRectangle rect; + + attrs = pango_attr_list_new (); + pango_attr_list_insert (attrs, pango_attr_scale_new (PANGO_SCALE_X_LARGE)); + pango_layout_set_attributes (layout, attrs); + pango_attr_list_unref (attrs); + + pango_layout_get_extents (layout, &rect, NULL); + chooser->emoji_max_width = rect.width; + + g_object_unref (layout); + } + + chooser->recent_long_press = gtk_gesture_long_press_new (chooser->recent.box); + g_signal_connect (chooser->recent_long_press, "pressed", G_CALLBACK (long_pressed_cb), chooser); + chooser->recent_multi_press = gtk_gesture_multi_press_new (chooser->recent.box); + gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (chooser->recent_multi_press), GDK_BUTTON_SECONDARY); + g_signal_connect (chooser->recent_multi_press, "pressed", G_CALLBACK (pressed_cb), chooser); + + chooser->people_long_press = gtk_gesture_long_press_new (chooser->people.box); + g_signal_connect (chooser->people_long_press, "pressed", G_CALLBACK (long_pressed_cb), chooser); + chooser->people_multi_press = gtk_gesture_multi_press_new (chooser->people.box); + gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (chooser->people_multi_press), GDK_BUTTON_SECONDARY); + g_signal_connect (chooser->people_multi_press, "pressed", G_CALLBACK (pressed_cb), chooser); + + chooser->body_long_press = gtk_gesture_long_press_new (chooser->body.box); + g_signal_connect (chooser->body_long_press, "pressed", G_CALLBACK (long_pressed_cb), chooser); + chooser->body_multi_press = gtk_gesture_multi_press_new (chooser->body.box); + gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (chooser->body_multi_press), GDK_BUTTON_SECONDARY); + g_signal_connect (chooser->body_multi_press, "pressed", G_CALLBACK (pressed_cb), chooser); + + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window)); + g_signal_connect (adj, "value-changed", G_CALLBACK (adj_value_changed), chooser); + + setup_section (chooser, &chooser->recent, NULL, "emoji-recent-symbolic"); + setup_section (chooser, &chooser->people, "grinning face", "emoji-people-symbolic"); + setup_section (chooser, &chooser->body, "selfie", "emoji-body-symbolic"); + setup_section (chooser, &chooser->nature, "monkey face", "emoji-nature-symbolic"); + setup_section (chooser, &chooser->food, "grapes", "emoji-food-symbolic"); + setup_section (chooser, &chooser->travel, "globe showing Europe-Africa", "emoji-travel-symbolic"); + setup_section (chooser, &chooser->activities, "jack-o-lantern", "emoji-activities-symbolic"); + setup_section (chooser, &chooser->objects, "muted speaker", "emoji-objects-symbolic"); + setup_section (chooser, &chooser->symbols, "ATM sign", "emoji-symbols-symbolic"); + setup_section (chooser, &chooser->flags, "chequered flag", "emoji-flags-symbolic"); + + populate_recent_section (chooser); + + chooser->populate_idle = g_idle_add (populate_emoji_chooser, chooser); + g_source_set_name_by_id (chooser->populate_idle, "[gtk] populate_emoji_chooser"); +} + +static void +dino_emoji_chooser_show (GtkWidget *widget) +{ + DinoEmojiChooser *chooser = DINO_EMOJI_CHOOSER (widget); + GtkAdjustment *adj; + + GTK_WIDGET_CLASS (dino_emoji_chooser_parent_class)->show (widget); + + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window)); + gtk_adjustment_set_value (adj, 0); + + gtk_entry_set_text (GTK_ENTRY (chooser->search_entry), ""); +} + +static gboolean +dino_emoji_chooser_button_release (GtkWidget *widget, + GdkEventButton *event) +{ + DinoEmojiChooserClass *klass = DINO_EMOJI_CHOOSER_GET_CLASS(widget); + GtkWidget *event_widget = gtk_get_event_widget ((GdkEvent *) event); + if (!event_widget && event->window != gtk_widget_get_window (widget)) + { + return GDK_EVENT_PROPAGATE; + } + return klass->popover_button_release_event (widget, event); +} + +static void +dino_emoji_chooser_class_init (DinoEmojiChooserClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = dino_emoji_chooser_finalize; + widget_class->show = dino_emoji_chooser_show; + klass->popover_button_release_event = widget_class->button_release_event; + widget_class->button_release_event = dino_emoji_chooser_button_release; + + signals[EMOJI_PICKED] = g_signal_new ("emoji-picked", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + NULL, + G_TYPE_NONE, 1, G_TYPE_STRING|G_SIGNAL_TYPE_STATIC_SCOPE); + + gtk_widget_class_set_template_from_resource (widget_class, "/im/dino/Dino/emojichooser.ui"); + + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, search_entry); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, stack); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, scrolled_window); + + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, recent.box); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, recent.button); + + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, people.box); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, people.heading); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, people.button); + + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, body.box); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, body.heading); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, body.button); + + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, nature.box); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, nature.heading); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, nature.button); + + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, food.box); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, food.heading); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, food.button); + + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, travel.box); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, travel.heading); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, travel.button); + + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, activities.box); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, activities.heading); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, activities.button); + + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, objects.box); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, objects.heading); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, objects.button); + + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, symbols.box); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, symbols.heading); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, symbols.button); + + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, flags.box); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, flags.heading); + gtk_widget_class_bind_template_child (widget_class, DinoEmojiChooser, flags.button); + + gtk_widget_class_bind_template_callback (widget_class, emoji_activated); + gtk_widget_class_bind_template_callback (widget_class, search_changed); +} + +GtkWidget * +dino_emoji_chooser_new (void) +{ + return GTK_WIDGET (g_object_new (GTK_TYPE_EMOJI_CHOOSER, NULL)); +} diff --git a/main/src/emojichooser.h b/main/src/emojichooser.h new file mode 100644 index 00000000..3e459431 --- /dev/null +++ b/main/src/emojichooser.h @@ -0,0 +1,36 @@ +/* gtkemojichooser.h: An Emoji chooser widget + * Copyright 2017, Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GTK_TYPE_EMOJI_CHOOSER (dino_emoji_chooser_get_type ()) +#define DINO_EMOJI_CHOOSER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_EMOJI_CHOOSER, DinoEmojiChooser)) +#define DINO_EMOJI_CHOOSER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_EMOJI_CHOOSER, DinoEmojiChooserClass)) +#define GTK_IS_EMOJI_CHOOSER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_EMOJI_CHOOSER)) +#define GTK_IS_EMOJI_CHOOSER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_EMOJI_CHOOSER)) +#define DINO_EMOJI_CHOOSER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_EMOJI_CHOOSER, DinoEmojiChooserClass)) + +typedef struct _DinoEmojiChooser DinoEmojiChooser; +typedef struct _DinoEmojiChooserClass DinoEmojiChooserClass; + +GType dino_emoji_chooser_get_type (void) G_GNUC_CONST; +GtkWidget *dino_emoji_chooser_new (void); + +G_END_DECLS diff --git a/main/src/ui/avatar_image.vala b/main/src/ui/avatar_image.vala index 846bf0ff..d98e5baa 100644 --- a/main/src/ui/avatar_image.vala +++ b/main/src/ui/avatar_image.vala @@ -40,11 +40,11 @@ public class AvatarImage : Misc { Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height); Cairo.Context bufctx = new Cairo.Context(buffer); if (idx == -1 || current_avatars[idx] == null) { - set_source_hex_color(bufctx, gray || idx == -1 ? "555753" : Util.get_avatar_hex_color(stream_interactor, account, current_jids[idx])); + set_source_hex_color(bufctx, gray || idx == -1 || current_jids[idx] == null ? "555753" : Util.get_avatar_hex_color(stream_interactor, account, current_jids[idx])); bufctx.rectangle(0, 0, width, height); bufctx.fill(); - string text = text_only ?? (idx == -1 ? "…" : Util.get_display_name(stream_interactor, current_jids[idx], account).get_char(0).toupper().to_string()); + string text = text_only ?? (idx == -1 || current_jids[idx] == null ? "…" : Util.get_display_name(stream_interactor, current_jids[idx], account).get_char(0).toupper().to_string()); bufctx.select_font_face(get_pango_context().get_font_description().get_family(), Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL); bufctx.set_font_size(width / font_factor < 40 ? font_factor * 17 : font_factor * 25); Cairo.TextExtents extents; diff --git a/main/vapi/emojichooser.vapi b/main/vapi/emojichooser.vapi new file mode 100644 index 00000000..f29e52d3 --- /dev/null +++ b/main/vapi/emojichooser.vapi @@ -0,0 +1,7 @@ +namespace Dino { + [CCode (cheader_filename = "emojichooser.h")] + class EmojiChooser : Gtk.Popover { + public signal void emoji_picked(string text); + public EmojiChooser(); + } +} diff --git a/qlite/src/delete_builder.vala b/qlite/src/delete_builder.vala index e7e3b784..45e09dc7 100644 --- a/qlite/src/delete_builder.vala +++ b/qlite/src/delete_builder.vala @@ -53,7 +53,7 @@ public class DeleteBuilder : StatementBuilder { public void perform() { if (prepare().step() != DONE) { - error(@"SQLite error: %d - %s", db.errcode(), db.errmsg()); + critical(@"SQLite error: %d - %s", db.errcode(), db.errmsg()); } } diff --git a/qlite/src/insert_builder.vala b/qlite/src/insert_builder.vala index b66464a6..8b14b33f 100644 --- a/qlite/src/insert_builder.vala +++ b/qlite/src/insert_builder.vala @@ -74,7 +74,7 @@ public class InsertBuilder : StatementBuilder { public int64 perform() { if (prepare().step() != DONE) { - error(@"SQLite error: %d - %s", db.errcode(), db.errmsg()); + critical(@"SQLite error: %d - %s", db.errcode(), db.errmsg()); } return db.last_insert_rowid(); } diff --git a/qlite/src/update_builder.vala b/qlite/src/update_builder.vala index f675a85c..0bf83aee 100644 --- a/qlite/src/update_builder.vala +++ b/qlite/src/update_builder.vala @@ -94,7 +94,7 @@ public class UpdateBuilder : StatementBuilder { public void perform() { if (fields.length == 0) return; if (prepare().step() != DONE) { - error("SQLite error: %d - %s", db.errcode(), db.errmsg()); + critical("SQLite error: %d - %s", db.errcode(), db.errmsg()); } } diff --git a/qlite/src/upsert_builder.vala b/qlite/src/upsert_builder.vala index 54ba9924..6d29b288 100644 --- a/qlite/src/upsert_builder.vala +++ b/qlite/src/upsert_builder.vala @@ -100,7 +100,7 @@ public class UpsertBuilder : StatementBuilder { public int64 perform() { if (prepare_update().step() != DONE || prepare_insert().step() != DONE) { - error(@"SQLite error: %d - %s", db.errcode(), db.errmsg()); + critical(@"SQLite error: %d - %s", db.errcode(), db.errmsg()); } return db.last_insert_rowid(); } diff --git a/xmpp-vala/src/core/stanza_node.vala b/xmpp-vala/src/core/stanza_node.vala index 0544cc1f..6ef3f0aa 100644 --- a/xmpp-vala/src/core/stanza_node.vala +++ b/xmpp-vala/src/core/stanza_node.vala @@ -333,6 +333,9 @@ public class StanzaNode : StanzaEntry { internal string printf(int i, string fmt_start_begin, string start_empty_end, string start_content_end, string fmt_end, string fmt_attr, bool no_ns = false) { string indent = string.nfill (i * 2, ' '); if (name == "#text") { + if (((!)val).length > 1000) { + return indent + "[... retracted for brevity ...]\n"; + } return indent + ((!)val).replace("\n", indent + "\n") + "\n"; } var sb = new StringBuilder(); diff --git a/xmpp-vala/src/core/stanza_reader.vala b/xmpp-vala/src/core/stanza_reader.vala index 72aa0b5f..c90390b5 100644 --- a/xmpp-vala/src/core/stanza_reader.vala +++ b/xmpp-vala/src/core/stanza_reader.vala @@ -63,14 +63,14 @@ public class StanzaReader { if (buffer_pos >= buffer_fill) { yield update_buffer(); } - char c = (char) buffer[buffer_pos++]; - return c; + return (char) buffer[buffer_pos++]; } private async char peek_single() throws XmlError { - var res = yield read_single(); - buffer_pos--; - return res; + if (buffer_pos >= buffer_fill) { + yield update_buffer(); + } + return (char) buffer[buffer_pos]; } private bool is_ws(uint8 what) { @@ -82,37 +82,55 @@ public class StanzaReader { } private async void skip_until_non_ws() throws XmlError { - while (is_ws(yield peek_single())) { - skip_single(); + if (buffer_pos >= buffer_fill) { + yield update_buffer(); + } + while (is_ws(buffer[buffer_pos])) { + buffer_pos++; + if (buffer_pos >= buffer_fill) { + yield update_buffer(); + } } } private async string read_until_ws() throws XmlError { var res = new StringBuilder(); - var what = yield peek_single(); - while (!is_ws(what)) { - res.append_c(yield read_single()); - what = yield peek_single(); + if (buffer_pos >= buffer_fill) { + yield update_buffer(); + } + while (!is_ws(buffer[buffer_pos])) { + res.append_c((char) buffer[buffer_pos++]); + if (buffer_pos >= buffer_fill) { + yield update_buffer(); + } } return res.str; } private async string read_until_char_or_ws(char x, char y = 0) throws XmlError { var res = new StringBuilder(); - var what = yield peek_single(); - while (what != x && what != y && !is_ws(what)) { - res.append_c(yield read_single()); - what = yield peek_single(); + if (buffer_pos >= buffer_fill) { + yield update_buffer(); + } + while (buffer[buffer_pos] != x && buffer[buffer_pos] != y && !is_ws(buffer[buffer_pos])) { + res.append_c((char) buffer[buffer_pos++]); + if (buffer_pos >= buffer_fill) { + yield update_buffer(); + } } return res.str; } private async string read_until_char(char x) throws XmlError { var res = new StringBuilder(); - var what = yield peek_single(); - while (what != x) { - res.append_c(yield read_single()); - what = yield peek_single(); + if (buffer_pos >= buffer_fill) { + yield update_buffer(); + } + while (buffer[buffer_pos] != x) { + res.append_c((char) buffer[buffer_pos++]); + if (buffer_pos >= buffer_fill) { + yield update_buffer(); + } } return res.str; } |