From a6e74ec8080d322c21bc845dbf957293b5a276a4 Mon Sep 17 00:00:00 2001 From: sunglocto Date: Tue, 25 Aug 2026 20:17:23 +0100 Subject: [PATCH] Rest in peace Dolly Patron --- README.md | 6 +- assets.go | 67 +++- assets/cap.png | Bin 0 -> 287 bytes assets/client_icons/adium.png | Bin 0 -> 783 bytes assets/client_icons/anotherim.png | Bin 0 -> 196 bytes assets/client_icons/bitlbee.png | Bin 0 -> 1618 bytes assets/client_icons/blabber.png | Bin 0 -> 246 bytes assets/client_icons/bot.png | Bin 0 -> 551 bytes assets/client_icons/cheogram.png | Bin 0 -> 1129 bytes assets/client_icons/conv6ations.png | Bin 0 -> 1674 bytes assets/client_icons/conversations.png | Bin 0 -> 1754 bytes assets/client_icons/converse.png | Bin 0 -> 5215 bytes assets/client_icons/dino.png | Bin 0 -> 1850 bytes assets/client_icons/gajim.png | Bin 0 -> 1652 bytes assets/client_icons/isida-bot.png | Bin 0 -> 1590 bytes assets/client_icons/jtalk.png | Bin 0 -> 1816 bytes assets/client_icons/kopete.png | Bin 0 -> 1397 bytes assets/client_icons/leechcraft-azoth.png | Bin 0 -> 1699 bytes assets/client_icons/mcabber.png | Bin 0 -> 362 bytes assets/client_icons/miranda-ng.png | Bin 0 -> 1067 bytes assets/client_icons/miranda.png | Bin 0 -> 1087 bytes assets/client_icons/monocles.png | Bin 0 -> 1143 bytes assets/client_icons/movim.png | Bin 0 -> 1752 bytes assets/client_icons/pidgin.png | Bin 0 -> 1879 bytes assets/client_icons/pix-art.png | Bin 0 -> 1847 bytes assets/client_icons/poezio.png | Bin 0 -> 1363 bytes assets/client_icons/profanity.png | Bin 0 -> 1492 bytes assets/client_icons/psi.png | Bin 0 -> 1519 bytes assets/client_icons/qip.png | Bin 0 -> 1423 bytes assets/client_icons/qutim.png | Bin 0 -> 1897 bytes assets/client_icons/spark.png | Bin 0 -> 1583 bytes assets/client_icons/swift.png | Bin 0 -> 588 bytes assets/client_icons/tkabber.png | Bin 0 -> 1234 bytes assets/client_icons/vacuum.png | Bin 0 -> 1351 bytes assets/client_icons/vk4xmpp.png | Bin 0 -> 1857 bytes assets/client_icons/xabber.png | Bin 0 -> 263 bytes assets/client_icons/yaxim.png | Bin 0 -> 1316 bytes debug_pac.png | Bin 0 -> 17436 bytes debug_psi.png | Bin 0 -> 2577 bytes debug.png => debug_snit.png | Bin go-xmpp/stanza/caps.go | 26 ++ go-xmpp/stanza/stream_features.go | 13 - gtk-helpers.go | 246 ++++++++---- gtk-message.go | 167 +++++--- gtk-xmpp-markup.go | 5 +- helpers.go | 27 ++ l18n.go => l10n.go | 44 +- main.go | 485 ++++++++++------------- style.css | 3 + types.go | 5 +- xmpp-delay.go | 30 ++ xmpp-retractions.go | 13 +- 52 files changed, 675 insertions(+), 462 deletions(-) create mode 100644 assets/cap.png create mode 100644 assets/client_icons/adium.png create mode 100644 assets/client_icons/anotherim.png create mode 100644 assets/client_icons/bitlbee.png create mode 100644 assets/client_icons/blabber.png create mode 100644 assets/client_icons/bot.png create mode 100644 assets/client_icons/cheogram.png create mode 100644 assets/client_icons/conv6ations.png create mode 100644 assets/client_icons/conversations.png create mode 100644 assets/client_icons/converse.png create mode 100644 assets/client_icons/dino.png create mode 100644 assets/client_icons/gajim.png create mode 100644 assets/client_icons/isida-bot.png create mode 100644 assets/client_icons/jtalk.png create mode 100644 assets/client_icons/kopete.png create mode 100644 assets/client_icons/leechcraft-azoth.png create mode 100644 assets/client_icons/mcabber.png create mode 100644 assets/client_icons/miranda-ng.png create mode 100644 assets/client_icons/miranda.png create mode 100644 assets/client_icons/monocles.png create mode 100644 assets/client_icons/movim.png create mode 100644 assets/client_icons/pidgin.png create mode 100644 assets/client_icons/pix-art.png create mode 100644 assets/client_icons/poezio.png create mode 100644 assets/client_icons/profanity.png create mode 100644 assets/client_icons/psi.png create mode 100644 assets/client_icons/qip.png create mode 100644 assets/client_icons/qutim.png create mode 100644 assets/client_icons/spark.png create mode 100644 assets/client_icons/swift.png create mode 100644 assets/client_icons/tkabber.png create mode 100644 assets/client_icons/vacuum.png create mode 100644 assets/client_icons/vk4xmpp.png create mode 100644 assets/client_icons/xabber.png create mode 100644 assets/client_icons/yaxim.png create mode 100644 debug_pac.png create mode 100644 debug_psi.png rename debug.png => debug_snit.png (100%) create mode 100644 go-xmpp/stanza/caps.go rename l18n.go => l10n.go (81%) create mode 100644 xmpp-delay.go diff --git a/README.md b/README.md index ad41ac2..0391da0 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ an XMPP client +Experimental XMPP client written in Go, with the GTK4 UI toolkit and the Fluux IO XMPP libraries. + icons are from Psi+ ([https://github.com/psi-im](https://github.com/psi-im)) -additional icons are by Mark James's Silk Icon Set [https://github.com/markjames/famfamfam-silk-icons](https://github.com/markjames/famfamfam-silk-icons) +additional icons are by Mark James's Silk Icon Set [https://github.com/markjames/famfamfam-silk-icons](https://github.com/markjames/famfamfam-silk-icons) and the [Haiku project](https://www.haiku-os.org/) + +pepper & carrot default avatar is from [Pepper&Carrot](https://peppercarrot.com) - created by David Revoy, licensed under Creative Commons Attribution 4.0. Change made: cropped and made black & white diff --git a/assets.go b/assets.go index ea162f1..7cecf77 100644 --- a/assets.go +++ b/assets.go @@ -7,7 +7,9 @@ import ( "sync" ) -//go:embed debug.png +// If you want to change the default profile picture used when Identicons are disabled, you can change the below line to an image in the repository and recompile Lambda. + +//go:embed debug_psi.png var defaultAvatarBytes []byte //go:embed failed_load.png @@ -159,6 +161,53 @@ var solarBytes []byte //go:embed assets/timeline_marker.png var timelineMarkerBytes []byte +//go:embed assets/cap.png +var capIconBytes []byte + +// client icons + +//go:embed assets/client_icons/monocles.png +var monoclesCapBytes []byte + +//go:embed assets/client_icons/gajim.png +var gajimCapBytes []byte + +//go:embed assets/client_icons/conversations.png +var conversationsCapBytes []byte + +//go:embed assets/client_icons/dino.png +var dinoCapBytes []byte + +//go:embed assets/client_icons/blabber.png +var blabberCapBytes []byte + +//go:embed assets/client_icons/profanity.png +var profanityCapBytes []byte + +//go:embed assets/client_icons/poezio.png +var poezioCapBytes []byte + +//go:embed assets/client_icons/mcabber.png +var mcabberCapBytes []byte + +//go:embed assets/client_icons/converse.png +var converseJSCapBytes []byte + +//go:embed assets/client_icons/pidgin.png +var pidginCapBytes []byte + +//go:embed assets/client_icons/anotherim.png +var anotherImCapBytes []byte + +//go:embed assets/client_icons/movim.png +var movimCapBytes []byte + +//go:embed assets/client_icons/yaxim.png +var yaximCapBytes []byte + +//go:embed assets/client_icons/psi.png +var psiCapBytes []byte + var clientAssets sync.Map func loadAsset(key string, data []byte) { @@ -234,6 +283,22 @@ func init() { "update": updateBytes, "solar": solarBytes, "timeline_marker": timelineMarkerBytes, + "cap": capIconBytes, + + "https://monocles.eu": monoclesCapBytes, + "https://gajim.org": gajimCapBytes, + "http://conversations.im": conversationsCapBytes, + "https://dino.im": dinoCapBytes, + "http://blabber.im": blabberCapBytes, + "http://profanity-im.github.io": profanityCapBytes, + "https://poez.io": poezioCapBytes, + "http://mcabber.com/caps": mcabberCapBytes, + "https://conversejs.org": converseJSCapBytes, + "http://pidgin.im/": pidginCapBytes, + "https://dev.narayana.im/narayana/Conversations-Classic": anotherImCapBytes, + "https://movim.eu/": movimCapBytes, + "https://yaxim.org/": yaximCapBytes, + "https://psi-im.org": psiCapBytes, } { loadAsset(key, data) } diff --git a/assets/cap.png b/assets/cap.png new file mode 100644 index 0000000000000000000000000000000000000000..0cd803115831933aa171497cfe9c1af983035f86 GIT binary patch literal 287 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7SkfJR9T^zbpD<_bdda}R zAX(xXQ4*Y=R#Ki=l*-_klAn~S;F+74o*I;zm{M7IGS!BGfnlSki(`nz>9?13xeo;} zv_5=%*FAE>d;TaFo+hr@F^7*WT68ffR*;VjtC_++*I%;VB_)4A35ooCkCWbnaY;irnTI&T8E zOI>-aSZ)xKs2~_{v&QXi-lw?rhP_V5Hgq$KGgf}Kd3CWNQ{=;)mqlF5f3zyM>~5bB p9g)Qv?fc*aL+Ey$kNyXwKi1sqew|XR$-uzC;OXk;vd$@?2>@U0Z=wJI literal 0 HcmV?d00001 diff --git a/assets/client_icons/adium.png b/assets/client_icons/adium.png new file mode 100644 index 0000000000000000000000000000000000000000..8a7a9d513f78ff26136a5e33a24183e412d08265 GIT binary patch literal 783 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!tou6epRhFF|#4UY8)2^9Dj z^tn9v`rS~j-;QA-8w0p_bTnPh&Yn5z#x%tn&C6Q9D2SwVPrA|Ev0&krZvI($UANZR zXJ~#&*|t=uacYLZH)W2_7whtt-phO!>${qrzvj$Lu#@$)mH_O!AeDifM`;@=_RGb#Iz>^jHg zCC`$|+pMxs#CYG29||4Ccg%{mcg#uYzp}Wld5>5&!%<1cjlDd~E+?nEoGVLKdtw@wX-e#%rOWWheLT@-lBPNlDJ$>L2_v;FkQvCwe-c+UFErx?7-K%HXY|p#G)K z=w!;{Cnlw5ZlAC))z#aa|MQO4@$Urpp9la_COTq@s_^mDeg(ZZZHA#3Iuov7B-`_k-Z zo?N+_NkQGI|D+C|%C;g=U7KRJX(y+as7+XRb}q~0&o&ct+gMAMFvZvMDNNrP$HB$* zsgbEXo9p2Vjn*3h>{-S;x*o?WnwSc>3QsGzcHci$q3XDmwvH$B zX`#f+lMf63HUD-ul&Ta^{kLa|*V6ccf(_T>>nBC6y|+AQ<)NMG&nxRS4GcbT{dSe= sT{a_Eaei#t?34SZ6*J9L3jZKod#LE&2TRYR3=9kmp00i_>zopr0O$E;t^fc4 literal 0 HcmV?d00001 diff --git a/assets/client_icons/anotherim.png b/assets/client_icons/anotherim.png new file mode 100644 index 0000000000000000000000000000000000000000..4bdc43ba42cf0da23c2366c3bd749f374db24c60 GIT binary patch literal 196 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7x;92dRwR!K2dz+cmHp(xLekkf8m~(qdcYb{%clV==t^(1dN#+NvHn=+7 zw>)=gQEQ$B`|2HGb(h#rh|WI0(B;bi_Dn|my9PSD;-c3vFfcH9y85}Sb4q9e0ON2= A?f?J) literal 0 HcmV?d00001 diff --git a/assets/client_icons/bitlbee.png b/assets/client_icons/bitlbee.png new file mode 100644 index 0000000000000000000000000000000000000000..217c11feb948fd271d39a7191e3f1fed85e39261 GIT binary patch literal 1618 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd0X7B(2LEJbB?bmINlzEYkczn4(CQH1NSS-z z?-ZY_ox8cZL+yQ0=)KiT1G7$LdvR56y;h;~_~PD0X&S97ZHy-EN%wFOPMqkz=!v1n z#e|-iiGizDf0A$szf=9%?$_CMiK#o!y#MoYZSA>x_rL#@tCnrC<5YM!rQyQegWr={ zoLaVgFTK1*=I#3Dzn<7lyruW8>uk^KgLYvTC+GQ=E#WtJ{k`+&QSR)8d4A7gG&ld_ zE$*=^iwK$!aqU#{-pEBc=L`ZWo-Ws#&$*(FE&BL(HaCtNuPx0FrOb9q`!{3fg}$$P zUS%JPX8P`w4mUVIlTC!_!@nu4zn1+B)hYuMxVY5&hOi^^p4%tRWW@cx^~sa1zjt% zPaF+8z*MblxrpJ(-6zT#h6gUq^wz#n^J0fWBm<{{_o z*PcB;?bjWiwK9=UPiYEW-!}D5>*6`)DY;+7R+jf}+Ls&KeXq{#g>BHxD~wNg9?W=G z;rT%#;oG^JRcV$7qRww+UF6H)c%M~v$$rKeIrf!yb6Eo}FBL0nXZ*rE``2fG#znu~ z<&&;6%FL+WF!{*uulc!bIm>PYap}9V75_OOWFp6^d@Cqom+!2`GA5lsy}-@w3?6KI zcUUs4=Qn-x@G#@CD{Op=?AJ+n%P~6aQQGI{a!t*BvkbpN+o>$>9Wf1`8b!nA{N5Rv z%l1aB>$<9rw?XHhA}0&g?Gh7tGt6c+OE9FjeokAZe_)z$w0oyD(~F?5)<3kIB+~ef z=dt`Zyq@eoY2y1vb)iW+ExcsflWzBxXtoA<&ahHn-d>gMm<^VH~H zS&h&sjhoiaN$nT59((;f_1`WUg5UO+P8ThaLBZWFf2d(>FFiKUBPQP zAKz(>%boh(oqgeTbLsXE_ZQDiD?ROBvFOgq+{4q3JXOEzf6cpuS2|Za*OX~>*&z$x z8k1~=Lz%})a{Ms++oye^N$;{RJ$CtZpHOY`E>nx6)^O>^%}7IoP%{pQZx9lx$5_4)jDy1M9LjzWWq>!iMG(er*c1lulq zm(+4=rN{GC<&V5Kzn*`6Pr%0~Ri?bT-=1u3u{b`by;CvtgKT)D+<}1m3pYCP2i*$Z z7o|9}F=fge9i_v&T(*}8dRtjHNI!6#*cGsFijZX_SDnMn+DKh1rVUzVjbfFtlNQ`| z54At-cW36R+NIS8r@x&3H}>-bA$9f&>%Xxa^L?xK+}^=nzcl`1L2ast`QyC0LpNHC z0vlFd`r7iExhthh#G^C9d!c{eh6sV?6S`cVc%qzt+icV<)U(t-{W-c})t%M93t#`@ z$={S;_DI@!DzB`Gklm)D_PDztNB6IjONhI?Nvgj~EM{MTg#OxapGneH)@X3yuU z;hgDuMo5I`f&9aIw-wbhr(8E)@A1kw_>R?^X4MybMg?x#|3sbI{vK1=*ZC?*Y({W_ zLw|^Kmmz2Qi+z9Xm0I3~PK}+KX%p+VpI5@OJAR&Hb%52R2(C*9I06}dE;RkJNz7#S zo@aCUiW5$)x7Gjk(5pQ1ykX|rLvz!);u?fmZn)GZ{GV^I)?D;a?%FF)o;SYykaM+@ zqtmKt)}ok~heh_;$ZoNGVQ0&eaOw0(<(EExFKdX1o^1PX`s>8k#etL7NSOETUnV5~ qHfDb8^x#{kuAjPmciG(k@s=wl9;zui(a*rZz~JfX=d#Wzp$Py`)e-3c literal 0 HcmV?d00001 diff --git a/assets/client_icons/blabber.png b/assets/client_icons/blabber.png new file mode 100644 index 0000000000000000000000000000000000000000..d83cbd001396ecd5896b2761904ce2ca2f0aa821 GIT binary patch literal 246 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7_IkQFhE&YiI?)zdFN36fbC7rk!!XZI~ZJD*-bvH9BNQeb2>EVlj{3| zVr5NV%URK_0v=maK5T01v~^dx$du{pmi_h+n<(RZSh!)%uuw}vyHGwU=bk<}jdT||P*U->lnX28jhU>b! z!-fbG7nXxRoc7NtHk-Ysa_3F{wnU3LGxPV&e5k@AWAW}>nQ?)IO#eNhCq--W>NX= zJ5TJyz2U1vrzr?sJ8o=DkLtS7-^wC9C&p++bH{U#-!(TD4 z-s#954H2#dO-8Cq+DyEGUW7#KWV L{an^LB{Ts5rR@BZ literal 0 HcmV?d00001 diff --git a/assets/client_icons/cheogram.png b/assets/client_icons/cheogram.png new file mode 100644 index 0000000000000000000000000000000000000000..f90c4f089582925b493095c072690874cdf86e52 GIT binary patch literal 1129 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4rT@h1`S>QUWq9!W`@ff0e?B_#?Ze|=zkmF_u=MjJ@lPxK-nRFd`wf9Ko1tz&rJ!0^1D;dvv&n+d!>n;Bj= zF}$8-@c!MMKd-O--VpO?vclU5l5dyVersiT-^TR*?DQYMfBgIO^w0bIe|~-W*Us>% zm-qADq90r0er!wlb#BtXPKIw?48M99{&X|^>|yxb%ka04;a?BK?{0=4oeW>w89uc! zyl-K6)5P$yk>Pm{!>=xe@68M^yBU6VFnnodc+<%6s*B-UJID*~rz*YQUHrL?;o~Zw zFK=%A0h|1zo#D$oqc0yG{C#=p&#$llPj>u1JNehw*ME1V|6c9>tB>dF(-VJsSbugh z{ODo+*~RelKwm}U|4!ol-OKR%(yV`n%Kk1k z{M*a?r-$KBF9`kZ0XZAwa)wbrL`=z>rW9FR zQ-51N#zc`hKfgupZt+n0#?ZGQ(8EQ>adp6sDSk&BU6u#FkWE%H53KGqsf zuAIyxzGj0`;)Is`-(f*6E-Q6@nauOhSd+QNMc|05cjn#q>ts(Y=2g7DQu?&CiuPf9 zgXgZ+4b{_U-{5(u-TY*4&+S!?9Wq5B*GkSFXq#ukFA)~iyQuz|i2X^<%1bU>rE1nu ztcDEfX&P?-vXr*DK9SgWWZMk6-vRTr4>IRTe2bsAUvBZk?5+=>VD)tMb6Mw<&;$TL Ch2{zX literal 0 HcmV?d00001 diff --git a/assets/client_icons/conv6ations.png b/assets/client_icons/conv6ations.png new file mode 100644 index 0000000000000000000000000000000000000000..9d4ad0f9d035c88dff424f5ffb8e129b6a2abe09 GIT binary patch literal 1674 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd0X7B(2LEJbB?bnzKu;IPkczn8sg(i2;UdTQ z-B`3(R65$YI+YeB+!ZKY)3^I-l%uSy>~*13!}qKy!YZ8BdI~~QT#xoeUE%9`6t%pe z`r^u-j&ptCjI^`RO=bM|zby<7{D7d_JqWx4RdWAh9v zwty8^ryb#BP`htXDu0vVK(YCPVa*ox_6AF7tGGL4YrY2_;0Y;xLop=L{Hr+P8EjJ z)7lIEOo{5yzL)a$-VAZ>gjK0$F3xrNz^MC1blHD#(-Mn^k*g*xw$nCiTYS=tIdhrm z=j3yYExqYC|DN|?Sf*(%binwSREP!BgSw`|#WjY4J3Ov_<2wEC@(b^*49GmC|=L+H`alG$VewSSO{-WHHb;1YLZcXI)E4~#q%+t7Q z6Dh!X$>XTdzG-HH`<8!6d2O5ewN@#hBKJ_t_4^a{t1sm}wl;0v>1-RWiCc@*69s~{ z>K=$apsVun&jG_9yPvgq9h@)mrle@Lx0C-t_qt=E%kxEgKbTcj^OosMOfCJFl_RtB z*Sj48NfFL7tGfH{Waqwq{(If&u6rr(Oyw+xigT*=7PQBEw z8MhS|7wt=D2vRN=OX{t$+4c3=fv4A(#g*Pkcoc4N?U3`yUDIRicUVdIGqiC=`L(`h z+)?ZQ+$mka+5VR$XTh{vp7(AG=fp~yoH?`Oz}&6>Jky%*yFIz<^K`}c6NxWh*UDbl zpjX;zo@ZO^IrH_^#_0ExAHA8_X=>m*^`+d8Gpi~$PcM7Q`?WnXZ{BOWau>_Ylh@VP z9H=u0yB(x__;>xu zygp&7c1WG_(ufqFi8p@p?}af8Kh-RhSQ_bI5?*UtNQa^2&_eRWZb zyifJW%l^rnQx-XGcD(&E&-YvY&pN+nm-VZTNq-k@ir+Q!?5a#>J~N*S_q@FXKIu+B zdp(3PPvVBrmkZnV)Hl!m$NAyu=B#@yzB0$Zi7(oC_H0qMM%;PD+playR?Gh2&VI%9 zxaH9$gTS?FLCa^PJmA)4#z7#Ntda;Zjc@Tp6M$_<7Q9J>Qr6Eb5MJ(=1Z8U!L) zxWrf%diAcnyvXRvs^qKNn!{q2Jvj8zx5N0BUwYO5pJ!)(e^zt<_qmI#@ME`63j@c*Q#V^Y zGU89aN?Vh9_v0y5$z?^J@g+;u_}_W@s`+=_x4y4X_g?hwl^2bRu4J@y?bf%^e7Egm zSEudb-p;Ap0&=reLT)erUhwdHZ>!eOaPrlu{OS)?56@wHV?r6J<0L)@BQ zKi8}=YIY5aTp5#be|um?{^yGD7Y7c{BQ!}-8oQjF~XdjtqR(06p`21{hi+?;DirmvLpOT$?dv5vl6LZVef8_h^(YZ6H zgIA^W%UjP^1)iSPMGMv6b~wJ7yO zR$olJBh~cCrmyVDm!xQJm=1HuX8caz2pA1ZnNM0>hU|b>DAh*oU7Sd z5;=N4hMys;_*Q)!yWi+U{T5e9ah=cdVOY*u#2k%EA`I%fE9T zS4T~@>b%wxx%Qs(YPK1IF>7ZD@;Gz`EDcg(u$V1%bB-^=l~z-wS56G4lSAK5JI=rq zzV#GSIAhM=j00(}-9=qHEaD2MTCCfu9HLd(;*X+qTK;*k*w{E8{zRpUIh@IcIzC4@+&povr_xldev%UtD=+exlKYJQD$d zn|q8EgcP2>Ej}H5k8Oi>obly%7JiSGT$*1XdXp!vd^zoe^qIAWvF?wapQ?HEoPlp{)>dcrhR@4(>7~nt8xXi+M~Dn)Xv(*bHLnd`)%_qFR^tQwdyew4Vzl7 zTYYN1{$rhI`1!rFOTPLU{++%nbR;-lJfjsusS zw7+aQkdpPi;?drNCo^RpK6xaoda^b8jt3^p0L18QB`Q`PsA^zH$$4_;Ni>EZlW(!;Wrk)rZYN eo;x3~M9S&0}B?QS)?h45^s&W^VPC(5s~% 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 literal 0 HcmV?d00001 diff --git a/assets/client_icons/dino.png b/assets/client_icons/dino.png new file mode 100644 index 0000000000000000000000000000000000000000..951feb5cd0d6ce25bafe93f1f734fefe8c072a1e GIT binary patch literal 1850 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 literal 0 HcmV?d00001 diff --git a/assets/client_icons/isida-bot.png b/assets/client_icons/isida-bot.png new file mode 100644 index 0000000000000000000000000000000000000000..b99883d47b51a8c3093ad0cc9636430cf2f235a4 GIT binary patch literal 1590 zcmeAS@N?(olHy`uVBq!ia0y~yVBlw9U=R>sV_;yIdF-b<0|V<{PZ!6Kinz6*)h1Ue zMgE^L_%th3N~(WKneDW=DcdGb*{pTtn_6d7pr&R}h?s4UyYMHOrMYWY=w_9;nQC|~ zKX}bkGs)PeYL1F`|J%K{XYRFkzWM+Azt1z@&-}f2ezpDgd++&w-1oWdRj<|Dc9`}2 z1hyGFA~#y-mh>5U9>{h$e{(}+M|H-Obs_s39>>XKT}chg`m)C>^MqmOjQXszN$H!s zAMNS-u!3=#_mn*I;p0o4kIiICT1_e%OlQ(j2u#M9ypUM=iwo^Xz6M0X=K5wkFdl=~4VZEnR{Dq!&)iSAtiiz=oD?=vkVC6oPGR?9n zWYxZ;Uc0l?F266*+S>i)3EQT|?AMK3UixxPG@tMy>VL^!?*(r+MKzihKH^-~;(77< zDWm^vOl$Z|J{<0_P}uW?TmO~tmi(8$`{OUFU#&5HQDY z^xCrXmB($Hb?3o7gL&&xnVz_4ZT0`bcyitK>#WNdE=8Dr^xC&#x;rQ5 z+b5AqvRRYE+7$n58hrP>5%1i=zx%^_A1jWE2w$ChGDqTWJX&Jv9AD36Jv;yBrHfUQ z88>`%a%^ax^W0tQ3%lHrE$nC42ChADe&5|U+4~&cyzYLuS*DS-bz-8MaD#2Dss1C; z1FJ5Cq}!=JFw9CmEb!>On2gN4@7jr%yj)M^tYh)}oh{|N*_`3x^;GxiA`IeOS6i0p z=Zd_!*zn(9DLp#YYr!VV&s!ucg03guzv=ya)3YyEUaI^&F{gN*;Q^Q2?@4_^8_G*! zf6KdVj@;&LFQ3%7qdzp?=|97#ta)d`?y;0DnKsp$XYm=a=bbD|9Gn90&X!f#7!V{q z{hH^Dc|7?#?3DeHk@-pjHRw~c+ zYUNf7I@+5nsv45K=3{IA-9L@*JW7u)K6BifpR-y1ky4Drm9ItuS(B33ca-;jlUwE# zWu@(Z{qnB4J$KS7`eb~Z*8G-b|8A^cFgM>j=($ztw-f2X$JVP}eHXLJp|Y;?xs1Zn zN85gAl$t+FSh}9ST|_AL{sswmQT5K4-gk_NtNXa$7H4ww$NrYkRbRSBBv|$1nX);h zN;wa(lY+fNG;x?~h0+T4}F5gj2bPky?$My#(bG)dg<$rjNi8^hJXFW;CHqA*+zLrzD+y- z&bM#oag^w0s*cFnv%-DhG`p_Y?20+xw%)t%TJ2Mx@ym%JYMNCX^K!*+`fd^d&90WN R%?u0-44$rjF6*2UngESn3rzq3 literal 0 HcmV?d00001 diff --git a/assets/client_icons/jtalk.png b/assets/client_icons/jtalk.png new file mode 100644 index 0000000000000000000000000000000000000000..71f90b984f74eb34e64202eabe5a5463dd0afd63 GIT binary patch literal 1816 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd0X7B(2LEJbB?bnz`<^b2Ar)~`r&b4Cxh{JA zdWIrn&kMgCT{p35e{H`V{B+~~-u=h*6yB`U(c$NO^vPqX(h8l#z)7nZb)6>kYKSs% z*)0p~X<|8=VdQzmW$_ZjGAUnQlf?^rGox5nU(DF0*0Y>leeTQoZ{zPi4Uh(%bjQ-x5fc#NWx+utwZ-c=-iTpUW2Y04zm~w_eAp6DaQ|vRs z{ko^dFAlr3)XTK)ymD)i7(;!dw&{*PnOb>j#wV`-jDBJJaG^Lx%LpU_XTeiIWuHGptzCvGa@>gGp>xfqm~!&-Wa2L|T3- zZwZKO`K=N>ds5d&rXZc?3=(~>T_+#DzvNNaM#Vqj{~QaAI49Q~a1r+nx@1r|p=xTB z%>%xd6U>>Gq^Xr%EW2aj;x+vq(}~=Yp6CsamI$VHZINxas}gjsO}c7ScE}>@!;x!0*+cGBebf9m|DSz_W6G>A1x*vaX{={l zQZE~yva@;W*}k18-?6O7x*TRC%fPqwtIS=NfY-X67hf|LRmq;#sbx51DwTM2Vucj% z`r~ztucb=+H|HxiG_tKxCuDPFAE^9KvV z+oj=ZT9F@GHy*B$TVR*hx5n|&Cl0k~`K}H7Z|GePVt5~NUg)tUL;31h)jeGd-7lkF z+ciAW|61%@xq{Kwbm`+d-Y=_)KC3sRuw*E%EfQvWW9VlX{daoEoh-AS{qO!q@hE2f zH1wI_?>&X9VT=Ex>D-$crhR!8R^Z9t{d?J<7vz~@r&1{M=;#(Jz{9p^LwvD-s$gx3cP!t9;gt$skFa`q2B1&%+-Idhs5Yg zpXvW`{Kblb@L$>ukEU_u>_2p>FD>lJ7KStXXZ+i9yW2Q)_o__=+gknHXKRZ)s%@V5 zhnXS%)13UpV$31;lgeLyY-lQcK8we^A$z~JF}L;fE2*{0tY^Jux8CA#!*SscR+nTHRuvELbGuCnTX z-P;nk@+99`W90)TLVE1V2h0xlKRd$N;Pgz)zK5ZHTa1YRqTS|C3eG<;wJfN3@Oh!t z8;8huS8BKq{JXX9YxBh$Gc6_e-mKXF`u@82c@pdveD8VZ*6}S^yZP-uZEePHIWJ;w zigRt)oqGP$-^q-Ae?OKScR$J?YOcjIRhyyXe2`w1z=8A~S@TR7AEd1N;B3+4qZqwu z(QoF@T1L{0zova%Z`(2T*`67HKifzyuwM83a_kR=)YkNOCoI`6eE$1w@BV&fi;6!N z4lj<#eErt<(llj;xwCiAdv&mHOa9A>oNVI**`dGpF#A7Pdo$wnI;ZcGv!`h*&AOdp zmeXt7%<#r_*(~vm%<4MtJOAF;zVb-& z;r7(H&5x^&sjx^DoZcC+@b1)IS34E@HkbKY9o(V%)%ZDYjs2n^v75C!;&Q()OHN!g zea(c^9~FO1|7RJj%w(`?24h)c{R}zba~{qW3oe?~>%|#IH%*!u)4=U^@_x!4kD7TE zsdqdcX)cy9h-=c9YUI8Vgkt;%e*Gpg4nPW`mXJuiRK-xakf+xNWRpQK)RxEL1orm+*2hT{1RXl#>WX_Cr-K(9RmdF>?U6HtJ@cq*N1yXl4*DciReVcIR&fi^o s4W+ke?kb+wZrrpi{QM2cZQt)73w*Ni|1zopr0J-0Ck^lez literal 0 HcmV?d00001 diff --git a/assets/client_icons/kopete.png b/assets/client_icons/kopete.png new file mode 100644 index 0000000000000000000000000000000000000000..bad87a0b6838f65ae4602b58071dfe83c5556056 GIT binary patch literal 1397 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd0X7B(2LEJbB?bmo8&4OuM}%SDc7 zOK3@3@n%l3IO=N_Thp^|4eM*+i@tXP3>SR-%TN+Lap{(8x1!g2yj-wZY(s~VOKRVS z4t|%%CmKp)cI@50RCl}TrN%|hS2SN;c`#?5EQejtHl2%q|NVS2@BYu)^FQYn*G)H8 zKVNg`FndL=*eq{briK&g&s^p7E~QSZxaam;vDkC_-v0gkmJ^kW!gp+c;27U`F7~Y- z+YHyI5_g*?e4FD`u5s)56SZAa_twp3Oq>3!ZO3#*9gh?Kd0Z?%?$mi)+QRjg)AIY> ziF>W0|2yuVJ}0tOZQ9wx4EuVFL^rTF{JE&KvRuPx*@w;E>KF73bkp4x`e#Pne9w?( zbE|&lT&@j%aeJ=pW>a|YxAt>bxzb#Ad6UFN#`!aXB1C_QFI=}XU*_DItH<=Ef-hcd zH&*%GXXeLrVf~sn*EcdGyx(Q~%sb-I%9cE}$nHAto=tArzf2|nN=;{SQJwW}Q&K|{ z``1agGZ|*RDP~^Z_Mq;H-MVasU;4(!%+tR09-bMO`C-Q<#$OxGe7Nydn=4!*YQ|}X z%a1};m)kNdn*6EDh@Fciy=!KXPU#xMCfSbPTN2x}#T2I2U%D2?Q}D$7)1kE@cj_n2 z%$mCJ9(&u>bvsXJHD51%kt%a;%hSl_IiVU4PH&BjQD;^7YZv_f$R)ozADcB@>@)T& zzCFb%eV{R{`;XZ@9sM~WcZ?a1EJ=SYo6HzCsnvhajnB!SU%aX|FT9~6`A_UhXU4^s zFCVYit*CwF<=al<8ORY1HGf&)niCM>j zxpViY*e&v(o;^HxCO+cT!dXq-R~b(4{aI9IrnYS5-=A>|VG3G%7ni9wr1XA^F5=p@ zL7_SChrn0&7kl}Hggb65=+3g}Z=6?LtIvFSzUhOr)1JuM-0C?jE}ebven`7h@%KM| z?aUv9pUyu2ctTt2@|0#>E52d^2ls2|4gxvaE2B3R}m;CW#toVwr zFZ|!V+z|Vwv-sY7=Kau=y(M_J=DRz*W-_~GZ2RjUIj7M-efiU? z37nP8K~bl#S_qdpg|#o8zvfzj=9{;3+9iY1EDmhi(foP#M4^{?db%!q7w+Di>v%%t ztxa^e|D2cG&Q1Sl@krrpGTZXK{Kwxsy0zi>qVz2BQVHhCn|j3yXH-ny$G74KuYn3xx)ml~rR#Xb z%jNRi+^0(BhW+oH=1Lz4OS_@H>%UW9#uBS8JxR-ltm3PSGf#goO5E{EQFh@I?wzZ= zq^k90mS!z56l7d1<$HIp{AS3Q-jz1op1b7^Zv>w(SJW43!h?m?Lag?i{Yv4u)D_?4{Z-|{Z{vHVN>v`i9Wm6 zpOV(O7gQ3GbDi((^w&`Lz1Vj&BM3<|he<3fw<>e^afF>B05(^>ZHSY(4S) z%l{3B4yD}uYW2h=GOYI4p$8lKQ+2<_9}Vrfzw*Qeomb{rmv&tdePX_Ed0hQp-hyAQ Wt1GCh8d`0$I&OOIE*CK}+!>PP z`}P2X`p!9W#7X0d>7-6IsYiwF|U(_rPgV8iQYnw89FD~f4ptdxVT+p`^3vH zI$xiDeERgJ`J1HQPVo?5|2rz4{mkt3TD(hVXmk5i7ME?|0 z({56kDr%T1Xx(u1b<*aP$&InGo8za-sOQX-)rfSlc--py$nB7R`q!=`&Agqj4D`D` zow+FaRqa6CrF%*0NjkHSEZfj}p*^@Nbf5H%d9yE_tQOj_QE9V!jjloI%_UK)rJE;B zyshlcv}*s<(7^ah`=TzcpULzl&g8Y^GyMk-|IHCU^ta))dwjg!7RK8eLB*c@t|mUjQ+2#5*avtlP($a8gDuHT|B_RK3Y)nKit+wo>b z1IIBN?RP>Gif(Box@z>A#tXjSig07;TT|=)-74YjhIyN}2{U%zS+wy(M`Gr~w~w@! zG(_#5|6GanRJu9mqXt&v(CDWt9hBmig#-kvig+%b!R>H__oD7h*%e-wY@}@*Os_7N z`C8w`al_@Bfu+Fi;0pH>OpC-n&D~RYMflBuH3!lh<}ZIY zh}6dDXhyqp$%L@5|L5gNQGIpXUtRO;tt@-l(zga@E6UV+{Wqy~{uR5q{Ipujg!|u| zPuZMfdJvcr^X$fKmzrhU`uEORlAgUk>HWgLJZo$2a4FbNwXeM+rSze0-?jw1gn!3s z4*d}od{Ob?az~$iu zlVg--$>~b#DymNFNqZv{lvCbT!@7NzS5csyRm+mQSC8F!9MJ!!%I}jw$1;sv(ezY> zz51-Btjw=BH}uVxt(A>+Ibd@8OIK&!u75rgH&)0WusyU>Phq9%-$!XzGqcRP?<}qL zxcuSBl8YawL}d4vs08;uo@Bzm+^enN!=DEW?wopLZ;9lKX42B=C^X()2ffX1Yr4+E|u% z+V;JrRTAkEuH>5*N!uX^RCJMMeH}Kl23o1dgbZupzZhW zJMP{1I3P9r>gg-W$G6u9{e3C$>s)PFbKL@qIljeO>w@kyKb4H0$NY-_%K2P{Lu`v5 zZh83RK;pGRPTiF=(<4$m%yk@sKhJ#Deb$~qH&(Yi`o&SBTRrs>nSIgvTkhrAx?PgZ z{k^rf&hG8(+u65`+b)#9+x+=w;GAk*mlZp=-n`}Gzx|==;iSVcCNlhA*KD@`&)t}K WWL31nb6Exk1_n=8KbLh*2~7Yka!6?a literal 0 HcmV?d00001 diff --git a/assets/client_icons/mcabber.png b/assets/client_icons/mcabber.png new file mode 100644 index 0000000000000000000000000000000000000000..b8a7fe5c406520b41792b2e4151f8b61fe639e4f GIT binary patch literal 362 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s74Lw~PLoEE~PPX+s>>%J4 zeusr~r=Rcpb4&FZHU;oKI5aht(_Hh8+mvYn9-PnH8VsoJb*F?M2 z%|D-=o&R>-A9c^P^C!QqjZu+*e8lqj!{W^o6D9tM$X)%FD=VUK!Qe@y-Pupwn{;k7 z|EXm85G&%UvS5{oukzZsaCe4O$F-}x%68|!+~s%cc-SphZQsitvDXQ3!E0OUeC7?3e*_yQg-C=X&TXH%>VzEQG<|=g z|2sH0O}39+9Q#~w!kK#~1?1#DKggKRWuu(E1{tdod z%5dCXHFH6D&?~(|N*hm~>H8cc_qb=_D5j8%=FrIgy+eb(J*2=EN9kI&x{&X49N+H00X6J!ot6t@f7i{1=<-7hKnWKmXqQ?|a|#ma$$pJ}w{l zO?}DNi=vghA0?N|N2R6q%xTfxU$=hGxf5#c1*c8t@_5B9pRi?5@41JycI%DAOI(sy z{N~P|SSbChba@4LuXUv-cVOBUWxe&WCYz&NpHDbkS$Fy*kJlk@{<@eAMw^U-*sPxK zHZS(yXz=u6+mBdB-;0wCr#J*Wc)*oDPuikO`k74$ms4-PuVabhiC?w*j-G4UAkuT+ zXj-nwRkKrVXRq(+2<>mLFj91>JG?vQpq-#mNBOR%8l~4Vb!UpD58ZnGNon=j*E+RN zPuh9ka1(sZ|245))=9TOWVe=L$du?yA8G}}Y#tjvyVJJ&K*S!g#+n?xb{mZ*f3xP* z7e#$?V~_GZQaw_czCCNZs!*WittIB+=Wo90E_}VPLtOgW$!g=Dt;zFhvfjJa{rqw5 zPxNc~1q_d0iY*lnkNmdgsPd}qy5$#T+@;GH?A@1GMo*l*f@}7sR}UATl{0KPHfQVO z6P(793f~LYdcP=Byj-DXn9T8N_kp|imu9M@x$#fq{-$Dk&EDK|t-AW8y`}r+@G?C7 zbH{5*W88J_e@}05ubmYi_4CY!;<=ZqZ%=>v*zb$em!(VA&wjG) zaL^LliFXgLwZEdcdy%5_3A-#P{-CaApFb>`y7{frCR?r7}%K11ZpTIpoJ zDy@4NC*|rQ_n7+};**e_6uz(GSN#DyvsDwfGhVo4@@Jogna}aRsW~pY7C+iME4Wwq n`;H~scb&3${fC?34{xKN|H+ldbU!gLFfe$!`njxgN@xNAlivh> literal 0 HcmV?d00001 diff --git a/assets/client_icons/miranda.png b/assets/client_icons/miranda.png new file mode 100644 index 0000000000000000000000000000000000000000..7a8b6cb7ce777b077ebd18d276c81d7560615ecb GIT binary patch literal 1087 zcmeAS@N?(olHy`uVBq!ia0y~yU=UznVBip7V_;xd@NDrF1_l;3PZ!6Kinyu$zs+x# zOB}afB@^3UHmA$G$x3hO*^QU33A#kJUEEpf>ERxI<`B=`8CPDcIodfjC|>qu>$eMY z$_`4d^xkbM_Qd8iopHbq_5_xmV7ei*l6c*PW7gv+tj8l+PlP~CP{C~TsUn)MB@*g zv+K{zF@ED*YU35jrgiM<))u{&;lH>iFOSYiyur{it4&nm+Rt*yBBq20{Z$30=G^hT zT6w2*w@6}=#4Hz2@lJz1rIlh+CuM%R<2QfG@syc+dW0N{Tkf2Ryz}yOp7Gx;rF$k# z-J9ONWU+zJ$J1%|FNHr2Jbw00()+ync!@Y(<#we#OHU>QulmsNXv*tllV>f8RzJh; z$QAUlF7;{Il9;H3#W!!Rd9Z!X`m0^0)$Lm6s=S_?9{=YcyePFi?e4s3t-SLmd%cV; zJ1F^lOWVv&C%mIM2RcQGB3bLT%`{w)9DOj7zgMe}oyUM#M@ zx+3Kb_s96TttWC~+6r4^-(6V3Q5XKzd-CpkUj#4A4SJ(3*s*Rct8UDBdxLhVn}1Vu zRMWF=i)7H<*V&=vHJL2Np== zsrU0)Q^>RX=aMbf>{*ch-cdXBA+_F@0gzncorK4oK= zv!TsDb{F$93(f8IWnRnYRLzP$R+zPI_x-@|e`_z>F#KnzR^of;(zvgifq{X+)78&q Iol`;+0A6Vl5dZ)H literal 0 HcmV?d00001 diff --git a/assets/client_icons/monocles.png b/assets/client_icons/monocles.png new file mode 100644 index 0000000000000000000000000000000000000000..6197498a57da061790795f07820ca237e61350bb GIT binary patch literal 1143 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4rT@h1`S>QU~p-tSP zfkprS|7RElLoEdOue~i|U|`5C3Gxeux}8xc#9dIxL)$LPFppor&B&I&NXXmC*-N;H zPqT&H$C|xWT_WF&Q!-ysS;9R}Ok62EH`~}GOi5IPRa8ubiA7X*+PcYF;(UI##znU; zU%i=cWbezf^X=cC$E;8O+NLiZmF49VA-OI}JdS~ZVW+2yV@O3@YyW<3$3l^#=YQ^c zFCJtxq48qUMvZ1iNguWP2K&>`{42du%%wy!GDSO z)!^jTm7n`gO8xxy(?MC=@Y}mfD(hDWb5yvs$7%+9Yc-s&(Xgs*jR`o*=6O!~na?{$ z<5O!UsB!U4+}^(R=DF@|N=y@#Q=D!Xv{pxZ2K>atuu69to-$VBPi57UHx3vIVCg!0AaB)NB{r; literal 0 HcmV?d00001 diff --git a/assets/client_icons/movim.png b/assets/client_icons/movim.png new file mode 100644 index 0000000000000000000000000000000000000000..295d98f6e9f00da7399237bee2c2b42a456c644b GIT binary patch literal 1752 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

u5`t8`irpGcpnx~G0 zPZv=7m;PYQUB4?yZ1Wt|?`r9}ZSMc_UE_z;f@2@59Cj!iNXe6Ana4Dva@L-epSRza zp7?&gV_T8)ov1s?=J~&Jc3-_~Blp((l+PzU+wSwa>NeM2@4F!U!6!%I{q(a(ZwP3w z3g@(RIMgpV|D4LJ8(O|ISJvt)8Um)@~~dO;=Z8sa`nG6)%TfFzWpek>C15RPeXqGLi?Dw8SiiJzRvK@V2@PB zmq_Q=Dp&t>^TumwezKXd-Tj+NM$C>S65re=X?*vZToUN|pl#07KFxcNY#-Wl*{`g* zbM~&8-K!NRPtDz8I-eyqx>hQy#(dg>S*>}q^1OQzZ!OxZ|A|Z2#dz7pJ&(IDmR;Dg zN2O~YG{n@P04j~mapE- zdb0HN>zkrEXLq|C`lxg1O+wBb-g&z|RvpU!#A6z=yL8%u>ZbR8W_q>i&KB94pJ%@O z%)jJ+^pUdO&`C=!R(Nze*2#SQROdCZ^JeZD&sW<~&V5$rM?Ekp9e#+;Q=VtHuqOx<=kLx;xzvomG-~RNZg#FIfz;5sDnib~x?td>T zFdK{iwVY}GyreahC;92IGe_;i+?ReS*}vyfm&4J{sf+KuN^{z~TxIGWQw^tBx4h+E z=Vv}T;m4;omqkYJ?A@zP?=*W~m&tGR`J(dpS;xU=opSLHsy=A3IrY8W5fR)e%$v5& ziKkTZrnh=2>+~zopr0L;RxX8-^I literal 0 HcmV?d00001 diff --git a/assets/client_icons/pix-art.png b/assets/client_icons/pix-art.png new file mode 100644 index 0000000000000000000000000000000000000000..10d8b1d0859db4e12374f5a93e0cd1c59cbeebd6 GIT binary patch literal 1847 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd0X7B(2LEJbB?bnzf1WOmAr*18p|uuMeoEB^ zDn3b`bnJQ7w7V0gOuiZPbB5&7C)uB73JNDrzuPn6iLpy!M5oKl`w2YFiW9r8h35!v zt}YAhv)Y>OzqMK~^46|tSiCScb8k-)pIH6t*9ZQo(wB~I4V`R!Sv^QB+syw$ywr|fRkzr^zgH_e z?pQC(^{%#kWAu#Bk9U^nU)XrE-A<$P&DOow7o?rIAN%j-iCgx?d+Wb!|G4TBhtt~A zkwtn=r>6&$ORqWW!>7#Arncz+J8Rjv zcX2G*;lHT(`P5~l66XImJI;KtY2LK&`Mr|s=RZEgn_=&H!0nn>$!pPj>hrfw*}i(ys=ygFkuxW(uXigy5&k(fDuYCpkC3^ccTHokx#hdtmo5!!{$Ii=H{XT#rR|_0GR~ zH@TJXegJ3E@*i_H@3eI+O;~Og(z-v+UifF}@6Ag?wb{yiuk8&HD>?6&r}9ckcG3a9 z&oYswWls&%Ggm^^t$l5mECWiBb^Le82Zv?vo_3|++Ue&bl}tH z-F#QRGUR;hcv>=*`ACu1{JxAh4W$rqiHG9vPM3sc&1_WKq

bpz39o0|=E*<1?DNN6%POXJe*A9B9xPfb8F8TD#VMEA z&B7A}gMtmBW-XKZlAL#P!B(ZXt+lB3H5@dx z*X4Xyk?U~a*t0l>$6W_BHMh0BlKsHHQtp5hm-XgMz5@l#|0{wuw`H}*&w0Yukb6OK za^K>vBK;|!uIo+zlG*fsgUYuoxw|(1{@q*>UFY9@{ZH-w%wi7D7p*!cFYMg!z4Lc* z%ib#j)r)>>@5-MLcf&rUV| zxYi!gk^G^8d!OP;YbH7G+>HV|>t|T3tDmhncc=0EWwveG8}#&@PFY_}R8!8GYv;J* zj%|HW$&3%FE{p2k{PbP6^u(MkP8vt5a|&*54=duRKHDFXEzh>0^Wm}bB@Aoc_x@SR zT%CWlcEW^*_t$RbJe_;ke96qJf^_b4B5%vjE#Qe34x3b;>`@vu(I(W;A}1-cy^}X4 z=ess*h3U5++J(iX(~cjTuOWNyb*t;&%hmky_7~j4m)tBVVz>Gb7O}y}HhMF|n`eug z^d9PN_}=-f{1sD9$&{iN28d+!yn2QvKhwUH+e5 zo|+iAHUCHakvW#8yFM(svGZMlqxt`{cC!uj*4};5yZ5kCe(3Mpt@4viwLiP9Qfud) zZ}Q}B)yn3~l_t+zcu+3LrznkE$p4?W%@M*U6v!{y=wZG^5asG4SsWi6L zvlU8CU5{Djx|(OB`lB$h+q2Z%r>5`r@T>fmKdE@z{QvXZH^zLhj*Anyo2&VuOv~^7 z+FVdQ&MBb@0QDG(^Z)<= literal 0 HcmV?d00001 diff --git a/assets/client_icons/poezio.png b/assets/client_icons/poezio.png new file mode 100644 index 0000000000000000000000000000000000000000..0c143118de00f5c4fdbff98480ce74f04efb5d4e GIT binary patch literal 1363 zcmeAS@N?(olHy`uVBq!ia0y~yU=UznVBi;EV_;y|`*?B=0|Tp+r;B4qMcmZjT%X`@ ziG3aGns;&r&CHNJY_#FRz2fTp>f7GY8X`LPJEB-mxlinO`mv^_bE=eDARj?v?*-zVCTH?|J3D?|a%6 z?g<1(zHbk<=2&zxY@habBgYh(s0xK+GKO&_hNoxPrO)4LwBW$TFNfJzz1nricUfY2 z?#qvJ9=cxn{&OYsN2j@NA%+w4FHM{~$FMZf+A+n>N9MxrrST!QdL_Em&x;nr)@c0s^{zK7{o&Nq z&DW1Dn^0(<;e5~5YGV1OjW;gkT1~ZDUA5)zwUWJmg1;Z^O^;?;ZOL)_@~n4L-Zv%B zefn+5sYlP#ekW|bW>oh+%o0}!h@RQ8 z@{R4yo7d;?B)Xq}7e~+3mEY!Xs7^U2>;nS94nDDo87M84^6L`a7DP<@!--lDZY&PLM+D(y&RN!k}L!oFC19Gc%V{pf=@`T zZp+uY^MA@JmS2CTVK-fN*ZP!#v)^u&{&(2%IPLoTZ-N}sFeXa*6r$6Gs@AI8xgE0DagC_9+qQq#>-J6j7hSnxtNrz<&072K>E`U@Uue8&;q_nd zmp)8Mev@#;_+;u=j=$1B4UBj`2Sl<@Y>=5yqTsY7<%_dhhJt8)bMHjX)rEFNUYg6F zaDSHwV5pqw`?X=7XV&HQQooZ=Ssn}8yf?@B#41<+mfPRT=R{oPd+4?LYL48_^`1T# zq!-;gZ@uCTLvm2`i_ANY7c!o(etuo?q|IOw7vn6(E$d|-@Xli8c*=0(e`9Y@VC5gt z6SMeNAAhm@NpbLme_Yp>ujrM~{iS8Ceb`jRsCOBs@D1ZxFLtjAJf>Z_?A5B8o6%YUPqlN6o~+z{q4~+j6*YSEE;3HzbC{}l$ccNo zqXu_DVHCe#_c0H*zR%%?+Z&HdX5LVEIlrMr)4|obgl}zc>^$uR#>dyYI3gW>bA0Ik zbhG}`+O=AT%h{(@JPz}U6cl5-5~Jgz7W=qlp5o`b?Vt4L%Qgo-6+Gswcl`W$<0(ch zi9Rwj`aef0JWp@&fI^iMw&uRJT|4}aW(w?-?9eqCZbP0l+XkKb?}o{ literal 0 HcmV?d00001 diff --git a/assets/client_icons/profanity.png b/assets/client_icons/profanity.png new file mode 100644 index 0000000000000000000000000000000000000000..2022adb3ba8c6f3954a37f6d8d3afd5c9757d767 GIT binary patch literal 1492 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 literal 0 HcmV?d00001 diff --git a/assets/client_icons/qip.png b/assets/client_icons/qip.png new file mode 100644 index 0000000000000000000000000000000000000000..3dfca69b1b0e7f948f0faffef264ddd9fc96e800 GIT binary patch literal 1423 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd0X7B(2LEJbB?bo8Fi#i9kczn8DU|_NQYDV> zU$9~K22+_yOmRCG=vDL_Pf*}iYU*&LM+t6Rv2>@|)Qv&acQ@U#@6g=UUV5?T?u4yY<|+o$*6+LWe2RT? zZSLILa~E|zX4$`2^ls_B46)g7`}S-MgAeiVILp3&Tu?`L;ss>Rjl5{ zJ&V2V=Vn~|=<(fl|G$Xo9z5^9Ts$oqy+_$nJ@H|2=L3DEGmn~<9xTc(7oBW*lXJ^2 zz740@-Fz6-+!?=ZZ@6oI-7RgKTdP6Ydxzb3Gj|obI={YVe6;88-DTxBLfv2go_%W4 zcEhsz#NeDmV#o5*O>aF9IJQT+Db_ZBO9&%#s>2r12Yyy{-VB~*3BjBeIcBU63M3?U ziO;C#w0O;vbAkE%Z)U~t#=a9zkB2{*{c~SeV0yNAN$Q0u_RGM|~R zhjEj(CHsM2feyvWsSmspz8?J0{cz)pyD|~R3y(3Tsi*{*75&#VJ@wn>q{XW1D=%Bb zt=aj5*?oHUx-E4_*(8=R+x-+hv4c_KQDcp}V|ttLhUa{&8y>N8mNBxnxAuz09e7)$ zXss99^|mY5A=Rfc*;0Q^)Y7Q0yFS=oIg*iNA$C>dYQdZG75D9gy2bz82+Fiq|6l%$ z$^A={-yYi!znyORm@vhPxE!oYO!l;Oy?4H3!~WUJXR8MkecAMJ)5QHRbH2Wr$8@18 zd#~lQtxvZWPPyf|@|koQSKX0k4m?v|REZUQiFHix<$oD;O<+E^&Ehug9XdHWi;w-x zoK$eZ>deLK7sYRghL-GmVN-uO!Z)X%F(u2`GHprPk|Go746}xsnM@OUSRbwUw|$mI zgJaV;(wp`Y4W{hnUtX;_n))o ztmTrx7#I6cmdg=Ar>-VuPQLu^Bcn?8<6W!)>(@MAkrnp$MB%}f{=c2u4p?yQ*u?Q; z%BEVv!rPT;i;EjBe=>}myh7*LLeb-=URON)t@Tef zVEKw*i)m%kKFyS<$<0x;O$li$hKi9AKs?(6( zcBNxl*Lxt---N}A8&i}zDCBL@bIe~Y77hv44$rjF6*2UngG=!okjov literal 0 HcmV?d00001 diff --git a/assets/client_icons/qutim.png b/assets/client_icons/qutim.png new file mode 100644 index 0000000000000000000000000000000000000000..3f67b0c1cc9299bedd3d0617dad79cb27ef1cc5c GIT binary patch literal 1897 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 literal 0 HcmV?d00001 diff --git a/assets/client_icons/tkabber.png b/assets/client_icons/tkabber.png new file mode 100644 index 0000000000000000000000000000000000000000..536781aeaa031a826d370a88da360550dd42508d GIT binary patch literal 1234 zcmeAS@N?(olHy`uVBq!ia0y~yVBle3U=R>sV_;w?`sn1sz`!!o)5S5QB5v-KYM9+;fo&KP7^mSu-<=YA`O<)d=vaE_>&zvh3!x)cf`OEWh8m|Geh?`#Z%9 z{~UW0H`)EZ%Wzj?u22o@g>P^7x&7`w#NK?`FMs*G_R2nqQ~8QZ!^~c;3v^aFcI`K> zG^2qT@0B?Xmy`YHM{w<3VU*gYEB^05;rm;f49D`rSQqOwe0!FBw(OBB>#^5MxmO1K zwAt)a%)mA8?ji3N(mDdx7X{Zumu_LvF#5LO?3){pLbmfRx#k}HVAI>6$J!?u{FA(d zDrYlRu^UO;TXFpBL$y@(tovovw%>4{LCq~aZtE5`%HaX<&1%4i9-=9)5Y5^T*>Ut|9Z24}CCM zDsnU|L-*FN=O6eqC6Z3vV0xVQRO}S@W9h*5gZEe;KU{PwCu04 z`>JNwu^$g3ig@aUGf)2G@?ZD8+N9j#`lWjxwBE^@hUYr{d(O0L#+C@jD>nHr&Y#d+ zE_dde-1+sKPEFUe!=~<%@i3Vj8OMH{^M_AQz?LSJ>2JP?$0^RAS5(7mA=vNp-0b?L zg;580y=ng<9fQiqf)kQiJ{|t5xD~bQ(roBc0Kl8HCgN10RzvA z?p@ehp2t?=b7Q|%@2Rf4wOdcqp6hWO z)BXvHuQRy4D&_9)et5w&PWV`5^*+CXxW0{kWosJ6y&I;V_bYmEv`2Qg`Qzf%u@_(0 z@|VuuBwNwDknt(|^{p010*$|J_FL;{xUEF#?^5N1E0(?rsr|T3s-2}L`<=Uq^*n{W zbLZba_jg&)FO%sJ>t;3IsqX)xnzr-fUMAD|HEbIcc=SvvPZ_;Eox4)Mv(ElkL-X_{ zmYxDj<~=mOQMg}apUVYDzJNV{G*yJ^qwli3W!9@Y@i>@$k?hS`y}cV6U*3GBn6-83 z-(Dx%@cNftl`5{bQ(4Z0U5nd&TSR^P=i8+j2D&?Ecy+IN#b;@$8S&?$SaVdNoMy%B z_2>TQEqlQoeQLkPoAUdq#ZlKUCpKr7J}T`DkB^aTp7rFm$j;fj^3yX|o-BDi`!lgTe~ HDWM4faO_uE literal 0 HcmV?d00001 diff --git a/assets/client_icons/vacuum.png b/assets/client_icons/vacuum.png new file mode 100644 index 0000000000000000000000000000000000000000..b41efcc3a41f6956e3f412a04228e7e93316887d GIT binary patch literal 1351 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd0X7B(2LEJbB?bmoK2I0NkczmeQ!9N!!bOhV z-zW_|XCk@L&jd#V19JqRCW-NQlbARzDjTnJG#&^^K+hikviT)V<@AZLZ4xF56J zbK4!2welPGDP?~6B-l{A_j8`GC@%&V#wa4Y<*R5f`m1d$Iwv>Te z{Jj4h->*y7O$s|H$CPk7!lt`5OheG;5a+DKUBy$9g*R9!2MU`v2r17xy!uhYw9mfc z+8jBZcGqkt=}cXIV)`k*+sp;(=Vownr0)8`kYBXm)}ozrIR1Q1ylW`T$S%e7u1V+gKS(L=uE=Mm*!HSlaiq>W1ZlMZd4k*=#)DIxzA(UqRF?(Vg|yZ>OsC zt4+Nr*Kjv^>l+QzRh$uKj=UyUX7BAj#+X@J?R)DOe>llOeeP=#dzFgA$L(#l1O6LP_?!EMoV}ns$ z>!ly6YyZC0p84}*!+SsZ{~I5g6tm_(mtn<^;*%>UW(Z!4Vht!+cwx#{>kXZ`Ut)g> z1WePsZWX@o$rS$2sdeia1yY(q5>meQXU%-YIbrXokgs*&w}bxN-rV=?$ox9jRXI$b z-z>e#)5TCVfa~TG9Jl`-eb_z7kWC}clD{v$!6~MCet!Aw)dN_c=nA|t(^Oo$;N3q{~vnA{3}T~ z`nBbVE6W3mMZbgF?ccoKXV_2>tReg&hW~fYi$#hll`Gvp-nV?5<7cD2R&~Rk%a;8{ z*O>RG%)7FzmjBN-4gK!{+y*CdA1wK9yJDj7WX|$8ulHFxv}(R@zUgrQQZ>3gW*E0T4op)M%^4097 z2gc3kzAnCfS^t<#`J=2{jeisU&N&!R>v1%lDZ&EdQ4GGPlH^ z-A}Ub{>|-u4(IO46&VU}L>g>8HGi7dBwf#`d$Q+$U$u|n!p%o#8JDnLVh&>ebz#My z&3k$so}8&{n^}^+IC#im;r!Zrt4}%QB%T#{F=^=zd5`G^tcz~wL`QJH zxiU?_>z2#+=f2sETOwwu&P~vrAUiF0QeigZ`R6xlxc6?q%K1)dfAIhEJ16{aJc_$f Z-@?@S$H_xSiGhKE!PC{xWt~$(69C+2kW~Nx literal 0 HcmV?d00001 diff --git a/assets/client_icons/vk4xmpp.png b/assets/client_icons/vk4xmpp.png new file mode 100644 index 0000000000000000000000000000000000000000..c32d1a0e541502cda94979275f085ff5e1ae296c GIT binary patch literal 1857 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 T=Ud6Zz`)??>gTe~DWM4feynfx literal 0 HcmV?d00001 diff --git a/assets/client_icons/yaxim.png b/assets/client_icons/yaxim.png new file mode 100644 index 0000000000000000000000000000000000000000..4a48b8658581f738d965de9c872fa0dd0cbe84d7 GIT binary patch literal 1316 zcmeAS@N?(olHy`uVBq!ia0vp@3=9kc0&ENn40DY&s~8wqUVFMYhE&8QPx!01PGeof z9Zf;yEuI;Z-&nD6F)kKgEWB8lxp{Hh*QnQ9)x!7w+o@9=d9AANVD;j)lh$5Z`F&d1 zuHz;Bk@GKnm|i}q-08ZXQAE(a*`KwZhrV8StbOUXrQh@~NpjtJyzHp&@4M1RUqrr* z+*{KWp!S~Yo~TVQ%iW4^pWnD&@0VT?6D$-Ul#pQX(LBI7;Bu9v>B_H(uMhwJmpk2S z+H{@&_7fLQT&u7^;X&*hkqRr`frS@woqCS54|3By>%Vd_x>hpN^&D8rJ^Y3ihyl>kJ zvmamk-~Oulvi2oyJC&O^UPiu%eB$NMlUyti`o#3no`VMttUR?xKt*ZJK|4kl?W0=H zG<6?wor#+CLG5Ab?bWG!Ctvf6`mLQVmH+k9tEK<{xBHY{D!LS9GyR}OMETrzR%<(J z&9<6uHN9@8Z;G~8yl>MHH%zTA75W}ewJn4-TT#l>O$9@o2OvV`2YX^H~EUwo%Qw`gx}EF=#tcV zSUiG5gLTSo`2za~EAKxInin5e_5M)Hr%P{B!~Xw29=iLgwf+C<3z>_17>mDOp7~Gi z({)LSb-o|!6w=Qg_@HvmEhtgz|CIgwuj-a>R^w!r&6rcE{P}Ua#`pivnw~Yye64uQ zcx`IJjDs6QIVArzGcq?X>%Qcz>bJaCj+<90L~_1OSKpPWqspKD*Z0lXrr{B~aoRg> zf!J5;zKgg#&&b%zezESD)|@#;_GPa!G(%3eK5KKgWIc&vkxAY&)PIy<~s!nUsVLyHD58INfxb=_FH$g4&_l*^fK=w=J8wLg9Sx z{55^|L#LfA`TPI<)s@!KN8kScuc5j*;PSWs|L;a^^%7bC|Ns0Q_e&>cO9sS+{a)mD zaR2dBa!OyF#Qv67s%^?mxqaeNXP%vi!OG}&f%BH`DRbL>UvROd+xkYf%$@Jb<}&x( zdGq7<#gh*6Id{IWe*feD{uPR0>S=%f|L484REM?q|9|)FTT5?*GS`OXCL}-jALqY1 z?9;9%rIVdXq!%=3-p}fOm2>sx(R+=d&sIL0*`HlJ`%l^rRt8;@FPp!bu}1AUx%%$^ZXv^ZWja6aM(W{`JzHz8TB*8?RM=m2>4@RylJb(<&vUo5pzP#(&CmNyKcwpZ`TyVNs?gIu(f|MR=iRSgTXTw!H8jjJl+$gZ z8k-uMn}Q1C3j+`16@~^GDOxSwdj&&g?_B&UI_l`0HmdKI;Vst0KB=TL;wH) literal 0 HcmV?d00001 diff --git a/debug_pac.png b/debug_pac.png new file mode 100644 index 0000000000000000000000000000000000000000..51bcf0b8b61dc494a17bca1afc10196b94715927 GIT binary patch literal 17436 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ǕXR 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 F1OO{|zuf=; literal 0 HcmV?d00001 diff --git a/debug_psi.png b/debug_psi.png new file mode 100644 index 0000000000000000000000000000000000000000..c1018877967f00839378f5574d9ab76f2d0337c7 GIT binary patch literal 2577 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 WWKTJ7&jmYBQ_s`Y&t;ucLK6VpwH!16 literal 0 HcmV?d00001 diff --git a/debug.png b/debug_snit.png similarity index 100% rename from debug.png rename to debug_snit.png diff --git a/go-xmpp/stanza/caps.go b/go-xmpp/stanza/caps.go new file mode 100644 index 0000000..6f5ae8c --- /dev/null +++ b/go-xmpp/stanza/caps.go @@ -0,0 +1,26 @@ +package stanza + +import ( + "encoding/xml" +) + +// Capabilities +// Reference: https://xmpp.org/extensions/xep-0115.html#stream +// "A server MAY include its entity capabilities in a stream feature element so that connecting clients +// and peer servers do not need to send service discovery requests each time they connect." +// This is not a stream feature but a way to let client cache server disco info. + +// Moved from stream_features.go and made it a presence extension as per spec + +type Caps struct { + PresExtension + XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"` + Hash string `xml:"hash,attr"` + Node string `xml:"node,attr"` + Ver string `xml:"ver,attr"` + Ext string `xml:"ext,attr,omitempty"` +} + +func init() { + TypeRegistry.MapExtension(PKTPresence, xml.Name{Space: "http://jabber.org/protocol/caps", Local: "c"}, Caps{}) +} diff --git a/go-xmpp/stanza/stream_features.go b/go-xmpp/stanza/stream_features.go index d1b6274..6fb1195 100644 --- a/go-xmpp/stanza/stream_features.go +++ b/go-xmpp/stanza/stream_features.go @@ -42,19 +42,6 @@ func (streamFeatureDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamF return packet, err } -// Capabilities -// Reference: https://xmpp.org/extensions/xep-0115.html#stream -// "A server MAY include its entity capabilities in a stream feature element so that connecting clients -// and peer servers do not need to send service discovery requests each time they connect." -// This is not a stream feature but a way to let client cache server disco info. -type Caps struct { - XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"` - Hash string `xml:"hash,attr"` - Node string `xml:"node,attr"` - Ver string `xml:"ver,attr"` - Ext string `xml:"ext,attr,omitempty"` -} - // ============================================================================ // Supported Stream Features diff --git a/gtk-helpers.go b/gtk-helpers.go index 70b4a16..cd54f93 100644 --- a/gtk-helpers.go +++ b/gtk-helpers.go @@ -4,6 +4,11 @@ import ( "bytes" "context" "fmt" + "image" + "image/color" + "image/png" + "strconv" + "github.com/boxes-ltd/imaging" "github.com/crazy3lf/colorconv" "github.com/diamondburned/gotk4/pkg/gdk/v4" @@ -14,20 +19,45 @@ import ( "github.com/google/uuid" "github.com/rrivera/identicon" "gosrc.io/xmpp/stanza" - "image" - "image/color" - "image/png" xmpp_color "mellium.im/xmpp/color" - "strconv" ) -func init() { +func createTabBox(name, jid, av_hash string, removable bool) *gtk.Box { + box := gtk.NewBox(gtk.OrientationHorizontal, 10) + name_label := gtk.NewLabel(name) + left_click_gesture := gtk.NewGestureClick() + left_click_gesture.SetButton(1) + left_click_gesture.Connect("pressed", func() { + switchToTab(jid, &window.Window) + }) + box.AddController(left_click_gesture) + + if removable { + right_click_gesture := gtk.NewGestureClick() + right_click_gesture.SetButton(3) + right_click_gesture.Connect("pressed", func() { + removeTab(jid, &window.Window) + box.SetVisible(false) + }) + box.AddController(right_click_gesture) + } + + box.Append(name_label) + image := gtk.NewImage() + image.SetPixelSize(40) + go getAvatar(jid, av_hash, image) + box.Prepend(image) + + return box } func showUserWindow(u stanza.Presence, custom_nick string) { var ocu OccupantID u.Get(&ocu) + var caps stanza.Caps + u.Get(&caps) + var id string id = JidMustParse(u.From).Resource @@ -67,7 +97,7 @@ func showUserWindow(u stanza.Presence, custom_nick string) { } if loadedConfig.BlockingOI[ocu.ID] { - blocked_note := gtk.NewLabel("You have blocked this user!") + blocked_note := gtk.NewLabel(loadedLocale["blockedUserNotice"]) blocked_note.AddCSSClass("blocked") profile_box.Prepend(blocked_note) } @@ -82,7 +112,7 @@ func showUserWindow(u stanza.Presence, custom_nick string) { Type: "get", From: clientroot.Session.BindJid, To: u.From, - Id: uuid.New().String(), + Id: uuid.NewString(), Lang: "en", }) @@ -135,7 +165,7 @@ func showUserWindow(u stanza.Presence, custom_nick string) { Type: "get", From: clientroot.Session.BindJid, To: u.From, - Id: uuid.New().String(), + Id: uuid.NewString(), Lang: "en", }) @@ -210,30 +240,43 @@ func showUserWindow(u stanza.Presence, custom_nick string) { mmmm, ok := mmm.Load(id) im := gtk.NewImage() + im.SetPixelSize(160) + im.AddCSSClass("author_img") + profile_box.Prepend(im) + createIdenticon(u.From, false, im) + if ok { pres := mmmm.(stanza.Presence) - var vu VCardUpdate pres.Get(&vu) if vu.Photo != "" { getAvatar(u.From, vu.Photo, im) - im.SetPixelSize(80) - im.AddCSSClass("author_img") - profile_box.Prepend(im) - } else { - createIdenticon(u.From, false, im) - im.SetPixelSize(80) - im.AddCSSClass("author_img") - profile_box.Prepend(im) } - } else { - createIdenticon(u.From, false, im) - im.SetPixelSize(80) - im.AddCSSClass("author_img") - profile_box.Prepend(im) } }() + if caps.Hash != "" { + cap_box := gtk.NewBox(gtk.OrientationHorizontal, 2) + cap_box.SetHAlign(gtk.AlignCenter) + icon := gtk.NewImageFromPaintable(clientAssetsLoad("cap")) + icon.SetCursor(gdk.NewCursorFromName("pointer", nil)) + + cap_box.Append(icon) + cap_box.Append(gtk.NewLabel(fmt.Sprintf("Entity capabilities: Node %s", caps.Node))) + hasher := gtk.NewLabel(fmt.Sprintf("Using %s hash: %s", caps.Hash, caps.Ver)) + hasher.SetVisible(false) + cap_box.Append(hasher) + + left_click_gesture := gtk.NewGestureClick() + left_click_gesture.SetButton(1) + left_click_gesture.Connect("pressed", func() { + hasher.SetVisible(!(hasher.Visible())) + }) + icon.AddController(left_click_gesture) + profile_box.Append(cap_box) + + } + win.SetChild(profile_box) win.SetTransientFor(win) win.Present() @@ -311,37 +354,30 @@ func switchToTab(jid string, w *gtk.Window) { typed_tab := tab.(*chatTab) scroller.SetChild(typed_tab.msgs) typingStatus.SetText("") + + createFailBox := func() { + box := gtk.NewBox(gtk.OrientationVertical, 10) + failed_icon := gtk.NewImageFromPaintable(clientAssetsLoad("fail")) + failed_icon.SetPixelSize(100) + box.Append(failed_icon) + jid_label := gtk.NewLabel(jid) + jid_label.AddCSSClass("author") + jid_label.AddCSSClass("jid") + box.Append(jid_label) + label := gtk.NewLabel(loadedLocale["mucLoadError"]) + label.SetWrap(true) + box.Append(label) + memberList.SetChild(box) + } if typed_tab.isMuc { m, ok := mucmembers.Load(jid) if !ok { - box := gtk.NewBox(gtk.OrientationVertical, 10) - failed_icon := gtk.NewImageFromPaintable(clientAssetsLoad("fail")) - failed_icon.SetPixelSize(100) - box.Append(failed_icon) - jid_label := gtk.NewLabel(jid) - jid_label.AddCSSClass("author") - jid_label.AddCSSClass("jid") - box.Append(jid_label) - label := gtk.NewLabel("There was an error loading the members of this room. Maybe the MUC does not give user presence, it does not exist, you have been banned from it, or you have to fill a CAPTCHA to access it. Try joining the room again or check if the room exists.") - label.SetWrap(true) - box.Append(label) - memberList.SetChild(box) + createFailBox() return } ma, ok := m.(mucUnit) if !ok { - box := gtk.NewBox(gtk.OrientationVertical, 10) - failed_icon := gtk.NewImageFromPaintable(clientAssetsLoad("fail")) - failed_icon.SetPixelSize(100) - box.Append(failed_icon) - jid_label := gtk.NewLabel(jid) - jid_label.AddCSSClass("author") - jid_label.AddCSSClass("jid") - box.Append(jid_label) - label := gtk.NewLabel("There was an error loading the members of this room. Maybe the MUC does not give user presence, it does not exist, you have been banned from it, or you have to fill a CAPTCHA to access it. Try joining the room again or check if the room exists.") - label.SetWrap(true) - box.Append(label) - memberList.SetChild(box) + createFailBox() return } mm := ma.Members @@ -370,11 +406,6 @@ func switchToTab(jid string, w *gtk.Window) { gen.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) } - //id := ocu.ID - //if id == "" { - // id := JidMustParse(u.From).Resource - //} - nick_label := gtk.NewLabel(JidMustParse(u.From).Resource) var custom_nick string @@ -403,7 +434,7 @@ func switchToTab(jid string, w *gtk.Window) { nick_label.SetOpacity(0.5) } - userbox.SetTooltipText(fmt.Sprintf("%s\n%s\n%s\n%s", u.From, mu.MucUserItem.Role, mu.MucUserItem.Affiliation, loadedLocale["clickForMoreInfo"])) + // userbox.SetTooltipText(fmt.Sprintf("%s\n%s\n%s\n%s", u.From, mu.MucUserItem.Role, mu.MucUserItem.Affiliation, loadedLocale["clickForMoreInfo"])) userbox.Append(nick_label) var hats Hats @@ -418,7 +449,6 @@ func switchToTab(jid string, w *gtk.Window) { xc := xmpp_color.String(hat.URI, 255, loadedConfig.CVD) r, g, b, _ := xc.RGBA() val, _, _ = colorconv.RGBToHSV(uint8(r), uint8(g), uint8(b)) - } tB := tagBytes img, _, _ := image.Decode(bytes.NewReader(tB)) @@ -452,12 +482,26 @@ func switchToTab(jid string, w *gtk.Window) { userbox.Append(status_message) } + end_box := gtk.NewBox(gtk.OrientationHorizontal, 0) + + var caps stanza.Caps + ok = u.Get(&caps) + if ok { + client_icon := gtk.NewImageFromPaintable(clientAssetsLoad(caps.Node)) + //client_icon.SetHAlign(gtk.AlignEnd) + //client_icon.SetHExpand(true) + end_box.Append(client_icon) + } medal := gtk.NewImageFromPaintable(clientAssetsLoad(mu.MucUserItem.Affiliation)) medal.SetTooltipText(mu.MucUserItem.Affiliation) - medal.SetHAlign(gtk.AlignEnd) - medal.SetHExpand(true) - userbox.Append(medal) + //medal.SetHAlign(gtk.AlignEnd) + //medal.SetHExpand(true) + end_box.Append(medal) + end_box.SetHAlign(gtk.AlignEnd) + end_box.SetHExpand(true) + + userbox.Append(end_box) if loadedConfig.ShowAvatarsInMemberList { default_av := gtk.NewImage() @@ -479,6 +523,52 @@ func switchToTab(jid string, w *gtk.Window) { } } + userbox.SetHasTooltip(true) + userbox.Connect("query-tooltip", func( + x int, + y int, + keyboardMode bool, + tooltip *gtk.Tooltip, + ) bool { + box := gtk.NewBox(gtk.OrientationHorizontal, 4) + av_box := gtk.NewBox(gtk.OrientationHorizontal, 0) + if loadedConfig.ShowAvatarsInMemberList { + default_av := gtk.NewImage() + default_av.SetPixelSize(100) + createIdenticon(u.From, false, default_av) + av_box.Prepend(default_av) + var vcu VCardUpdate + ok = u.Get(&vcu) + if ok { + photo := vcu.Photo + go func() { + glib.IdleAdd(func() { + new_im := gtk.NewImage() + new_im.SetPixelSize(100) + av_box.Remove(default_av) + av_box.Append(new_im) + go getAvatar(u.From, photo, new_im) + }) + }() + + } + } + + box.Append(av_box) + + content_box := gtk.NewBox(gtk.OrientationVertical, 4) + content_box.Append(gtk.NewLabel(u.From)) + content_box.Append(gtk.NewLabel(mu.MucUserItem.Role)) + content_box.Append(gtk.NewLabel(mu.MucUserItem.Affiliation)) + content_box.Append(gtk.NewLabel(mu.MucUserItem.JID)) + + box.Append(content_box) + + tooltip.SetCustom(box) + + return true + }) + gesture := gtk.NewGestureClick() gesture.SetButton(1) @@ -498,7 +588,7 @@ func switchToTab(jid string, w *gtk.Window) { kb.ConnectClicked(func() { client.SendRaw(fmt.Sprintf(` @@ -506,7 +596,7 @@ func switchToTab(jid string, w *gtk.Window) { - `, clientroot.Session.BindJid, jid, JidMustParse(u.From).Resource)) + `, clientroot.Session.BindJid, uuid.NewString(), jid, JidMustParse(u.From).Resource)) }) bb.ConnectClicked(func() { @@ -516,14 +606,14 @@ func switchToTab(jid string, w *gtk.Window) { if mu.MucUserItem.JID != "" { client.SendRaw(fmt.Sprintf(` - `, clientroot.Session.BindJid, jid, JidMustParse(mu.MucUserItem.JID).Bare())) + `, clientroot.Session.BindJid, uuid.NewString(), jid, JidMustParse(mu.MucUserItem.JID).Bare())) } } }) @@ -547,14 +637,14 @@ func switchToTab(jid string, w *gtk.Window) { submit.ConnectClicked(func() { client.SendRaw(fmt.Sprintf(` - `, clientroot.Session.BindJid, jid, the_entry.Text(), JidMustParse(mu.MucUserItem.JID).Bare())) + `, clientroot.Session.BindJid, uuid.NewString(), jid, the_entry.Text(), JidMustParse(mu.MucUserItem.JID).Bare())) win.Destroy() }) @@ -588,7 +678,7 @@ func switchToTab(jid string, w *gtk.Window) { client.SendRaw(fmt.Sprintf(` @@ -596,7 +686,7 @@ func switchToTab(jid string, w *gtk.Window) { - `, clientroot.Session.BindJid, jid, JidMustParse(u.From).Resource, the_entry.Text())) + `, clientroot.Session.BindJid, uuid.NewString(), jid, JidMustParse(u.From).Resource, the_entry.Text())) }) box.Append(the_entry) @@ -642,7 +732,13 @@ func switchToTab(jid string, w *gtk.Window) { } else { headerBox.Append(gtk.NewImageFromPaintable(clientAssetsLoad("group"))) } - headerBox.Append(gtk.NewLabel(fmt.Sprintf("%d %s", i, loadedLocale["participants"]))) + + pword := loadedLocale["participants"] + if i == 1 { + pword = loadedLocale["participant"] + } + + headerBox.Append(gtk.NewLabel(fmt.Sprintf("%d %s", i, pword))) gen.Prepend(headerBox) muc_presence := typed_tab.muc_presence @@ -717,37 +813,29 @@ func createIdenticon(word string, always_create bool, i *gtk.Image) { // This fu loader.Close() p := loader.Pixbuf() gt := gdk.NewTextureForPixbuf(p) - /* - i := gtk.NewImageFromPaintable(gt) - i.AddCSSClass(loadedConfig.CVD.String() + "_CVD") - return i - */ glib.IdleAdd(func() { i.SetFromPaintable(gt) - i.AddCSSClass(loadedConfig.CVD.String() + "_CVD") }) } func jidBuilder(en *gtk.Entry) { // This function spawns a window that allows the user to interactively build a JID - // TODO: Localise this - win := gtk.NewWindow() - win.SetTitle("Build-A-JID") + win.SetTitle(loadedLocale["buildAJID"]) win.SetDefaultSize(400, 1) win.SetResizable(false) box := gtk.NewBox(gtk.OrientationVertical, 2) - header := gtk.NewLabel("Build-A-JID") + header := gtk.NewLabel(loadedLocale["buildAJID"]) header.AddCSSClass("author") box.Append(header) - box.Append(gtk.NewLabel("All fields except for domain are optional")) + box.Append(gtk.NewLabel(loadedLocale["buildAJIDOptionalFieldsNote"])) jid_builder := gtk.NewBox(gtk.OrientationHorizontal, 2) localPartEntry := gtk.NewEntry() - localPartEntry.SetPlaceholderText("localpart") + localPartEntry.SetPlaceholderText(loadedLocale["BAJlocalpart"]) jid_builder.Append(localPartEntry) at_sign := gtk.NewLabel("@") @@ -755,7 +843,7 @@ func jidBuilder(en *gtk.Entry) { // This function spawns a window that allows th jid_builder.Append(at_sign) domainEntry := gtk.NewEntry() - domainEntry.SetPlaceholderText("domain") + domainEntry.SetPlaceholderText(loadedLocale["BAJdomain"]) jid_builder.Append(domainEntry) resource_sign := gtk.NewLabel("/") @@ -763,7 +851,7 @@ func jidBuilder(en *gtk.Entry) { // This function spawns a window that allows th jid_builder.Append(resource_sign) resourceEntry := gtk.NewEntry() - resourceEntry.SetPlaceholderText("resource") + resourceEntry.SetPlaceholderText(loadedLocale["BAJresource"]) jid_builder.Append(resourceEntry) box.Append(jid_builder) diff --git a/gtk-message.go b/gtk-message.go index f3616f5..aa26fcd 100644 --- a/gtk-message.go +++ b/gtk-message.go @@ -81,12 +81,29 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi return gtk.NewLabel("Unsupported message."), true } - readmarker := Marker{} - ok = m.Get(&readmarker) - if ok { - b := gtk.NewBox(gtk.OrientationHorizontal, 0) - b.Append(gtk.NewLabel(fmt.Sprintf("%s%s", JidMustParse(m.From).Resource, loadedLocale["readWidget"]))) - return b, true + sid := StanzaID{} + m.Get(&sid) + + ocu := OccupantID{} + m.Get(&ocu) + + store_label := false + var msg *abMessage + tab, ok := tabs.Load(target) + typed_tab, ok2 := tab.(*chatTab) + if ok && ok2 { + store_label = true + msg, ok = typed_tab.msg_refsID[m.Id] + if !ok { + msg = new(abMessage) + typed_tab.msg_refsID[m.Id] = msg + } + + msg.ocu_id = ocu.ID + + if sid.ID != "" { + typed_tab.msg_refsOSID[sid.ID] = msg + } } composing := stanza.StateComposing{} @@ -108,19 +125,6 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi return error_box, true } - store_label := false - tab, ok := tabs.Load(target) - if ok { - store_label = true - } - typed_tab := tab.(*chatTab) - - sid := StanzaID{} - m.Get(&sid) - - ocu := OccupantID{} - - m.Get(&ocu) id := JidMustParse(m.From).Resource name := id @@ -179,7 +183,7 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi %s - `, m.To, jid.MustParse(m.From).Bare().String(), uuid.New().String(), m.Type, sid.ID, v)) + `, m.To, jid.MustParse(m.From).Bare().String(), uuid.NewString(), m.Type, sid.ID, v)) }) reactions.Append(like) } @@ -198,7 +202,7 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi %s - `, m.To, jid.MustParse(m.From).Bare().String(), uuid.New().String(), m.Type, sid.ID, custom.Text())) + `, m.To, jid.MustParse(m.From).Bare().String(), uuid.NewString(), m.Type, sid.ID, custom.Text())) }) rc_box.Append(custom) rc_box.Append(enter_custom) @@ -234,6 +238,26 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi rc_box.Append(vp) + live_edit := gtk.NewButtonWithLabel("Live edit") + live_edit.ConnectClicked(func() { + en := gtk.NewEntry() + en.ConnectChanged(func() { + client.SendRaw(fmt.Sprintf("%s", + uuid.NewString(), + target, + m.Type, + en.Text(), + m.Id, + )) + }) + + win := gtk.NewWindow() + win.SetChild(en) + win.SetVisible(true) + }) + + rc_box.Append(live_edit) + popover.SetChild(rc_box) rect := gdk.NewRectangle(x, y, 1, 1) popover.SetPointingTo(&rect) @@ -273,18 +297,8 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi // TODO: Decouple this from widget generation if store_label { - msg, ok := typed_tab.msg_refsID[m.Id] - if !ok { - msg = new(abMessage) - typed_tab.msg_refsID[m.Id] = msg - } - msg.label_ref = mlabel msg.ocu_id = ocu.ID - - if sid.ID != "" { - typed_tab.msg_refsOSID[sid.ID] = msg - } } } else { authorBox := gtk.NewBox(gtk.OrientationHorizontal, 10) @@ -312,10 +326,11 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi } im := gtk.NewImage() - im.SetFromPaintable(clientAssetsLoad("DefaultAvatar")) + // im := gtk.NewImageFromPaintable(clientAssetsLoad("DefaultAvatar")) + go createIdenticon(m.From, false, im) + // im.SetFromPaintable(clientAssetsLoad("DefaultAvatar")) im.SetPixelSize(40) im.AddCSSClass("author_img") - // createIdenticon(m.From, false, im) authorBox.Append(im) if can_generate { pres := mmmm.(stanza.Presence) @@ -325,14 +340,38 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi go getAvatar(m.From, vu.Photo, im) } } else { - createIdenticon(m.From, false, im) + go createIdenticon(m.From, false, im) } } else if m.Type == stanza.MessageTypeChat { al.SetText(al.Text() + loadedLocale["whispers"]) } + color_ind := gtk.NewLabel("") + + ycbcr := xmpp_color.String(id, 255, loadedConfig.CVD) + r, g, b, _ := ycbcr.RGBA() + hexcolor := fmt.Sprintf("#%02x%02x%02x", r, g, b) + + color_ind.SetMarkup(fmt.Sprintf("|", hexcolor)) + + authorBox.Append(color_ind) authorBox.Append(al) + oob := stanza.OOB{} + ok = m.Get(&oob) + if ok { + // media := newPictureFromWeb(oob.URL) + // media.SetCanShrink(false) + // mainBox.Append(media) + // media.AddCSSClass("chat_image") + mbtn := gtk.NewButtonWithLabel("🖼️") + // mbtn.SetChild(newImageFromWeb(oob.URL)) + mbtn.ConnectClicked(func() { + gopen.Open(oob.URL) + }) + authorBox.Append(mbtn) + } + if m.Thread != "" { im := gtk.NewImage() createIdenticon(m.Thread, true, im) @@ -346,6 +385,33 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi authorBox.Append(gtk.NewLabel("🎮")) } + stamp_label := gtk.NewLabel("") + stamp_label.SetHExpand(true) + stamp_label.SetHAlign(gtk.AlignEnd) + stamp_label.AddCSSClass("visitor") + + stamp_label.SetText(time.Now().Format("03:04 PM")) + stamp_label.SetTooltipText(time.Now().Format("2006-01-02 15:04:05")) + + stamp := new(Delayed_Stamp) + ok = m.Get(stamp) + + if ok { + stamped_time, err := stamp.GetStamp() + if err == nil { + stamped_time := *stamped_time + loc := time.Now().Location() + + stamped_time = stamped_time.In(loc) + str := stamped_time.Format("03:04 PM") + str_full := stamped_time.Format("2006-01-02 15:04:05") + stamp_label.SetText(str) + stamp_label.SetTooltipText(str_full) + } + } + + authorBox.Append(stamp_label) + body := m.Body body = XEPToPango(body, false) @@ -361,40 +427,15 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi // TODO: Decouple this from widget generation if store_label { - msg, ok := typed_tab.msg_refsID[m.Id] - if !ok { - msg = new(abMessage) - typed_tab.msg_refsID[m.Id] = msg - } - msg.label_ref = mlabel - msg.ocu_id = ocu.ID - - if sid.ID != "" { - typed_tab.msg_refsOSID[sid.ID] = msg - } + msg.read_indicator = gtk.NewBox(gtk.OrientationHorizontal, padding) + authorBox.Append(msg.read_indicator) } contentBox.Append(mlabel) - mainBox.Append(authorBox) mainBox.Append(contentBox) - oob := stanza.OOB{} - ok = m.Get(&oob) - if ok { - // media := newPictureFromWeb(oob.URL) - // media.SetCanShrink(false) - // mainBox.Append(media) - // media.AddCSSClass("chat_image") - mbtn := gtk.NewButtonWithLabel("🖼️") - // mbtn.SetChild(newImageFromWeb(oob.URL)) - mbtn.ConnectClicked(func() { - gopen.Open(oob.URL) - }) - authorBox.Append(mbtn) - } - if m.Subject != "" { subjectlabel := gtk.NewLabel(m.Subject) subjectlabel.SetWrap(true) @@ -460,6 +501,8 @@ func getVAdjustment(scrolledWindow *gtk.ScrolledWindow) *gtk.Adjustment { } func getAvatar(j, hash string, i *gtk.Image) { // TODO: This function probably shouldn't be here, and should probably be in xmpp-helpers or somewhere similar. + time.Sleep(200 * time.Millisecond) + hash = strings.ReplaceAll(hash, "/", "-") oghash := hash p, err := ensureCache() if err != nil { @@ -492,7 +535,7 @@ func getAvatar(j, hash string, i *gtk.Image) { // TODO: This function probably s Type: "get", From: clientroot.Session.BindJid, To: j, - Id: uuid.New().String(), + Id: uuid.NewString(), Lang: "en", }) diff --git a/gtk-xmpp-markup.go b/gtk-xmpp-markup.go index 29087c3..c176714 100644 --- a/gtk-xmpp-markup.go +++ b/gtk-xmpp-markup.go @@ -2,10 +2,10 @@ package main import ( "fmt" + "github.com/diamondburned/gotk4/pkg/gtk/v4" "html" "regexp" "strings" - "github.com/diamondburned/gotk4/pkg/gtk/v4" ) var ( @@ -65,6 +65,9 @@ func XEPToPango(text string, correction bool) string { } func GenerateRetraction(lab *gtk.Label, retraction Retraction) { + if lab == nil { + return + } if retraction.Moderated.OccupantID.ID != "" { lab.AddCSSClass("moderated") lab.SetTooltipMarkup(fmt.Sprintf("This message was retracted by a moderator.\nReason: '%s'", html.EscapeString(retraction.Reason))) diff --git a/helpers.go b/helpers.go index ea5016f..1a05ff3 100644 --- a/helpers.go +++ b/helpers.go @@ -2,6 +2,8 @@ package main import ( + "errors" + "gosrc.io/xmpp/stanza" "sort" "sync" ) @@ -23,3 +25,28 @@ func rangeOrdered(m *sync.Map, fn func(k, v any) bool) { } } } + +func getPresenceFromJIDMuc(jid *stanza.Jid) (*stanza.Presence, error) { + mo, ok := mucmembers.Load(jid.Bare()) + if !ok { + return nil, errors.New("tab is not present in mucmembers") + } + + mm, ok := mo.(mucUnit) + if !ok { + return nil, errors.New("mucUnit is nil") + } + + mmm := mm.Members + mmmm, ok := mmm.Load(jid.Resource) + if !ok { + return nil, errors.New("user not in room") + } + + pres, ok := mmmm.(stanza.Presence) + if !ok { + return nil, errors.New("presence is nil") + } + + return &pres, nil +} diff --git a/l18n.go b/l10n.go similarity index 81% rename from l18n.go rename to l10n.go index a004ca4..05685d2 100644 --- a/l18n.go +++ b/l10n.go @@ -76,24 +76,32 @@ var enGB = map[string]string{ // British English // gtk-helpers.go - "getPastMessages": "Get past messages...", - "clickForMoreInfo": "Click for more information", - "ban": "Ban", - "kick": "Kick", - "setAffil": "Set affiliation", - "setAffilDescPartOne": "Set ", - "setAffilDescPartTwo": "'s affiliation", - "setRole": "Set role", - "setRoleDescPartOne": "Set ", - "setRoleDescPartTwo": "'s role", - "setRoleWarning": "Important: if you want this to be permanent, set their affiliation instead", - "gettingVersion": "Getting version...", - "connectedWithRole": "Connected with role ", - "affiliatedAs": "Affiliated as ", - "unaffiliated": "Unaffiliated", - "participants": "participant(s)", - "versionQueryEmpty": "You do not have permission to view this client's version", - "versionQueryError": "Got error trying to get version", + "getPastMessages": "Get past messages...", + "mucLoadError": "There was an error loading the members of this room. Maybe the MUC does not give user presence, it does not exist, you have been banned from it, or you have to fill a CAPTCHA to access it. Try joining the room again or check if the room exists.", + "clickForMoreInfo": "Click for more information", + "ban": "Ban", + "kick": "Kick", + "setAffil": "Set affiliation", + "setAffilDescPartOne": "Set ", + "setAffilDescPartTwo": "'s affiliation", + "setRole": "Set role", + "setRoleDescPartOne": "Set ", + "setRoleDescPartTwo": "'s role", + "setRoleWarning": "Important: if you want this to be permanent, set their affiliation instead", + "gettingVersion": "Getting version...", + "connectedWithRole": "Connected with role ", + "affiliatedAs": "Affiliated as ", + "unaffiliated": "Unaffiliated", + "participants": "participants", + "participant": "participant", + "versionQueryEmpty": "You do not have permission to view this client's version", + "versionQueryError": "Got error trying to get version", + "blockedUserNotice": "You have blocked this user!", + "buildAJID": "Build-A-JID", + "buildAJIDOptionalFieldsNote": "All fields except for domain are optional", + "BAJlocalpart": "localpart", + "BAJdomain": "domain", + "BAJresource": "resource", "away": "Away", "chat": "Free for chat", diff --git a/main.go b/main.go index 812f833..704e776 100644 --- a/main.go +++ b/main.go @@ -49,11 +49,6 @@ var mIcon *gtk.Image var gStatus *gtk.Label var gIcon *gtk.Image -/* -var sStatus *gtk.Label -var sIcon *gtk.Image -*/ - var typingStatus *gtk.Label var pingStatus *gtk.Label @@ -93,9 +88,6 @@ func init() { } func main() { - go func() { - log.Println(http.ListenAndServe("localhost:6060", nil)) - }() pingTimes = append(pingTimes, []float64{}) p, err := ensureConfig() @@ -113,6 +105,12 @@ func main() { if err != nil { panic(err) } + if loadedConfig.Debug { + go func() { + log.Println(http.ListenAndServe("localhost:6060", nil)) + }() + log.Println("pprof debug server running at port 6060") + } if loadedConfig.Resource == "" { fmt.Println(loadedLocale["configResourceEmptyWarning"]) @@ -257,6 +255,31 @@ func main() { return } + originator := JidMustParse(m.From).Bare() + var typed_tab *chatTab + tab, ok := tabs.Load(originator) + if ok { + typed_tab = tab.(*chatTab) + } else { + + if m.Type != stanza.MessageTypeGroupchat { + createTab(originator, false, originator) + tab, ok = tabs.Load(originator) + if ok { + typed_tab = tab.(*chatTab) + glib.IdleAdd(func() { + box := createTabBox(originator, originator, originator, true) + menu.Append(box) + menu.Append(gtk.NewSeparator(gtk.OrientationVertical)) + }) + } else { + return + } + } else { + return + } + } + oi := new(OccupantID) ok = m.Get(oi) if ok { @@ -276,12 +299,6 @@ func main() { sid := new(StanzaID) m.Get(sid) - originator := JidMustParse(m.From).Bare() - - glib.IdleAdd(func() { - mStatus.SetText(originator) - }) - at := new(Attention) ok = m.Get(at) if ok { @@ -340,6 +357,41 @@ func main() { return } + // Handle read indicators + marker := new(Marker) + ok = m.Get(marker) + if ok { + _, ok := typed_tab.msg_refsOSID[marker.ID] + if ok { + im := gtk.NewImage() + im.SetPixelSize(20) + im.SetTooltipText(JidMustParse(m.From).Resource) + glib.IdleAdd(func() { + createIdenticon(m.From, false, im) + }) + + pres, err := getPresenceFromJIDMuc(JidMustParse(m.From)) + if err != nil { + return + } + + var vu VCardUpdate + ok = pres.Get(&vu) + if ok { + go getAvatar(m.From, vu.Photo, im) + } + + ind, ok := typed_tab.msg_refsOSID[marker.ID] + if ok { + if ind.read_indicator != nil { + ind.read_indicator.Append(im) + } + } + return + } + return + } + // Handle corrections BEFORE attempting to generate a message correction := new(Correction) ok = m.Get(correction) @@ -352,9 +404,11 @@ func main() { if ok { if msg.ocu_id == oi.ID && msg.ocu_id != "" { lab := msg.label_ref - glib.IdleAdd(func() { - lab.SetMarkup(XEPToPango(m.Body, true)) - }) + if lab != nil { + glib.IdleAdd(func() { + lab.SetMarkup(XEPToPango(m.Body, true)) + }) + } } return } @@ -370,15 +424,15 @@ func main() { if ok { typed_tab := tab.(*chatTab) /* - to_retract := "" - refs := typed_tab.msg_refsID + to_retract := "" + refs := typed_tab.msg_refsID - // If it is a DM, then we use regular message ID. If it is a MUC, we use stanza id - to_retract = sid.ID - if typed_tab.isMuc && sid != nil { + // If it is a DM, then we use regular message ID. If it is a MUC, we use stanza id to_retract = sid.ID - refs = typed_tab.msg_refsOSID - } + if typed_tab.isMuc && sid != nil { + to_retract = sid.ID + refs = typed_tab.msg_refsOSID + } */ msg, ok := typed_tab.msg_refsOSID[retraction.ID] @@ -387,7 +441,8 @@ func main() { glib.IdleAdd(func() { GenerateRetraction(lab, *retraction) }) - } + return + } } } @@ -399,7 +454,10 @@ func main() { return } - typed_tab := tab.(*chatTab) + typed_tab, ok := tab.(*chatTab) + if !ok { + return + } if ok { if current == JidMustParse(m.From).Bare() { @@ -413,6 +471,7 @@ func main() { ba, converted_successfully := untyped_box.(*gtk.Box) if valid && converted_successfully { + mStatus.SetText(originator) typed_tab.msgs.Append(b) b.Append(ba) b.Append(gtk.NewSeparator(gtk.OrientationVertical)) @@ -439,10 +498,7 @@ func main() { if ok { // This is a presence stanza from a user in a MUC presence.Get(&ocu) - // id := ocu.ID - // if id == "" { id := JidMustParse(presence.From).Resource - // } from, _ := stanza.NewJid(presence.From) muc := from.Bare() _, ok = mucmembers.Load(muc) @@ -567,7 +623,7 @@ func main() { pingStatus.AddCSSClass("pending") before := time.Now() iq := new(stanza.IQ) - iq.Id = uuid.New().String() + iq.Id = uuid.NewString() iq.From = clientroot.Session.BindJid iq.Type = "get" iq.Payload = &Ping{} @@ -621,22 +677,8 @@ func main() { name := "Note To Self" createTab(jid, false, name) glib.IdleAdd(func() { - box := gtk.NewBox(gtk.OrientationHorizontal, 10) - b := gtk.NewLabel(name) - gesture1 := gtk.NewGestureClick() - gesture1.SetButton(1) - gesture1.Connect("pressed", func() { - switchToTab(jid, &window.Window) - }) - - box.Append(b) - im := gtk.NewImage() - im.SetPixelSize(40) - go getAvatar(jid, jid, im) - box.Prepend(im) - - box.AddController(gesture1) - menu.Append(box) + b := createTabBox(name, jid, jid, false) + menu.Append(b) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) }) @@ -666,30 +708,7 @@ func main() { createTab(jid, false, name) glib.IdleAdd(func() { - box := gtk.NewBox(gtk.OrientationHorizontal, 10) - b := gtk.NewLabel(name) - gesture1 := gtk.NewGestureClick() - gesture1.SetButton(1) - gesture1.Connect("pressed", func() { - switchToTab(jid, &window.Window) - }) - gesture2 := gtk.NewGestureClick() - gesture2.SetButton(3) - gesture2.Connect("pressed", func() { - removeTab(jid, &window.Window) - box.SetVisible(false) - }) - - im := gtk.NewImage() - im.SetPixelSize(40) - - box.Append(im) - box.Append(b) - - // TODO: Use PEP avatar and do not use JID as hash - go getAvatar(jid, jid, im) - - box.AddController(gesture1) + box := createTabBox(name, jid, jid, false) menu.Append(box) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) }) @@ -754,31 +773,7 @@ func main() { joinMuc(client, clientroot.Session.BindJid, jid, nick, password) createTab(jid, true, name) glib.IdleAdd(func() { - box := gtk.NewBox(gtk.OrientationHorizontal, 10) - b := gtk.NewLabel(name) - gesture1 := gtk.NewGestureClick() - gesture1.SetButton(1) - gesture1.Connect("pressed", func() { - switchToTab(jid, &window.Window) - }) - - gesture2 := gtk.NewGestureClick() - gesture2.SetButton(3) - gesture2.Connect("pressed", func() { - removeTab(jid, &window.Window) - box.SetVisible(false) - }) - - im := gtk.NewImage() - im.SetPixelSize(40) - - box.Append(b) - box.Prepend(im) - - go getAvatar(jid, jid, im) - - box.AddController(gesture1) - box.AddController(gesture2) + box := createTabBox(name, jid, jid, true) menu.Append(box) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) }) @@ -795,31 +790,7 @@ func main() { joinMuc(client, clientroot.Session.BindJid, jid, nick, "") createTab(jid, true, jid) glib.IdleAdd(func() { - box := gtk.NewBox(gtk.OrientationHorizontal, 10) - b := gtk.NewLabel(jid) - gesture1 := gtk.NewGestureClick() - gesture1.SetButton(1) - gesture1.Connect("pressed", func() { - switchToTab(jid, &window.Window) - }) - - gesture2 := gtk.NewGestureClick() - gesture2.SetButton(3) - gesture2.Connect("pressed", func() { - removeTab(jid, &window.Window) - box.SetVisible(false) - }) - - im := gtk.NewImage() - im.SetPixelSize(40) - - box.Append(b) - box.Prepend(im) - - go getAvatar(jid, jid, im) - - box.AddController(gesture1) - box.AddController(gesture2) + box := createTabBox(jid, jid, jid, true) menu.Append(box) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) }) @@ -891,26 +862,8 @@ func activate(app *gtk.Application) { submit := gtk.NewButtonWithLabel(loadedLocale["submit"]) submit.ConnectClicked(func() { t := jid_entry.Text() - n := t - createTab(t, false, n) - box := gtk.NewBox(gtk.OrientationHorizontal, 0) - b := gtk.NewLabel(n) - box.Append(b) - gesture1 := gtk.NewGestureClick() - gesture1.SetButton(1) - gesture1.Connect("pressed", func() { - switchToTab(t, &window.Window) - }) - - gesture2 := gtk.NewGestureClick() - gesture2.SetButton(3) - gesture2.Connect("pressed", func() { - removeTab(t, &window.Window) - box.SetVisible(false) - }) - - box.AddController(gesture1) - box.AddController(gesture2) + createTab(t, false, t) + box := createTabBox(t, t, t, true) menu.Append(box) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) dm_window.SetVisible(false) @@ -1007,19 +960,14 @@ func activate(app *gtk.Application) { if JidMustParse(mu.MucUserItem.JID).Bare() == JidMustParse(clientroot.Session.BindJid).Bare() { if mu.MucUserItem.Affiliation != "owner" { box.Append(gtk.NewLabel(loadedLocale["destroyMUCNotOwnerWarning"])) + return false } } } - } else { - panic("not ok") } - } else { - panic("not ok") } return true }) - } else { - panic("not ok") } win.SetChild(box) @@ -1101,31 +1049,7 @@ func activate(app *gtk.Application) { } createTab(t, true, n) - box := gtk.NewBox(gtk.OrientationHorizontal, 10) - im := gtk.NewImage() - im.SetPixelSize(40) - - go getAvatar(t, t, im) - - b := gtk.NewLabel(n) - - box.Append(im) - box.Append(b) - gesture1 := gtk.NewGestureClick() - gesture1.SetButton(1) - gesture1.Connect("pressed", func() { - switchToTab(t, &window.Window) - }) - - gesture2 := gtk.NewGestureClick() - gesture2.SetButton(3) - gesture2.Connect("pressed", func() { - removeTab(t, &window.Window) - box.SetVisible(false) - }) - - box.AddController(gesture1) - box.AddController(gesture2) + box := createTabBox(n, t, t, true) menu.Append(box) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) @@ -1137,7 +1061,6 @@ func activate(app *gtk.Application) { } } if !ok { - if !disco_check.Active() { jm(t, "") win.SetVisible(false) @@ -1152,7 +1075,7 @@ func activate(app *gtk.Application) { Type: "get", From: clientroot.Session.BindJid, To: t, - Id: "dicks", + Id: uuid.NewString(), Lang: "en", }) @@ -1163,122 +1086,128 @@ func activate(app *gtk.Application) { myIQ.Payload = &stanza.DiscoInfo{} ctx := context.Background() - mychan, err := client.SendIQ(ctx, myIQ) - if err == nil { - result := <-mychan - res, ok = result.Payload.(*stanza.DiscoInfo) - if ok { - if len(res.Identity) >= 1 { - features := res.Features - allowed = false - password_protected := false - password := "" - warning_win := gtk.NewWindow() - warning_win.SetTitle(fmt.Sprintf("%s%s", loadedLocale["joinPreviewTitle"], res.Identity[0].Name)) - warning_win.SetDefaultSize(400, 1) - warning_win.SetResizable(false) - buttons := gtk.NewBox(gtk.OrientationHorizontal, 10) + var mychan chan stanza.IQ + //go func() { + mychan, err = client.SendIQ(ctx, myIQ) + if err != nil { + showErrorDialog(err, &window.Window) + } + //}() - join_button := gtk.NewButtonWithLabel("Join") - join_button.ConnectClicked(func() { - warning_win.SetVisible(false) - if password_protected { - allowed = false + result := <-mychan + res, ok = result.Payload.(*stanza.DiscoInfo) + if ok { + if len(res.Identity) >= 1 { + features := res.Features + allowed = false + password_protected := false + password := "" + warning_win := gtk.NewWindow() + warning_win.SetTitle(fmt.Sprintf("%s%s", loadedLocale["joinPreviewTitle"], res.Identity[0].Name)) + warning_win.SetDefaultSize(400, 1) + warning_win.SetResizable(false) - password_win := gtk.NewWindow() - password_win.SetTitle(loadedLocale["joinPasswordRequired"]) - password_win.SetDefaultSize(400, 1) - password_win.SetResizable(false) - box := gtk.NewBox(gtk.OrientationVertical, 10) - en := gtk.NewPasswordEntry() - submit := gtk.NewButtonWithLabel(loadedLocale["submit"]) - submit.ConnectClicked(func() { - password = en.Text() - jm(res.Identity[0].Name, password) - password_win.SetVisible(false) - }) - box.Append(en) - box.Append(submit) - password_win.SetChild(box) - password_win.SetVisible(true) - } + buttons := gtk.NewBox(gtk.OrientationHorizontal, 10) - jm(res.Identity[0].Name, password) - }) + join_button := gtk.NewButtonWithLabel("Join") + join_button.ConnectClicked(func() { + warning_win.SetVisible(false) + if password_protected { + allowed = false - cancel_button := gtk.NewButtonWithLabel(loadedLocale["cancel"]) - cancel_button.ConnectClicked(func() { - warning_win.SetVisible(false) - }) - - join_button.SetHExpand(true) - cancel_button.SetHExpand(true) - - buttons.Append(join_button) - buttons.Append(cancel_button) - warning_box := gtk.NewBox(gtk.OrientationVertical, 10) - header := gtk.NewLabel(res.Identity[0].Name) - warning_box.Append(header) - addFeature := func(icon string, description string) { - box := gtk.NewBox(gtk.OrientationHorizontal, 10) - box.Append(gtk.NewImageFromPaintable(clientAssetsLoad(icon))) - box.Append(gtk.NewLabel(description)) - warning_box.Append(box) + password_win := gtk.NewWindow() + password_win.SetTitle(loadedLocale["joinPasswordRequired"]) + password_win.SetDefaultSize(400, 1) + password_win.SetResizable(false) + box := gtk.NewBox(gtk.OrientationVertical, 10) + en := gtk.NewPasswordEntry() + submit := gtk.NewButtonWithLabel(loadedLocale["submit"]) + submit.ConnectClicked(func() { + password = en.Text() + jm(res.Identity[0].Name, password) + password_win.SetVisible(false) + }) + box.Append(en) + box.Append(submit) + password_win.SetChild(box) + password_win.SetVisible(true) } - for _, feature := range features { - switch feature.Var { - case "muc_passwordprotected": - password_protected = true - addFeature("muc_passwordprotected", loadedLocale["muc_passwordprotected_description"]) - case "muc_unsecured": - addFeature("muc_unsecured", loadedLocale["muc_unsecured_description"]) - case "muc_membersonly": - addFeature("muc_membersonly", loadedLocale["muc_membersonly_description"]) - case "muc_open": - addFeature("muc_open", loadedLocale["muc_open_description"]) - case "muc_moderated": - addFeature("muc_moderated", loadedLocale["muc_moderated_description"]) - case "muc_unmoderated": - addFeature("muc_unmoderated", loadedLocale["muc_unmoderated_description"]) - case "muc_nonanonymous": - addFeature("muc_nonanonymous", loadedLocale["muc_nonanonymous_description"]) - case "muc_semianonymous": - addFeature("muc_semianonymous", loadedLocale["muc_semianonymous_description"]) - case "muc_persistent": - addFeature("muc_persistent", loadedLocale["muc_persistent_description"]) - case "muc_temporary": - addFeature("muc_temporary", loadedLocale["muc_temporary_description"]) - case "muc_public": - addFeature("muc_public", loadedLocale["muc_public_description"]) - case "muc_hidden": - addFeature("muc_hidden", loadedLocale["muc_hidden_description"]) - case "urn:xmpp:mam:0": - addFeature("ok", loadedLocale["urn:xmpp:mam_description"]) - case "urn:xmpp:message-moderate:0": - addFeature("moderate", loadedLocale["urn:xmpp:message-moderate_description"]) - /* - default: - addFeature("comment", feature.Var) - */ - } - } + jm(res.Identity[0].Name, password) + }) - warning_box.Append(buttons) - warning_win.SetChild(warning_box) - warning_win.Present() - } else { - allowed = false - showErrorDialog(fmt.Errorf(loadedLocale["discoFail"]), win) + cancel_button := gtk.NewButtonWithLabel(loadedLocale["cancel"]) + cancel_button.ConnectClicked(func() { + warning_win.SetVisible(false) + }) + + join_button.SetHExpand(true) + cancel_button.SetHExpand(true) + + buttons.Append(join_button) + buttons.Append(cancel_button) + warning_box := gtk.NewBox(gtk.OrientationVertical, 10) + header := gtk.NewLabel(res.Identity[0].Name) + warning_box.Append(header) + addFeature := func(icon string, description string) { + box := gtk.NewBox(gtk.OrientationHorizontal, 10) + box.Append(gtk.NewImageFromPaintable(clientAssetsLoad(icon))) + box.Append(gtk.NewLabel(description)) + warning_box.Append(box) } + + for _, feature := range features { + switch feature.Var { + case "muc_passwordprotected": + password_protected = true + addFeature("muc_passwordprotected", loadedLocale["muc_passwordprotected_description"]) + case "muc_unsecured": + addFeature("muc_unsecured", loadedLocale["muc_unsecured_description"]) + case "muc_membersonly": + addFeature("muc_membersonly", loadedLocale["muc_membersonly_description"]) + case "muc_open": + addFeature("muc_open", loadedLocale["muc_open_description"]) + case "muc_moderated": + addFeature("muc_moderated", loadedLocale["muc_moderated_description"]) + case "muc_unmoderated": + addFeature("muc_unmoderated", loadedLocale["muc_unmoderated_description"]) + case "muc_nonanonymous": + addFeature("muc_nonanonymous", loadedLocale["muc_nonanonymous_description"]) + case "muc_semianonymous": + addFeature("muc_semianonymous", loadedLocale["muc_semianonymous_description"]) + case "muc_persistent": + addFeature("muc_persistent", loadedLocale["muc_persistent_description"]) + case "muc_temporary": + addFeature("muc_temporary", loadedLocale["muc_temporary_description"]) + case "muc_public": + addFeature("muc_public", loadedLocale["muc_public_description"]) + case "muc_hidden": + addFeature("muc_hidden", loadedLocale["muc_hidden_description"]) + case "urn:xmpp:mam:0": + addFeature("ok", loadedLocale["urn:xmpp:mam_description"]) + case "urn:xmpp:message-moderate:0": + addFeature("moderate", loadedLocale["urn:xmpp:message-moderate_description"]) + /* + default: + addFeature("comment", feature.Var) + */ + } + } + + warning_box.Append(buttons) + warning_win.SetChild(warning_box) + warning_win.Present() } else { allowed = false - if result.Error != nil { - showErrorDialog(fmt.Errorf("%s: %s - %s", loadedLocale["discoFail"], result.Error.Reason, result.Error.Text), win) - } else { - showErrorDialog(fmt.Errorf(loadedLocale["discoFail"]), win) - } + showErrorDialog(fmt.Errorf(loadedLocale["discoFail"]), win) + } + } else { + allowed = false + if result.Error != nil { + showErrorDialog(fmt.Errorf("%s: %s - %s", loadedLocale["discoFail"], result.Error.Reason, result.Error.Text), win) + } else { + showErrorDialog(fmt.Errorf(loadedLocale["discoFail"]), win) } } diff --git a/style.css b/style.css index 60adecc..fd09de1 100644 --- a/style.css +++ b/style.css @@ -72,6 +72,9 @@ background-color: red; } + +// a11y + .RedGreen_CVD { filter: hue-rotate(30deg) saturate(120%) contrast(110%); } diff --git a/types.go b/types.go index 501929b..16609a1 100644 --- a/types.go +++ b/types.go @@ -8,8 +8,9 @@ import ( ) type abMessage struct { - label_ref *gtk.Label - ocu_id string + label_ref *gtk.Label + ocu_id string + read_indicator *gtk.Box } type chatTab struct { diff --git a/xmpp-delay.go b/xmpp-delay.go new file mode 100644 index 0000000..56e3e7c --- /dev/null +++ b/xmpp-delay.go @@ -0,0 +1,30 @@ +package main + +import ( + "encoding/xml" + "gosrc.io/xmpp/stanza" + "time" +) + +// Implementation of XEP-0203: Delayed Delivery +// https://xmpp.org/extensions/xep-0203.html + +type Delayed_Stamp struct { + XMLName xml.Name `xml:"urn:xmpp:delay delay"` + From string `xml:"from,attr"` + Stamp string `xml:"stamp,attr"` +} + +func (d *Delayed_Stamp) GetStamp() (*time.Time, error) { + stamp := d.Stamp + time, err := time.Parse(time.RFC3339, stamp) + if err != nil { + return nil, err + } + + return &time, nil +} + +func init() { + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:delay", Local: "delay"}, Delayed_Stamp{}) +} diff --git a/xmpp-retractions.go b/xmpp-retractions.go index 97ee754..430d4f7 100644 --- a/xmpp-retractions.go +++ b/xmpp-retractions.go @@ -5,21 +5,20 @@ import ( "gosrc.io/xmpp/stanza" ) -// Implementation of XEP-0424: Message Retraction as well as XEP-0425: Moderated Message Retraction +// Implementation of XEP-0424: Message Retraction as well as XEP-0425: Moderated Message Retraction type Retraction struct { stanza.MsgExtension - XMLName xml.Name `xml:"urn:xmpp:message-retract:1 retract"` - ID string `xml:"id,attr"` // Stanza ID is used in group chats. - Reason string `xml:"reason"` + XMLName xml.Name `xml:"urn:xmpp:message-retract:1 retract"` + ID string `xml:"id,attr"` // Stanza ID is used in group chats. + Reason string `xml:"reason"` Moderated Moderated `xml:"urn:xmpp:message-moderate:1 moderated"` } type Moderated struct { - XMLName xml.Name `xml:"urn:xmpp:message-moderate:1 moderated"` - By string `xml:"by,attr"` + XMLName xml.Name `xml:"urn:xmpp:message-moderate:1 moderated"` + By string `xml:"by,attr"` OccupantID OccupantID - } // To whoever thought putting the same element twice in the same message, but in different parents, and not specifying this behaviour anywhere in the XEP was a good idea, I want to kill you. Seriously, I do. If I see you walking home at night I will kill you. Brutally. Slowly. I will relish in the act. I will do it.