From 42cc6db0e29c26359a77cdf5ba25c25c858bb945 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 18 Jan 2026 02:30:56 +0100 Subject: [PATCH 01/17] added support for face filters --- CHANGELOG.md | 4 + assets/filters/beard_upper_lip.webp | Bin 0 -> 3652 bytes assets/filters/dog_brown_ear.webp | Bin 0 -> 4536 bytes assets/filters/dog_brown_nose.webp | Bin 0 -> 5904 bytes ios/Podfile.lock | 16 ++ .../camera_preview.dart | 13 +- .../camera_preview_controller_view.dart | 72 +++++- .../face_filters.dart | 33 +++ .../main_camera_controller.dart | 101 +++++++- .../face_filters/beard_filter_painter.dart | 174 +++++++++++++ .../face_filters/dog_filter_painter.dart | 242 ++++++++++++++++++ .../face_filters/face_filter_painter.dart | 44 ++++ pubspec.lock | 8 + pubspec.yaml | 4 +- 14 files changed, 696 insertions(+), 15 deletions(-) create mode 100644 assets/filters/beard_upper_lip.webp create mode 100644 assets/filters/dog_brown_ear.webp create mode 100644 assets/filters/dog_brown_nose.webp create mode 100644 lib/src/views/camera/camera_preview_components/face_filters.dart create mode 100644 lib/src/views/camera/painters/face_filters/beard_filter_painter.dart create mode 100644 lib/src/views/camera/painters/face_filters/dog_filter_painter.dart create mode 100644 lib/src/views/camera/painters/face_filters/face_filter_painter.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index a9d3887..ffabff6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.0.87 + +- Added basic support for face filters + ## 0.0.86 - Allows to reopen send images (if send without time limit or enabled auth) diff --git a/assets/filters/beard_upper_lip.webp b/assets/filters/beard_upper_lip.webp new file mode 100644 index 0000000000000000000000000000000000000000..058b2ed09d90d51c74c76ab03e73d741a1be63f3 GIT binary patch literal 3652 zcmV-K4!iMENk&FI4gdgGMM6+kP&il$0000G000170RT_{06|PpNKgv^00AK}4KY9c z11k~H{|WF7;oG(yOOhnXB4UgG|6&!Q0qg}3h?qpxpE4sR0HP#Gb(;tXI>C8&??8`F zXq*3EYUF)Wb;>T60Bt!M$9IM;GBMop0!Zuv5@(~t+|_&IPU3o z^B_ppqVpT{P~B4q;k6V+ZJL^{n`&BL6XxIvm2SWMN1}yOU)vYXNsi_q%$F{OxzS+g zzuYPdD#|KKuf@0~bvHGSsTrC_(+y4WqUnFdh!{#8?&*t-+Je}5bUda90unCW#&LKI zQ!{ls>mI`+Dt*napCTB#iN}DRmd>|YP2hMf8*pYxydnDZaItNj2!kb$K)eF}3_-Q& z;faKy-)TiK_0QcA2x7Jyyf-wm?MLSln1mkyaxM!GsIUNlD7ruWr<*S{c4&F+vHE{& z16`%C(-ZA?nsW@ZJ};5>m(}1`o>u)kcVWMKq8XY88klclS?tnfX85k2*-_91lvqz} z6w8R1UI!MJgzY|>p=+i`^UtK^c>?m96yC z4c*W*KLFx?n&vS*bW^>SCq(p6ss`5$%a`=lM~UuT3fNtf8SK(3f4D-eTqR}+iq-T; zCVfezH|_E4fiPHK7+#Tb17#&`SuUtW%Cnbp#T` zMl9LmRuP1MPjzu{QZZrh-*dznrMHu${#8Z{rS|R#FeG}|GLY2v?=@@8mY6S|BUq2u zS&s-~=_DSOAjY)zj6lVirRwxrs|=nM#R2Q7zw0!NOIdc z+{jUaCvpd^0{(zS45kK_tPf>;fPTQL<6`ah{%-&(f?11^i6*vdT*dg*1GlM;6GWAq zVru~e@7N7@J|VI|z%U@^jm*Je9!o8h$3yU4!eoU03`RJU2EpPe5TExRP5`=&O8f1E zymd6Dqjp!n&ty{V#mvM$?g8VX!LpR0$f6NS5|3ekL}v(Q*yRn&P%8nk{E#8sL5u~7 z5CDDvIBeW@6nCI(qm~ju$f23`02VN;VtlGP0Gv6H0Dw_|Jq2YaU^#ZC0iN z_jpeXNA4;jmK*^=g#^0BGXfEYJV0Ah*En|ZLRjtwQ`0H0?5_-P0ir&)6zLTTwHVjL zGXe>z?n9CI05(lnTiP@n+jjM|$U%2Q!(Qq$^ujzJo3uM&lyV6M+e&KC_BLSG!0X-`mpT#j7*Whet zbr@}jm562mAtYC#q_PvJ1j!`Tso-l}q`FoOOH!mX@SmptX)+L2{r;<}xf`kVAxEk7i9HlAAmu^E{%j7_W=07$IRL=0|J-~tGfIJRsYldWUN z#DEEd8*-&P}TRN3dZR4nd zErJOp0pR!qN5msqs;zGK7^aWL&hXvG_wE>}Tt+FGg?)$?=Mg($AZ(q55` zQQEvF1&Z*N!c1WA%9>j5P?QFAg1fzMzp82JYCVhMiAxMaq`VMoUTF}aI zd56`ZfhV(6XCUHj;t6QTZ<)!ZI61Tm7^o95EU_+yNzOL>IAO4aTUi1WWNrhIS(rb!#FfcRlI!nU{2qu{zc({duo~#_$cA}O! zmJ~;K5cRR<80ty@lsNRHwuy$7JCb?)`0zuQKJ>sW_xT_2>jh8!hE_lkL(`+QMDPOv zk>`#qUQa$&zx)7v4Bm_UOhN`TvA`3~)FuHohz!lgkB=WT;s!aWBtgi*CL26-NXBB3 zP$U5G2m*k6Vlbt^Z*;t-h1cN8g;hX+L;%2r-DGI@@#DwGF#OPCo!fPB+1Tz+umKQ3 zb)ii>A%-agL%_Lz)M27>o@<-z80kl~?Cm9qkl^?a1gu%%w;-PRz7$vmwBkqP9!#td z9|zX?@wZJ4>W4PFgfIqtdZ%$r;q7`+;-@zJoP{h95h*_-hAGs-BKaHL<&kht7-v!B zH(|uB>-3HEs9~*NsYfEYyhZUd}Jcyen4jsWQ-1yn~ z%wPc)9#wIG#$?W4FK3I=C}Z@bK1BIib&kRONV$>;<}ATGF#3#3Kz`U}*+t>Odi1NP zU_L&SWrHeTI^iLFH5`FfdNFip7}H(SR^+0i6(oT)^N6zJg`!i-!-b8cSYxY#m-N&D z?NCA|;&HRq-T(mp_|J4R^n4OB&ZgznA`h(gT`}B>!SGp}yv7OOf%ZW-IoAT*vqP2R zw??1^D8`>^Tk~_LL7NvXNXzaick}p~0OO5g%=(1Xvf|&gG+5t7@$AYxVKMUtkeO>` zAoYoF{}MspmH{$rOEFgq(|m*3SMAft1bf!%IF*VeV8KrTCy|Y%*~lFPmD)3P=(p0J=0Brnv2ZmFPBSG&jVh_aAkN^bnkfoL zHccVR=rs2+pKigT0xoNc0(BM9fy;^aFHtD6M67q0J#5>tYumUozt2U)*Cr(2V?Y1^2x0aW_R3jxgw*D#GNj)}gnVGmGPtB0NB!Dd WlnkhHfiZMRWfk;rn?TeI0002rmfOhy literal 0 HcmV?d00001 diff --git a/assets/filters/dog_brown_ear.webp b/assets/filters/dog_brown_ear.webp new file mode 100644 index 0000000000000000000000000000000000000000..0dfec38473f369efe1dc946ae1b3e0a59a103370 GIT binary patch literal 4536 zcmV;p5l8M)Nk&Gn5dZ*JMM6+kP&il$0000G0002t001`u06|PpNRkNv01fbfZQJrR z+P3XitdLOL(~i5lyU~u-om3g9kJR1Wy*NyZ*4@41?(Tw>Kte-9NUm#chvYbp+`dG_ z1mOR6H7bf;H8g8lb1M^qbXP*vf&`tx+-Xp>DF%Nt6m7y` zPJP53Tjjd2{XlcW1)o=RAxWLON3=uBMj8^@n!9$a4mCQO%D36QA~QtV2E?HCtp-E1 zrDwOwk`+zTQY?K2gu$P$dp5mrox>+6o%Mwj_6lP>Q1$l6yW zl$iS5IRtF7b>y{~r7qcobj`3J!0+txU{Q%fF5$MGRS5ieYx*M_)_Y}Qnmia(CQZB{ z#Ve7x!=Ut`#n08q?VTQ$ZcSE zIhegD56n(o=?1S#VgZ=7fZYKZ;8x{EH{v#MYXr9=uL8FbU{)D*Ik=q)X7yt%U^cl1 z%!W*A17;(@ZS+BK8wO@|X~V$nG%y?XiWTfG;YArR0_@IR3RcZCj9@nwy!IIbezzJ} zF^!1OsCaBMq!?{|M9@2E7E{)t5lMbvKyTR)hGk z!(m`vduu0g!$yOBtv^T)I|cr=e2_kH4kS2+vdF^_;T9cX!!sa4?K6;VJW~f59{ZRq z-5Cf`I|#DVA;wi?#r77%XF9U>LX00OR86dvLk(57NQhB$fab)35X3^7LbgDV#}%56 zLXzh}u`<>!ie{1q#X*!c6!pInveXE=s5gY^p(nZs(p*E1We3Fh6E(^n$Wx^zHbx-H zzY~)IfsR{3OL9kvSX{IeL874-Na^ns$uFQZWH=;R@(C#~LZmKGdQk_NggXf_ltHF# zng|(DFBCiIjPC@YT3lr4OCeQ*myEDDv3Nmdj9Dxdbg1>wQnAy~Xf#U2;v&OkR-{6x zqCu){n#eG_#X@PkSg!e!j8KSG?<3=3xi~=PvB$;YqQheni-!)MUMwy;yaut<(t%&w zNo6q|Zkn@ULJ0(~%N+fQOdamwDiPX@d zZWTjg)|sG;FYiNx(~6y~`>Ai;FZA4v;_-XfJxWodDd# zIqjDSAjC}@x@5)iw-LnKi%0TDzPhaIHJ zE}Dv-k7a8a7g(LqhO0vcHt_8gTvdV3t~t?6&1A%0eGOB;GJ?mK-pbM*aO&=!AIH&W ztl;<6<6;u|(wTYdZGP0n&tJ!j)58y( z>jx&XQU#7Tc?YrbHA`5~`C3jgd1}b)W98#?Tb_{j#6^5Gfa`U=W7znGt@20X*qFsu zbACD-PPVYR;*~facI)`^Eott3B@0#He2v;8nS%<}u)bz!0t0h-(+cck=sT$JhUPUF zrjYj;xc9Am_zvpEvWL27GjE{n5CrHc>KZ}Xw-P|XqZgA_Ap-k88)<7qfV|Gpgq?y6 z?FB<3==xd+Slr`Mx~58@xx=a(s8Z{t0DH%*n~6FAIX0#&zmulV1c8RWi>{+cT_g!q z<-HI~(Cd)p<~yz-=u=_f@xpfGXyc^edFV}Yc0ruK&DluJ*YZG4Y-eI-3dEN>^J-Gg zLZa^H^R1M8E)x7~%f5<`1v0^RX=|@1!}p6&@O$@#4^gonVs$?ERC*i@FUtj(Y0ikC zK>a~7-c_}uk_b2f(Q28$jgAYvL9&6G|LfrPfu{W`9AI73;K)FOd{;VV1lSA#Vbxdj zq5?^sFCi`Sv$UH7C>tWyikD>G9Wd%}83A~5)_s9e2r)N2x8UBuP}Agu=1vQ53W!2T zy1w_)8v;O06BQ~7`d#FIWe|4pw;2ikrlv{@TkgKd-?AZYXX*2a{-q9=7jpWW{A&>; z{+rdWv?(H6JJPR8A@W_fUgS@GWp<&)kF+w#T;1B!Z?tXOgoYX|-cQt`J&^j2hhqFg z%YO@E_cZv2w%`ZIU3;}lxF7uTC`7;KadYc^kOA3yn?qa8mjdCR*O^=E$NM3DpU>1v zwQC{&Msw@bW-9XYn$Wu6?nV%JutTf7_X>i+?=NeO>X8Tvk3P{F^AH?fsJ^)6-hDqv za3G?^-W!Erag(j3Duy9w%<7cTG8JnOJT}x`(jxWibU}pHZ9_r}{JauDWySUp|B&eewx6h90xz{N8^b`9e)>eKnmR>VSo`Q6$h1Dn!KED=+KNyu zhp0Q>&D$MQb0^g0o;pRP8!wO53V2EDhPlOK{rVs-yO^C5t^Kk1HTtZmb`RQ=<}{8U z9#J)!mq!((b0q~1L_J=cB3$ z`7(n;x=g`~&w2?%CU25C#Q~brZW~OdK;=16K7t_sQz&yxvZy8bs_4%uScS-e-}BTP zU;I_`7L%M#a^oO&y)Ts}jrEZcJAf6$8sGxv6@pic0nO|`L7n10IYja*!X{~yX*GyV zmYb4)LuBBmtIrm4*&y!-Myi%YGpJNt5ZO4_YOpmcZ1G(XPy)ND?OYmM@U@Hf$~9LK zoTy)~Ux^0&3ptb256V!s7-r0jJh?#10vCG7h8m65H8H>dPwwk2s5Y9^thsCaF*|&f z@5u`%zKFO+W@ct&mTnI!K}Bk_k-mQXj{&)}Gcz+Y0QfBX2^Tb|QAyIXRaI3~5nAEJ zwsRn{sgQEVkS?2I;06&RacgGZHw@P1uQ2VT8l;)kZ1f;uB1 zAHvw!bV`yncz>+p$e04wa+YNHSO68JU84C!V==&pRW z3*gAi*1CUrx80gC>1AqbqV(O(#_?>YP?*~CeR1-&A$M_V%a(iej1AeKsyIc4Ibq?l zz&Ps}O*n!2kYd1R@FWa7wY~EOv;kG!Hy4Y0$oK;=5-eOcwC)??gjTLIhuwJ(Hz3kH zz%NHUo~j*-AN2cJ`d}IAa!Zq>%rqj zifMb5#3R{(=TWhTLg$Mn$}w8i^R*{F?KXHaFw{c{C~Afs%c3oLYs-aiG`4Bg&E3@m zH3t1S{)Sdn(y3gGRrTTgSgsj=l?k-)qOv+T{<8`+WT%Vbrc!Ej3H;vBQJ=^3_bHdz zCpr2(;4&Y!C>e`6_pWLVNSEL8QFwA1ri9<>#_L?M0sjpF;7P8@owwBStkt^ADp>oY zr2?2x-gu1~2%kqi)gPuCbe!YiBAi_titKs(E01G`_iR?#3XpC@AV1%uyCzI%JExp) z{eR&J{~2XD$f?rR2FLe?T1MRkm< z^eOmJs~`F+b|?il+qf607IXEHo@iuzuY&z5|Noea;4dOL>_iJi){czF;YgzzhttPx zk5YZKWc=o?`L~d8-~yLa>v?Hc_27jgl|l*P9xy#e74pBTV3P?6J@ z|D$s-CFw5*HaJ`vY)>DFQ&JbHVTEQ3sKl9R1pkO9jOXHMr|as|dfQGhpj5 zJ#hfW;r{KvQeGd{`n{I*zV^xkdgSgBaCu-?O-l^tei$lyz$84jMmZce|Gb|l{c}U? zUS!s?6yZI2=7|cRA$Dt!;aXs;q^BpH_(vHKitN2!Q_LoG6N@vV|Cx8dt=%~A)b|3%R+STGIm9ICz^LV2aW;SDY) zxKC8KNEIJSu@`$hiV9dY{seXl>;_>VMjnFRD2gZUj6CgaRCNKN6+aGe#uAf4V_-S0 z*9k{?)1bxh&_9Yc8eCo6jLBV*$cAohM7^|a8AW0s>xt~T*)0&eHC6nuci@|{-j2dX z`_?A2JaDwl8$165Bvy6(i+~V*Vqv%g3CA?U<9>F|+)fz#PXIll>wxgNq*=^wBHJm6 zzux-3m)1B6X7%D;$|y`YZYuR0bVOp$xBLhJZ_?@R>^;?xSV=gI(w2=e_s$MF(hYcq z<7%;es73*Da6T~a=57Kvj00lLd>p?Mq#{_q{Xk0=yaq7vJCVT2Q2-rJL-q;`vxWT! zfOlmwXFQowJ%<4)=*$fq^=iWJxX=1~pTGadhO3+JIDi&v&zp(1hg1B>R~29Y0I9V9 W;k?mM5Xg3Rds(@is=+`20001pNVXLK literal 0 HcmV?d00001 diff --git a/assets/filters/dog_brown_nose.webp b/assets/filters/dog_brown_nose.webp new file mode 100644 index 0000000000000000000000000000000000000000..7716995089c34ec6d58f942344d81d52c71c0db6 GIT binary patch literal 5904 zcmV+r7w_m&Nk&Ep7XScPMM6+kP&il$0000G0002T0052v06|PpNb3v$01Y7p2?C%9 zBclHiF9tvo2qfpvy+cG~7Lp`MQY6VeGP9s*@c;j#<;Kd4@P%tvitH_70w%!Qwe4vn ziMI7TuOvGtv$(pdCo?nioIjtLc{W2)Rz`%wmfrW7kQfma6Fx=61R(rBXAqQlGX_s3 zA`GG%C<}-(NE9Jb^b{au!4rleloaMEd9RQn!c;_F0>pdW}O)p;8)J23rn85IPA6$r20{Yhk`5k*s`m?z8458%n?H zHnaK8rBa6N(gt63b(1V3)7V(MF}+^~qsg9sn}$Bg(&FuARp#qq zvDijyiwEmz@p{#7#~#$sd9JM{pBmyrMnq;{!2#(!y{WAzzgb#wOzczM2-%bo6Z$+h zc6q9pl?Z?K`M5aW22=XxOtH(w*Ov{{hl&0v2FBzzo&CeZ@@5${$Us9-W(WYFl0(@k zD4{VkqDO4{VinMXcj(T#v-Cv`G=7K(z7`M|5K30-@OAOVuq%jJBzO2yPX z1bsLNOp;7Sv+&(FUxcD$qf9tV3KVuNRx>|;m|LsfYJ4Dw3?xP2!l=6VB{7o$0Z=$( zIha7rXSpx_Zfin7I&wsm4NOAW)rU7O+bIm(Ljj;vFXQaNS$@4RHTQbt1WYm8Dvscr zpBfqs5~92C-$jkv$w$QxKJK%y8LD*5NJ3H!%opFmR%Qr@;EpQ1?b_Ha;@NVhxNrpG zSP@WY;Oe`JSA$7H03dKzrGSKpckIvRd5&pv=@=0u3CUS?VTz0h?zBK3hn+Y3?iUz+ za2#V}%qR=)C=e0cbE(xSO80*LT^B@%M<;~YB*+gFBXI8pNf;-rpUtWjNJoT#jGVdI z;%FlySjq4y$~?`u^Ee)2D9f_5FPdUx46LOfA{I|KQ;pEE0Lf;P%-x(3!I}zAd-oz) z8+arFIVuKMAtP8@My;1mAAf zSct)XvYV!hb?~srB$J)&m!=Y;RRRJ%E?;z?kF&%>5`lyUl)<{U`sviZd)a}9Vjuxo zEW!^oGwT-9z@PZi;Lu?R6vaf`(J!tbS~tjZ|EON5frlU@qgj@GpD6Fk#Lf{u}@H zYMNZwYjKXuWZ(kyWVia_Cq8Kq#61e`B+LpC=?Zkazx?68(GvDoBvcmaL6>q-rcE_7+<%bfrgnn8&pGK9K=GE# zl!pouk9OhbnteHPEx{G<~yJ*!>6yfZ4hTePjG~^Sq<(CpfdUchX4T zfKM0Sj#cD6fV*%TvqqqE=jZwIjk4(;0tC0yxX?LF1s7vZnY=3rP9_IO4!W0A+otWWX1~Z^52@HKm<+pNo_IoO@^}Eptu?O~DG{f%cH6Tq`Ry*!ZfQ8F z1NQY3vs=)kef84PX=Tx#eOq)3CGz|o{j>tQ;cSxPMo49qb{n2nVw?AAH-&COEOQ;o ziG?7aRv)JuA*sBbn|NAT)8@Md-^ESn_04%n)M*tQ>idsVbzMit7AiQo!^fy+!VOg- zoH)IurY@ixP`NbM=>@TrsID8J%Wc95P9)E}xQ^=FYUT+B_^h@Gx+)muqE9%%gWC2` z;A)HMvMY%vn8g%fq^^>@=7LjPBA6sxOoZ@QZdI|6*dmLZKTE8gj3u?-Pm1cmP;Fo@7d0j-BheVX^B^Gbl2fQ&%~^es1DP*0#gE6LBRO4=xNk z#UhPw&gV&2AkK*BB*!VXEZ%TK0%0wk zU}7q2p&PW(>H>ChNwLc$aeZL;}#>4fDB=b`vsnY&+Ab6)HsyH`my0XC#k}%n1{Nq?0-}5xbUW zC2piP{p-iGS<(>^ASB0bVcq+Z)0CT%7)J%ozg?V}=|-`A@YUl49u5C*Zr$W{_sgGc z-~7b;eyAw(Ap{){T7B_ml;K8P;_Q_i$560v8=Secc=2}+zx%WA`O_c1c>KlpeEaI> zF6CpG>qyAb%R%p*H!@3I!`5_oAq!B0kE@x{zW(ug@&5g$iE;^S~5{#z^Glvr^5*o?b7zb6=-vhQ;fczH75O-!isDvp6t0 zHda>|w`8=oIYUZ398qvpxiJ?!(-1C5P)kTnM4qrld$HaME_fPp}Ue&FM2OxQkkLxz>LN;3@d%MG)2cm@`#I6ss6g431 zp%m{Iu#JBGt6&EM_bp=J<6S@)*2uQd)mzNw5|15zMjn% zr7-Q6II5oa)7=25<7XdpfwVV*qfQOky4O9Xk_~(D+<8IcGdl6_cJYnD-3zL6Y*jv6osc{T+54xl&5I9`XW@(XT>wo>^Y%x#Z z9>{#+lX28NP$i}ApO98C@?IhoJ~1zrW?loq0|Jaz=8FYB|CgL+8N5Fxe@A4vL+`Xixux4?dEACkytq!x*>TI2W0e&kn7>yMvFAHmP z;clwdA?z1kcjFX(XYP}g#k6_8*p6{eP+;49mD|F9;9acIZXt#|5SIEbp?U~M1U?S- z?$=SrF#@^+sYPq!Q+w#Cj-mSkA}Z1}rf2mB-c4JlsQ)GkfIOT`wn)U}5{|08t;_Ho zk8A7#fw)z)U6`E>bNjA$YZvZcfTF_0{-a^5V1Nn>!l^(g0ZnvMAPhp6*#*6YaC{}7cDDU^H4Bfa0uoFR z0goZ3mt7a*1cUYj5D^+T#a}E(OlS&8MZv^e$WU_#M>#yUrmmbl9NOq%o)rMuVv6Ah z^E9~>#9k}L>Ea_xPcD*sTkL4E3>Bt{9)Y8+(8N~f@m*_pIEzQkCabvoL5-tB*n5v{ zF~jh%BK6xj)rw$X6(k(+RgkvwN zdJUZsGjDC4YRgE=7+c^VMG;YHOfVuy3Brd_h;X9N9O$gcOXLXQP(=u$%mN^S_;896 z7#WTnWH1cEfeVBPi1@II5CMpQi2v6M09H^qAod9W0B}J7odGJy0FMAZokpEWrKBRE zDO5~ouoH=64Z0#XLo526m|Jr*1ezSFp{@eQie-{4_|NsAIU;(Z7 z^T5jdD0dR<`b4%V7){+_}dUo4d#HC`^~Sw%1v! zt4+N2a(1R+!#Avu{gcb&n^hB~ML>V|^(uOy75sldA)Zx$Hqc&n&E3ELzv>J`s3%|| zIV5{tsn%t%&yydzn8g`L1Yi8XHDU%DeLbYt*_(-)6M4_wv_P_SGCWdOvEDYI>bqo= znl#N4j7R9Gryko3p#4;X&-@$C(tP5U9=A_PjUM69f&X1!0KPad==sB-V$P_7;NAGcxR1aR6BJQUiMw??Buu z8k)Rzhn(!elKW<5%JY{yr}OU}_Q7YeRv2quHJ3xW z7~?pQ&g8Ou4u9u;=rxt?U6K1%K#)%)KRA~2Bl5BEaJJW~+$*hanN#iHM|DWgFq2}U zZDZ1NQ{e6@UY~OKXD49%`CV_VIA<)0D6Z3K46MN^{0092`KmhCf ztIP3w-5iel0hVX0#8mqcMcg|>rM~K{;7MI3vkb)`LIlhD1_0E!{3Q~=IlbG>9}R?u z&LOYhRI&%~aUYo~*Qf87SP{u(`LqVXr7y)1%S+2V%{41|)#!I4%=kf_1>u{VX)Fw! zZPOP?L+XCXn}i;i?oxCo*6y`WT;M3!M>b~?T+#$AoxqGk=wCxL@JsRd>C6>C|3J)R z!Jj%~U-rp}Kv2G#GBRSLLKYQgWzLl<)+ZJPDlTmWPu-n#IHzK$qWCTwwRB7AJp1{6 z8)a#jN_c-OiQUZvn&NS3cE#vC+dl4^_>d_U$nDgXW=d@UhCO8nGUq^fMMSJrBL{%79mtm4IuCdW>UpQ= z9R2PNaST9TS(yn=WK$;X8Tj_U`kIsD+?L^w+;nQKN2P5M|ry0^!F@Dvjdv5}$VU_!^gwb;QpoRuhBi zPQN#ZErJ|lx$#i~FV-S)v{QTS_TH$6a3q9Z?L?Aj+Zz!I^BAW5^sa)x{R|AzOsraaWBL%4uJ3N(@3RseuykmODX_VNymhePLwn@^k z5CaRmQ=_ro(;PK(E+v#W^Kl+PYN4C3j^E0g`E}5oCfQ2E&Gxq}La#ADNtA%4$i!00 zE|HmRRNU*Q!+^JQdxNFjGYE*PF?rmOz1%bpd5Jpya>5BR596x8YSrpOzPt+%j~r?P z808e!f0o!;E1>MwPt*7f)ASBgU@5d42TY@!N#eeM+%3#-ss86%@eP_Ly=lUPF-}^q zc=#dBW8VY#k+UwoP1c%nz_*ZUehcPrP9N5l<#F$|p8Eo9LA*l?(K%Dy5{cL`Op*`k z9n#Mi(2eJ@)0#l3=%VwlUD!DeAP*tTX^5R~Annj~U63Xu0$Hfa`+sBsHy~V+xp;~y z-4_U#!7_IB33bkP0N0Ya|w#b1g zr%T0O@=qO<>{$)gDK&?qI?493G{qCsqXiaDBjRK&Bf^``2NElB>P5J^o$b{1y=}W zOt*v9BW<*d50zZbE;8ZscAG>QrHFfP+oUtPl9`3LHxI7 z@<|boPAFxSV*P)jxDa(deyj2Rt?Jh3YZocCD?8{ZcZU~ZC58(Q*JwiqJPXK$)tvow zZGTJ|AH6o43GU9n;v0QhIqDynqu|gYII5@y8%wc@#t2-X%W=C227ZZw zi54}C_?->M|NBm{ILJ$g0SaY{9J;wc(tv|isMj%EKrA6B+5h`{RpS~wrKIaSj!(C* z@A>ltu&-)UoNSLL>w)8HL!nj>@gPA^e^oZ56J%99a_8JtJ@7pwSWhOTC}dZN0GHPm zm}5VI9xEUyP7l3QRb}JH5uXY;Wr{IYnXQ*+gqw2ao_jH_a%PU-sCf?dxZQUSxN7Gg zHw4MZv5nqGlVP8;U6twGy&PYB1TzeyIY*pGs=rYBDo|#Z{%wlI@C8mD0AU9lAtrQw zien$j{=&6D?=lXaQRb4kei^_NdUe&&*i}UQcS(0eYN{@t1 z29^AGnDJwIM#p#n1d+Nw+toATnOn3z01JWySbI^G=MEpP`{`W2|9>>n;;rW0U1qjG0001L;7k7i literal 0 HcmV?d00001 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 301b4b9..b0cfa92 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -136,6 +136,10 @@ PODS: - google_mlkit_commons (0.11.0): - Flutter - MLKitVision + - google_mlkit_face_detection (0.13.1): + - Flutter + - google_mlkit_commons + - GoogleMLKit/FaceDetection (~> 7.0.0) - GoogleAdsOnDeviceConversion (3.2.0): - GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Logger (~> 8.1) @@ -169,6 +173,9 @@ PODS: - GoogleMLKit/BarcodeScanning (7.0.0): - GoogleMLKit/MLKitCore - MLKitBarcodeScanning (~> 6.0.0) + - GoogleMLKit/FaceDetection (7.0.0): + - GoogleMLKit/MLKitCore + - MLKitFaceDetection (~> 6.0.0) - GoogleMLKit/MLKitCore (7.0.0): - MLKitCommon (~> 12.0.0) - GoogleToolboxForMac/Defines (4.2.1) @@ -251,6 +258,9 @@ PODS: - GoogleUtilities/Logger (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0) - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLKitFaceDetection (6.0.0): + - MLKitCommon (~> 12.0) + - MLKitVision (~> 8.0) - MLKitVision (8.0.0): - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" @@ -357,6 +367,7 @@ DEPENDENCIES: - gal (from `.symlinks/plugins/gal/darwin`) - google_mlkit_barcode_scanning (from `.symlinks/plugins/google_mlkit_barcode_scanning/ios`) - google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`) + - google_mlkit_face_detection (from `.symlinks/plugins/google_mlkit_face_detection/ios`) - GoogleUtilities - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`) @@ -398,6 +409,7 @@ SPEC REPOS: - MLImage - MLKitBarcodeScanning - MLKitCommon + - MLKitFaceDetection - MLKitVision - nanopb - PromisesObjC @@ -454,6 +466,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/google_mlkit_barcode_scanning/ios" google_mlkit_commons: :path: ".symlinks/plugins/google_mlkit_commons/ios" + google_mlkit_face_detection: + :path: ".symlinks/plugins/google_mlkit_face_detection/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" in_app_purchase_storekit: @@ -518,6 +532,7 @@ SPEC CHECKSUMS: gal: baecd024ebfd13c441269ca7404792a7152fde89 google_mlkit_barcode_scanning: 8f5987f244a43fe1167689c548342a5174108159 google_mlkit_commons: 2abe6a70e1824e431d16a51085cb475b672c8aab + google_mlkit_face_detection: 754da2113a1952f063c7c5dc347ac6ae8934fb77 GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f GoogleAppMeasurement: 3bf40aff49a601af5da1c3345702fcb4991d35ee GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 @@ -533,6 +548,7 @@ SPEC CHECKSUMS: MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56 MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2 MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d + MLKitFaceDetection: 2a593db4837db503ad3426b565e7aab045cefea5 MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 no_screenshot: 89e778ede9f1e39cc3fb9404d782a42712f2a0b2 diff --git a/lib/src/views/camera/camera_preview_components/camera_preview.dart b/lib/src/views/camera/camera_preview_components/camera_preview.dart index 5d9d4bb..01c4948 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview.dart @@ -37,7 +37,18 @@ class MainCameraPreview extends StatelessWidget { .cameraController!.value.previewSize!.width, child: CameraPreview( mainCameraController.cameraController!, - child: mainCameraController.customPaint, + child: Stack( + children: [ + if (mainCameraController.customPaint != null) + Positioned.fill( + child: mainCameraController.customPaint!, + ), + if (mainCameraController.facePaint != null) + Positioned.fill( + child: mainCameraController.facePaint!, + ), + ], + ), ), ), ), diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index dadb1fd..ce13173 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -21,6 +21,7 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/qr.dart'; import 'package:twonly/src/utils/screenshot.dart'; import 'package:twonly/src/utils/storage.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/face_filters.dart'; import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart'; import 'package:twonly/src/views/camera/camera_preview_components/permissions_view.dart'; import 'package:twonly/src/views/camera/camera_preview_components/send_to.dart'; @@ -506,6 +507,36 @@ class _CameraPreviewViewState extends State { }); } + Future pressSideButtonLeft() async { + if (!mc.isSelectingFaceFilters) { + return pickImageFromGallery(); + } + if (mc.currentFilterType.index == 1) { + mc.setFilter(FaceFilterType.none); + setState(() { + mc.isSelectingFaceFilters = false; + }); + return; + } + mc.setFilter(mc.currentFilterType.goLeft()); + } + + Future pressSideButtonRight() async { + if (!mc.isSelectingFaceFilters) { + setState(() { + mc.isSelectingFaceFilters = true; + }); + } + if (mc.currentFilterType.index == FaceFilterType.values.length - 1) { + mc.setFilter(FaceFilterType.none); + setState(() { + mc.isSelectingFaceFilters = false; + }); + return; + } + mc.setFilter(mc.currentFilterType.goRight()); + } + Future startVideoRecording() async { if (mc.cameraController != null && mc.cameraController!.value.isRecordingVideo) { @@ -736,15 +767,19 @@ class _CameraPreviewViewState extends State { children: [ if (!_isVideoRecording) GestureDetector( - onTap: pickImageFromGallery, + onTap: pressSideButtonLeft, child: Align( child: Container( height: 50, width: 80, padding: const EdgeInsets.all(2), - child: const Center( + child: Center( child: FaIcon( - FontAwesomeIcons.photoFilm, + mc.isSelectingFaceFilters + ? mc.currentFilterType.index == 1 + ? FontAwesomeIcons.xmark + : FontAwesomeIcons.arrowLeft + : FontAwesomeIcons.photoFilm, color: Colors.white, size: 25, ), @@ -771,10 +806,39 @@ class _CameraPreviewViewState extends State { : Colors.white, ), ), + child: mc.currentFilterType.preview, ), ), ), - if (!_isVideoRecording) const SizedBox(width: 80), + if (!_isVideoRecording) + if (isFront) + GestureDetector( + onTap: pressSideButtonRight, + child: Align( + child: Container( + height: 50, + width: 80, + padding: const EdgeInsets.all(2), + child: Center( + child: FaIcon( + mc.isSelectingFaceFilters + ? mc.currentFilterType.index == + FaceFilterType + .values.length - + 1 + ? FontAwesomeIcons.xmark + : FontAwesomeIcons.arrowRight + : FontAwesomeIcons + .faceGrinTongueSquint, + color: Colors.white, + size: 25, + ), + ), + ), + ), + ) + else + const SizedBox(width: 80), ], ), ], diff --git a/lib/src/views/camera/camera_preview_components/face_filters.dart b/lib/src/views/camera/camera_preview_components/face_filters.dart new file mode 100644 index 0000000..f4826a0 --- /dev/null +++ b/lib/src/views/camera/camera_preview_components/face_filters.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/views/camera/painters/face_filters/beard_filter_painter.dart'; +import 'package:twonly/src/views/camera/painters/face_filters/dog_filter_painter.dart'; + +enum FaceFilterType { + none, + dogBrown, + beardUpperLip, +} + +extension FaceFilterTypeExtension on FaceFilterType { + FaceFilterType goRight() { + final nextIndex = (index + 1) % FaceFilterType.values.length; + return FaceFilterType.values[nextIndex]; + } + + FaceFilterType goLeft() { + final prevIndex = (index - 1 + FaceFilterType.values.length) % + FaceFilterType.values.length; + return FaceFilterType.values[prevIndex]; + } + + Widget get preview { + switch (this) { + case FaceFilterType.none: + return Container(); + case FaceFilterType.dogBrown: + return DogFilterPainter.getPreview(); + case FaceFilterType.beardUpperLip: + return BeardFilterPainter.getPreview(); + } + } +} diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart index a67d2fa..5fb9f8c 100644 --- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart @@ -5,6 +5,7 @@ import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; +import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -15,7 +16,11 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/qr.dart'; import 'package:twonly/src/utils/screenshot.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/face_filters.dart'; import 'package:twonly/src/views/camera/painters/barcode_detector_painter.dart'; +import 'package:twonly/src/views/camera/painters/face_filters/beard_filter_painter.dart'; +import 'package:twonly/src/views/camera/painters/face_filters/dog_filter_painter.dart'; +import 'package:twonly/src/views/camera/painters/face_filters/face_filter_painter.dart'; class ScannedVerifiedContact { ScannedVerifiedContact({ @@ -45,6 +50,22 @@ class MainCameraController { Map scannedNewProfiles = {}; String? scannedUrl; GlobalKey zoomButtonKey = GlobalKey(); + bool isSelectingFaceFilters = false; + + final BarcodeScanner _barcodeScanner = BarcodeScanner(); + final FaceDetector _faceDetector = FaceDetector( + options: FaceDetectorOptions( + enableContours: true, + enableLandmarks: true, + ), + ); + bool _isBusy = false; + bool _isBusyFaces = false; + CustomPaint? customPaint; + CustomPaint? facePaint; + + FaceFilterType _currentFilterType = FaceFilterType.beardUpperLip; + FaceFilterType get currentFilterType => _currentFilterType; Future closeCamera() async { contactsVerified = {}; @@ -73,10 +94,9 @@ class MainCameraController { selectedCameraDetails = opts.$1; cameraController = opts.$2; } - if (cameraController?.description.lensDirection == - CameraLensDirection.back) { - await cameraController?.startImageStream(_processCameraImage); - } + isSelectingFaceFilters = false; + setFilter(FaceFilterType.none); + await cameraController?.startImageStream(_processCameraImage); zoomButtonKey = GlobalKey(); setState(); return cameraController; @@ -95,13 +115,23 @@ class MainCameraController { } final tmp = cameraController; cameraController = null; + facePaint = null; + customPaint = null; await tmp!.dispose(); await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false); } - final BarcodeScanner _barcodeScanner = BarcodeScanner(); - bool _isBusy = false; - CustomPaint? customPaint; + void setFilter(FaceFilterType type) { + _currentFilterType = type; + if (_currentFilterType == FaceFilterType.none) { + faceFilterPainter = null; + facePaint = null; + _isBusyFaces = false; + } + setState(); + } + + FaceFilterPainter? faceFilterPainter; final Map _orientations = { DeviceOrientation.portraitUp: 0, @@ -113,7 +143,16 @@ class MainCameraController { void _processCameraImage(CameraImage image) { final inputImage = _inputImageFromCameraImage(image); if (inputImage == null) return; - _processImage(inputImage); + _processBarcode(inputImage); + // check if front camera is selected + if (cameraController?.description.lensDirection == + CameraLensDirection.front) { + if (_currentFilterType != FaceFilterType.none) { + _processFaces(inputImage); + } + } else { + _processBarcode(inputImage); + } } InputImage? _inputImageFromCameraImage(CameraImage image) { @@ -175,7 +214,7 @@ class MainCameraController { ); } - Future _processImage(InputImage inputImage) async { + Future _processBarcode(InputImage inputImage) async { if (_isBusy) return; _isBusy = true; final barcodes = await _barcodeScanner.processImage(inputImage); @@ -255,4 +294,48 @@ class MainCameraController { _isBusy = false; setState(); } + + Future _processFaces(InputImage inputImage) async { + if (_isBusyFaces) return; + _isBusyFaces = true; + final faces = await _faceDetector.processImage(inputImage); + if (inputImage.metadata?.size != null && + inputImage.metadata?.rotation != null && + cameraController != null) { + if (faces.isNotEmpty) { + CustomPainter? painter; + if (_currentFilterType == FaceFilterType.dogBrown) { + painter = DogFilterPainter( + faces, + inputImage.metadata!.size, + inputImage.metadata!.rotation, + cameraController!.description.lensDirection, + ); + } else if (_currentFilterType == FaceFilterType.beardUpperLip) { + painter = BeardFilterPainter( + faces, + inputImage.metadata!.size, + inputImage.metadata!.rotation, + cameraController!.description.lensDirection, + ); + } + + if (painter != null) { + facePaint = CustomPaint(painter: painter); + // Also set the correct FaceFilterPainter reference if needed for other logic, + // though currently facePaint is what's used for display. + if (painter is FaceFilterPainter) { + faceFilterPainter = painter; + } + } else { + facePaint = null; + faceFilterPainter = null; + } + } else { + facePaint = null; + } + } + _isBusyFaces = false; + setState(); + } } diff --git a/lib/src/views/camera/painters/face_filters/beard_filter_painter.dart b/lib/src/views/camera/painters/face_filters/beard_filter_painter.dart new file mode 100644 index 0000000..5451f41 --- /dev/null +++ b/lib/src/views/camera/painters/face_filters/beard_filter_painter.dart @@ -0,0 +1,174 @@ +import 'dart:async'; +import 'dart:math'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/views/camera/painters/coordinates_translator.dart'; +import 'package:twonly/src/views/camera/painters/face_filters/face_filter_painter.dart'; + +class BeardFilterPainter extends FaceFilterPainter { + BeardFilterPainter( + super.faces, + super.imageSize, + super.rotation, + super.cameraLensDirection, + ) { + _loadAssets(); + } + + static ui.Image? _beardImage; + static bool _loading = false; + + static Future _loadAssets() async { + if (_loading || _beardImage != null) return; + _loading = true; + try { + _beardImage = await _loadImage('assets/filters/beard_upper_lip.webp'); + } catch (e) { + Log.error('Failed to load filter assets: $e'); + } finally { + _loading = false; + } + } + + static Future _loadImage(String assetPath) async { + final data = await rootBundle.load(assetPath); + final list = Uint8List.view(data.buffer); + final completer = Completer(); + ui.decodeImageFromList(list, completer.complete); + return completer.future; + } + + @override + void paint(Canvas canvas, Size size) { + if (_beardImage == null) return; + + for (final face in faces) { + final noseBase = face.landmarks[FaceLandmarkType.noseBase]; + final mouthLeft = face.landmarks[FaceLandmarkType.leftMouth]; + final mouthRight = face.landmarks[FaceLandmarkType.rightMouth]; + final bottomMouth = face.landmarks[FaceLandmarkType.bottomMouth]; + + if (noseBase != null && + mouthLeft != null && + mouthRight != null && + bottomMouth != null) { + final noseX = translateX( + noseBase.position.x.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + final noseY = translateY( + noseBase.position.y.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + + final mouthLeftX = translateX( + mouthLeft.position.x.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + final mouthLeftY = translateY( + mouthLeft.position.y.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + + final mouthRightX = translateX( + mouthRight.position.x.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + final mouthRightY = translateY( + mouthRight.position.y.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + + final mouthCenterX = (mouthLeftX + mouthRightX) / 2; + final mouthCenterY = (mouthLeftY + mouthRightY) / 2; + + final beardCenterX = (noseX + mouthCenterX) / 2; + final beardCenterY = (noseY + mouthCenterY) / 2; + + final dx = mouthRightX - mouthLeftX; + final dy = mouthRightY - mouthLeftY; + final angle = atan2(dy, dx); + + final mouthWidth = sqrt(dx * dx + dy * dy); + final beardWidth = mouthWidth * 1.5; + + final yaw = face.headEulerAngleY ?? 0; + final scaleX = cos(yaw * pi / 180).abs(); + + _drawImage( + canvas, + _beardImage!, + Offset(beardCenterX, beardCenterY), + beardWidth, + angle, + scaleX, + ); + } + } + } + + void _drawImage( + Canvas canvas, + ui.Image image, + Offset position, + double width, + double rotation, + double scaleX, + ) { + canvas + ..save() + ..translate(position.dx, position.dy) + ..rotate(rotation) + ..scale(scaleX, -1); + + final srcRect = + Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()); + + final aspectRatio = image.width / image.height; + final dstWidth = width; + final dstHeight = width / aspectRatio; + + final dstRect = Rect.fromCenter( + center: Offset.zero, + width: dstWidth, + height: dstHeight, + ); + + canvas + ..drawImageRect(image, srcRect, dstRect, Paint()) + ..restore(); + } + + static Widget getPreview() { + return Preview( + child: Padding( + padding: const EdgeInsets.all(8), + child: Image.asset( + 'assets/filters/beard_upper_lip.webp', + fit: BoxFit.contain, + ), + ), + ); + } +} diff --git a/lib/src/views/camera/painters/face_filters/dog_filter_painter.dart b/lib/src/views/camera/painters/face_filters/dog_filter_painter.dart new file mode 100644 index 0000000..bab086f --- /dev/null +++ b/lib/src/views/camera/painters/face_filters/dog_filter_painter.dart @@ -0,0 +1,242 @@ +import 'dart:async'; +import 'dart:math'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/views/camera/painters/coordinates_translator.dart'; +import 'package:twonly/src/views/camera/painters/face_filters/face_filter_painter.dart'; + +class DogFilterPainter extends FaceFilterPainter { + DogFilterPainter( + super.faces, + super.imageSize, + super.rotation, + super.cameraLensDirection, + ) { + _loadAssets(); + } + + static ui.Image? _earImage; + static ui.Image? _noseImage; + static bool _loading = false; + + static Future _loadAssets() async { + if (_loading || (_earImage != null && _noseImage != null)) return; + _loading = true; + try { + _earImage = await _loadImage('assets/filters/dog_brown_ear.webp'); + _noseImage = await _loadImage('assets/filters/dog_brown_nose.webp'); + } catch (e) { + Log.error('Failed to load filter assets: $e'); + } finally { + _loading = false; + } + } + + static Future _loadImage(String assetPath) async { + final data = await rootBundle.load(assetPath); + final list = Uint8List.view(data.buffer); + final completer = Completer(); + ui.decodeImageFromList(list, completer.complete); + return completer.future; + } + + @override + void paint(Canvas canvas, Size size) { + if (_earImage == null || _noseImage == null) return; + + for (final face in faces) { + final faceContour = face.contours[FaceContourType.face]; + final noseBase = face.landmarks[FaceLandmarkType.noseBase]; + + if (faceContour != null && noseBase != null) { + final points = faceContour.points; + if (points.isEmpty) continue; + + final upperPoints = + points.where((p) => p.y < noseBase.position.y).toList(); + + if (upperPoints.isEmpty) continue; + + Point? leftMost; + Point? rightMost; + Point? topMost; + + for (final point in upperPoints) { + if (leftMost == null || point.x < leftMost.x) { + leftMost = point; + } + if (rightMost == null || point.x > rightMost.x) { + rightMost = point; + } + if (topMost == null || point.y < topMost.y) { + topMost = point; + } + } + + if (leftMost == null || rightMost == null || topMost == null) continue; + + final leftEarX = translateX( + leftMost.x.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + final leftEarY = translateY( + topMost.y.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + + final rightEarX = translateX( + rightMost.x.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + final rightEarY = translateY( + topMost.y.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + + final noseX = translateX( + noseBase.position.x.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + final noseY = translateY( + noseBase.position.y.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + + final dx = rightEarX - leftEarX; + final dy = rightEarY - leftEarY; + + final faceWidth = sqrt(dx * dx + dy * dy) * 1.5; + final angle = atan2(dy, dx); + + final yaw = face.headEulerAngleY ?? 0; + final scaleX = cos(yaw * pi / 180).abs(); + + final earSize = faceWidth / 2.5; + + _drawImage( + canvas, + _earImage!, + Offset(leftEarX, leftEarY + earSize * 0.3), + earSize, + angle, + scaleX, + ); + + _drawImage( + canvas, + _earImage!, + Offset(rightEarX, rightEarY + earSize * 0.3), + earSize, + angle, + scaleX, + isFlipped: true, + ); + + final noseSize = faceWidth * 0.4; + _drawImage( + canvas, + _noseImage!, + Offset(noseX, noseY + noseSize * 0.1), + noseSize, + angle, + scaleX, + ); + } + } + } + + void _drawImage( + Canvas canvas, + ui.Image image, + Offset position, + double size, + double rotation, + double scaleX, { + bool isFlipped = false, + }) { + canvas + ..save() + ..translate(position.dx, position.dy) + ..rotate(rotation); + if (isFlipped) { + canvas.scale(-scaleX, -1); + } else { + canvas.scale(scaleX, -1); + } + + final srcRect = + Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()); + final aspectRatio = image.width / image.height; + final dstWidth = size; + final dstHeight = size / aspectRatio; + + final dstRect = Rect.fromCenter( + center: Offset.zero, + width: dstWidth, + height: dstHeight, + ); + + canvas + ..drawImageRect(image, srcRect, dstRect, Paint()) + ..restore(); + } + + static Widget getPreview() { + return Preview( + child: Stack( + alignment: Alignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 25), + child: Image.asset( + 'assets/filters/dog_brown_nose.webp', + width: 25, + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/filters/dog_brown_ear.webp', + width: 20, + ), + const SizedBox(width: 15), + Transform.scale( + scaleX: -1, + child: Image.asset( + 'assets/filters/dog_brown_ear.webp', + width: 20, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/views/camera/painters/face_filters/face_filter_painter.dart b/lib/src/views/camera/painters/face_filters/face_filter_painter.dart new file mode 100644 index 0000000..b32ec7d --- /dev/null +++ b/lib/src/views/camera/painters/face_filters/face_filter_painter.dart @@ -0,0 +1,44 @@ +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; + +abstract class FaceFilterPainter extends CustomPainter { + FaceFilterPainter( + this.faces, + this.imageSize, + this.rotation, + this.cameraLensDirection, + ); + + final List faces; + final Size imageSize; + final InputImageRotation rotation; + final CameraLensDirection cameraLensDirection; + + @override + bool shouldRepaint(covariant FaceFilterPainter oldDelegate) { + return oldDelegate.imageSize != imageSize || + oldDelegate.faces != faces || + oldDelegate.rotation != rotation || + oldDelegate.cameraLensDirection != cameraLensDirection; + } +} + +class Preview extends StatelessWidget { + const Preview({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey.withValues(alpha: 0.2), + ), + child: Center( + child: child, + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 7df1a10..cc26ee5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -868,6 +868,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.0" + google_mlkit_face_detection: + dependency: "direct main" + description: + name: google_mlkit_face_detection + sha256: f336737d5b8a86797fd4368f42a5c26aeaa9c6dcc5243f0a16b5f6f663cfb70a + url: "https://pub.dev" + source: hosted + version: "0.13.1" graphs: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ce61f24..a0d05a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.0.85+85 +version: 0.0.86+86 environment: sdk: ^3.6.0 @@ -110,6 +110,7 @@ dependencies: hand_signature: ^3.0.3 flutter_sharing_intent: ^2.0.4 no_screenshot: ^0.3.1 + google_mlkit_face_detection: ^0.13.1 dependency_overrides: dots_indicator: @@ -203,5 +204,6 @@ flutter: - assets/animated_icons/ - assets/animations/ - assets/passwords/ + - assets/filters/ - CHANGELOG.md From d83e9a26c4c8afea7d23169d3a8f57b5e2ef9497 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 18 Jan 2026 02:43:09 +0100 Subject: [PATCH 02/17] fix: Apply platform-specific Y-axis scaling to face filter images. --- .../camera/painters/face_filters/beard_filter_painter.dart | 3 ++- .../camera/painters/face_filters/dog_filter_painter.dart | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/src/views/camera/painters/face_filters/beard_filter_painter.dart b/lib/src/views/camera/painters/face_filters/beard_filter_painter.dart index 5451f41..95f2b12 100644 --- a/lib/src/views/camera/painters/face_filters/beard_filter_painter.dart +++ b/lib/src/views/camera/painters/face_filters/beard_filter_painter.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; @@ -140,7 +141,7 @@ class BeardFilterPainter extends FaceFilterPainter { ..save() ..translate(position.dx, position.dy) ..rotate(rotation) - ..scale(scaleX, -1); + ..scale(scaleX, Platform.isAndroid ? -1 : 1); final srcRect = Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()); diff --git a/lib/src/views/camera/painters/face_filters/dog_filter_painter.dart b/lib/src/views/camera/painters/face_filters/dog_filter_painter.dart index bab086f..5ca873c 100644 --- a/lib/src/views/camera/painters/face_filters/dog_filter_painter.dart +++ b/lib/src/views/camera/painters/face_filters/dog_filter_painter.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; @@ -180,9 +181,9 @@ class DogFilterPainter extends FaceFilterPainter { ..translate(position.dx, position.dy) ..rotate(rotation); if (isFlipped) { - canvas.scale(-scaleX, -1); + canvas.scale(-scaleX, Platform.isAndroid ? -1 : 1); } else { - canvas.scale(scaleX, -1); + canvas.scale(scaleX, Platform.isAndroid ? -1 : 1); } final srcRect = From cd5deca6b6f95471cc06fa2c6d96d5410d7cdd71 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 18 Jan 2026 15:23:52 +0100 Subject: [PATCH 03/17] improving camera --- CHANGELOG.md | 6 +- .../camera_preview.dart | 19 +++ .../camera_preview_controller_view.dart | 49 -------- .../main_camera_controller.dart | 112 +++++++++++++----- .../views/camera/camera_qr_scanner.view.dart | 3 +- lib/src/views/camera/camera_send_to_view.dart | 3 +- lib/src/views/home.view.dart | 6 +- 7 files changed, 114 insertions(+), 84 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffabff6..503743c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ # Changelog -## 0.0.87 +## 0.0.89 -- Added basic support for face filters +- Adds option to manual focus in the camera +- Adds support to switch between front and back camera during video recording +- Adds basic face filters ## 0.0.86 diff --git a/lib/src/views/camera/camera_preview_components/camera_preview.dart b/lib/src/views/camera/camera_preview_components/camera_preview.dart index 01c4948..cde7474 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview.dart @@ -36,6 +36,7 @@ class MainCameraPreview extends StatelessWidget { height: mainCameraController .cameraController!.value.previewSize!.width, child: CameraPreview( + key: mainCameraController.cameraPreviewKey, mainCameraController.cameraController!, child: Stack( children: [ @@ -47,6 +48,24 @@ class MainCameraPreview extends StatelessWidget { Positioned.fill( child: mainCameraController.facePaint!, ), + if (mainCameraController.focusPointOffset != null) + Positioned( + top: mainCameraController.focusPointOffset!.dy - 40, + left: + mainCameraController.focusPointOffset!.dx - 40, + child: Container( + height: 80, + width: 80, + clipBehavior: Clip.antiAliasWithSaveLayer, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 1, + color: Colors.white.withAlpha(150), + ), + ), + ), + ) ], ), ), diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index ce13173..fe387ee 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -37,55 +37,6 @@ import 'package:url_launcher/url_launcher_string.dart'; int maxVideoRecordingTime = 60; -Future<(SelectedCameraDetails, CameraController)?> initializeCameraController( - SelectedCameraDetails details, - int sCameraId, - bool init, -) async { - var cameraId = sCameraId; - if (cameraId >= gCameras.length) return null; - if (init) { - for (; cameraId < gCameras.length; cameraId++) { - if (gCameras[cameraId].lensDirection == CameraLensDirection.back) { - break; - } - } - } - details.isZoomAble = false; - if (details.cameraId != cameraId) { - // switch between front and back - details.scaleFactor = 1; - } - - final cameraController = CameraController( - gCameras[cameraId], - ResolutionPreset.high, - enableAudio: await Permission.microphone.isGranted, - imageFormatGroup: - Platform.isAndroid ? ImageFormatGroup.nv21 : ImageFormatGroup.bgra8888, - ); - - await cameraController.initialize().then((_) async { - await cameraController.setZoomLevel(details.scaleFactor); - await cameraController.lockCaptureOrientation(DeviceOrientation.portraitUp); - await cameraController - .setFlashMode(details.isFlashOn ? FlashMode.always : FlashMode.off); - await cameraController - .getMaxZoomLevel() - .then((double value) => details.maxAvailableZoom = value); - await cameraController - .getMinZoomLevel() - .then((double value) => details.minAvailableZoom = value); - details - ..isZoomAble = details.maxAvailableZoom != details.minAvailableZoom - ..cameraLoaded = true - ..cameraId = cameraId; - }).catchError((Object e) { - Log.error('$e'); - }); - return (details, cameraController); -} - class SelectedCameraDetails { double maxAvailableZoom = 1; double minAvailableZoom = 1; diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart index 5fb9f8c..f710797 100644 --- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:camera/camera.dart'; import 'package:collection/collection.dart'; @@ -6,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -50,6 +52,7 @@ class MainCameraController { Map scannedNewProfiles = {}; String? scannedUrl; GlobalKey zoomButtonKey = GlobalKey(); + GlobalKey cameraPreviewKey = GlobalKey(); bool isSelectingFaceFilters = false; final BarcodeScanner _barcodeScanner = BarcodeScanner(); @@ -63,6 +66,7 @@ class MainCameraController { bool _isBusyFaces = false; CustomPaint? customPaint; CustomPaint? facePaint; + Offset? focusPointOffset; FaceFilterType _currentFilterType = FaceFilterType.beardUpperLip; FaceFilterType get currentFilterType => _currentFilterType; @@ -83,44 +87,96 @@ class MainCameraController { selectedCameraDetails = SelectedCameraDetails(); } - Future selectCamera(int sCameraId, bool init) async { + Future selectCamera(int sCameraId, bool init) async { initCameraStarted = true; - final opts = await initializeCameraController( - selectedCameraDetails, - sCameraId, - init, - ); - if (opts != null) { - selectedCameraDetails = opts.$1; - cameraController = opts.$2; - } - isSelectingFaceFilters = false; - setFilter(FaceFilterType.none); - await cameraController?.startImageStream(_processCameraImage); - zoomButtonKey = GlobalKey(); - setState(); - return cameraController; - } - Future toggleSelectedCamera() async { - if (cameraController == null) return; - // do not allow switching camera when recording - if (cameraController!.value.isRecordingVideo) { + var cameraId = sCameraId; + if (cameraId >= gCameras.length) { + Log.warn( + 'Trying to select a non existing camera $cameraId >= ${gCameras.length}', + ); return; } - try { - await cameraController!.stopImageStream(); - } catch (e) { - // Log.warn(e); + + if (init) { + for (; cameraId < gCameras.length; cameraId++) { + if (gCameras[cameraId].lensDirection == CameraLensDirection.back) { + break; + } + } } - final tmp = cameraController; - cameraController = null; + + selectedCameraDetails.isZoomAble = false; + if (selectedCameraDetails.cameraId != cameraId) { + // switched camera so reset the scaleFactor + selectedCameraDetails.scaleFactor = 1; + } + + if (cameraController == null) { + cameraController = CameraController( + gCameras[cameraId], + ResolutionPreset.high, + enableAudio: await Permission.microphone.isGranted, + imageFormatGroup: Platform.isAndroid + ? ImageFormatGroup.nv21 + : ImageFormatGroup.bgra8888, + ); + await cameraController?.initialize(); + await cameraController?.startImageStream(_processCameraImage); + } else { + await HapticFeedback.lightImpact(); + await cameraController?.setDescription(gCameras[cameraId]); + } + + await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor); + await cameraController + ?.lockCaptureOrientation(DeviceOrientation.portraitUp); + await cameraController?.setFlashMode( + selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off, + ); + selectedCameraDetails.maxAvailableZoom = + await cameraController?.getMaxZoomLevel() ?? 1; + selectedCameraDetails.minAvailableZoom = + await cameraController?.getMinZoomLevel() ?? 1; + selectedCameraDetails + ..isZoomAble = selectedCameraDetails.maxAvailableZoom != + selectedCameraDetails.minAvailableZoom + ..cameraLoaded = true + ..cameraId = cameraId; + facePaint = null; customPaint = null; - await tmp!.dispose(); + isSelectingFaceFilters = false; + setFilter(FaceFilterType.none); + zoomButtonKey = GlobalKey(); + setState(); + } + + Future onDoubleTap() async { await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false); } + Future onTapDown(TapDownDetails details) async { + final box = + cameraPreviewKey.currentContext?.findRenderObject() as RenderBox?; + if (box == null) return; + final localPosition = box.globalToLocal(details.globalPosition); + + focusPointOffset = Offset(localPosition.dx, localPosition.dy); + + final dx = localPosition.dx / box.size.width; + final dy = localPosition.dy / box.size.height; + + setState(); + + await HapticFeedback.lightImpact(); + await cameraController?.setFocusPoint(Offset(dx, dy)); + await cameraController?.setFocusMode(FocusMode.auto); + + focusPointOffset = null; + setState(); + } + void setFilter(FaceFilterType type) { _currentFilterType = type; if (_currentFilterType == FaceFilterType.none) { diff --git a/lib/src/views/camera/camera_qr_scanner.view.dart b/lib/src/views/camera/camera_qr_scanner.view.dart index c9fa6ea..32703cc 100644 --- a/lib/src/views/camera/camera_qr_scanner.view.dart +++ b/lib/src/views/camera/camera_qr_scanner.view.dart @@ -32,7 +32,8 @@ class QrCodeScannerState extends State { Widget build(BuildContext context) { return Scaffold( body: GestureDetector( - onDoubleTap: _mainCameraController.toggleSelectedCamera, + onDoubleTap: _mainCameraController.onDoubleTap, + onTapDown: _mainCameraController.onTapDown, child: Stack( children: [ MainCameraPreview( diff --git a/lib/src/views/camera/camera_send_to_view.dart b/lib/src/views/camera/camera_send_to_view.dart index 7dfefbf..cdda133 100644 --- a/lib/src/views/camera/camera_send_to_view.dart +++ b/lib/src/views/camera/camera_send_to_view.dart @@ -34,7 +34,8 @@ class CameraSendToViewState extends State { Widget build(BuildContext context) { return Scaffold( body: GestureDetector( - onDoubleTap: _mainCameraController.toggleSelectedCamera, + onDoubleTap: _mainCameraController.onDoubleTap, + onTapDown: _mainCameraController.onTapDown, child: Stack( children: [ MainCameraPreview( diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index b87ab11..b19210c 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -172,9 +172,9 @@ class HomeViewState extends State { Widget build(BuildContext context) { return Scaffold( body: GestureDetector( - onDoubleTap: offsetRatio == 0 - ? _mainCameraController.toggleSelectedCamera - : null, + onDoubleTap: + offsetRatio == 0 ? _mainCameraController.onDoubleTap : null, + onTapDown: offsetRatio == 0 ? _mainCameraController.onTapDown : null, child: Stack( children: [ MainCameraPreview(mainCameraController: _mainCameraController), From 2ef4566d694faa6dd8d6aa386fa9d5edfb9dd6e5 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 19 Jan 2026 23:15:28 +0100 Subject: [PATCH 04/17] improve image editor --- CHANGELOG.md | 2 + lib/src/database/daos/reactions.dao.dart | 4 +- lib/src/services/intent/links.intent.dart | 2 +- .../camera_preview.dart | 82 +- .../camera_preview_controller_view.dart | 4 +- .../face_filters.dart | 4 +- .../main_camera_controller.dart | 8 +- .../painters/barcode_detector_painter.dart | 0 .../painters/coordinates_translator.dart | 0 .../face_filters/beard_filter_painter.dart | 4 +- .../face_filters/dog_filter_painter.dart | 4 +- .../face_filters/face_filter_painter.dart | 0 ..._to_view.dart => camera_send_to.view.dart} | 0 .../views/camera/image_editor/data/data.dart | 710 ------------------ .../image_editor/layers/emoji_layer.dart | 173 ----- ...> share_image_contact_selection.view.dart} | 2 +- .../best_friends_selector.dart | 0 .../select_show_time.dart | 0 ...view.dart => share_image_editor.view.dart} | 15 +- .../action_button.dart | 0 .../data/image_item.dart | 0 .../data/layer.dart | 4 +- .../layers/background.layer.dart} | 2 +- .../layers/draw.layer.dart} | 69 +- .../layers/draw/custom_hand_signature.dart | 77 ++ .../layers/emoji.layer.dart | 248 ++++++ .../layers/filter.layer.dart} | 8 +- .../layers/filters/datetime_filter.dart | 2 +- .../layers/filters/image_filter.dart | 2 +- .../layers/filters/location_filter.dart | 4 +- .../layers/text.layer.dart} | 4 +- .../layers_viewer.dart | 29 +- .../chat_list_components/group_list_item.dart | 2 +- .../message_context_menu.dart | 4 +- .../message_input.dart | 2 +- .../message_send_state_icon.dart | 2 +- lib/src/views/chats/media_viewer.view.dart | 2 +- .../reaction_buttons.component.dart | 4 +- lib/src/views/components/animate_icon.dart | 10 +- .../emoji_picker.bottom.dart} | 2 +- lib/src/views/home.view.dart | 2 +- .../memories/memories_photo_slider.view.dart | 2 +- lib/src/views/settings/help/credits.view.dart | 2 +- test/unit_test.dart | 8 +- 44 files changed, 465 insertions(+), 1040 deletions(-) rename lib/src/views/camera/{ => camera_preview_components}/painters/barcode_detector_painter.dart (100%) rename lib/src/views/camera/{ => camera_preview_components}/painters/coordinates_translator.dart (100%) rename lib/src/views/camera/{ => camera_preview_components}/painters/face_filters/beard_filter_painter.dart (95%) rename lib/src/views/camera/{ => camera_preview_components}/painters/face_filters/dog_filter_painter.dart (96%) rename lib/src/views/camera/{ => camera_preview_components}/painters/face_filters/face_filter_painter.dart (100%) rename lib/src/views/camera/{camera_send_to_view.dart => camera_send_to.view.dart} (100%) delete mode 100755 lib/src/views/camera/image_editor/data/data.dart delete mode 100755 lib/src/views/camera/image_editor/layers/emoji_layer.dart rename lib/src/views/camera/{share_image_view.dart => share_image_contact_selection.view.dart} (99%) rename lib/src/views/camera/{share_image_components => share_image_contact_selection}/best_friends_selector.dart (100%) rename lib/src/views/camera/{share_image_components => share_image_contact_selection}/select_show_time.dart (100%) rename lib/src/views/camera/{share_image_editor_view.dart => share_image_editor.view.dart} (97%) rename lib/src/views/camera/{image_editor => share_image_editor}/action_button.dart (100%) rename lib/src/views/camera/{image_editor => share_image_editor}/data/image_item.dart (100%) rename lib/src/views/camera/{image_editor => share_image_editor}/data/layer.dart (95%) rename lib/src/views/camera/{image_editor/layers/background_layer.dart => share_image_editor/layers/background.layer.dart} (93%) rename lib/src/views/camera/{image_editor/layers/draw_layer.dart => share_image_editor/layers/draw.layer.dart} (79%) create mode 100644 lib/src/views/camera/share_image_editor/layers/draw/custom_hand_signature.dart create mode 100755 lib/src/views/camera/share_image_editor/layers/emoji.layer.dart rename lib/src/views/camera/{image_editor/layers/filter_layer.dart => share_image_editor/layers/filter.layer.dart} (89%) rename lib/src/views/camera/{image_editor => share_image_editor}/layers/filters/datetime_filter.dart (89%) rename lib/src/views/camera/{image_editor => share_image_editor}/layers/filters/image_filter.dart (87%) rename lib/src/views/camera/{image_editor => share_image_editor}/layers/filters/location_filter.dart (96%) rename lib/src/views/camera/{image_editor/layers/text_layer.dart => share_image_editor/layers/text.layer.dart} (98%) rename lib/src/views/camera/{image_editor => share_image_editor}/layers_viewer.dart (62%) rename lib/src/views/{camera/image_editor/modules/all_emojis.dart => components/emoji_picker.bottom.dart} (97%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 503743c..aa14e5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Adds option to manual focus in the camera - Adds support to switch between front and back camera during video recording - Adds basic face filters +- Improves image editor like emojies or text under a drawing can be moved +- Fixes issue with emojis disappearing in the image editor ## 0.0.86 diff --git a/lib/src/database/daos/reactions.dao.dart b/lib/src/database/daos/reactions.dao.dart index 59ef34a..286fc68 100644 --- a/lib/src/database/daos/reactions.dao.dart +++ b/lib/src/database/daos/reactions.dao.dart @@ -22,7 +22,7 @@ class ReactionsDao extends DatabaseAccessor with _$ReactionsDaoMixin { String emoji, bool remove, ) async { - if (!isEmoji(emoji)) { + if (!isOneEmoji(emoji)) { Log.error('Did not update reaction as it is not an emoji!'); return; } @@ -59,7 +59,7 @@ class ReactionsDao extends DatabaseAccessor with _$ReactionsDaoMixin { String emoji, bool remove, ) async { - if (!isEmoji(emoji)) { + if (!isOneEmoji(emoji)) { Log.error('Did not update reaction as it is not an emoji!'); return; } diff --git a/lib/src/services/intent/links.intent.dart b/lib/src/services/intent/links.intent.dart index c4a1066..055eae8 100644 --- a/lib/src/services/intent/links.intent.dart +++ b/lib/src/services/intent/links.intent.dart @@ -13,7 +13,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/share_image_editor_view.dart'; +import 'package:twonly/src/views/camera/share_image_editor.view.dart'; import 'package:twonly/src/views/chats/add_new_user.view.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/contact/contact.view.dart'; diff --git a/lib/src/views/camera/camera_preview_components/camera_preview.dart b/lib/src/views/camera/camera_preview_components/camera_preview.dart index cde7474..991748d 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview.dart @@ -23,32 +23,54 @@ class MainCameraPreview extends StatelessWidget { requiredHeight: 0, additionalPadding: 59, bottomNavigation: Container(), - child: Screenshot( - controller: mainCameraController.screenshotController, - child: AspectRatio( - aspectRatio: 9 / 16, - child: ClipRect( - child: FittedBox( - fit: BoxFit.cover, - child: SizedBox( - width: mainCameraController - .cameraController!.value.previewSize!.height, - height: mainCameraController - .cameraController!.value.previewSize!.width, - child: CameraPreview( - key: mainCameraController.cameraPreviewKey, - mainCameraController.cameraController!, - child: Stack( - children: [ - if (mainCameraController.customPaint != null) - Positioned.fill( - child: mainCameraController.customPaint!, - ), - if (mainCameraController.facePaint != null) - Positioned.fill( - child: mainCameraController.facePaint!, - ), - if (mainCameraController.focusPointOffset != null) + child: Stack( + children: [ + Screenshot( + controller: mainCameraController.screenshotController, + child: AspectRatio( + aspectRatio: 9 / 16, + child: ClipRect( + child: FittedBox( + fit: BoxFit.cover, + child: SizedBox( + width: mainCameraController + .cameraController!.value.previewSize!.height, + height: mainCameraController + .cameraController!.value.previewSize!.width, + child: CameraPreview( + key: mainCameraController.cameraPreviewKey, + mainCameraController.cameraController!, + child: Stack( + children: [ + if (mainCameraController.customPaint != null) + Positioned.fill( + child: mainCameraController.customPaint!, + ), + if (mainCameraController.facePaint != null) + Positioned.fill( + child: mainCameraController.facePaint!, + ), + ], + ), + ), + ), + ), + ), + ), + ), + if (mainCameraController.focusPointOffset != null) + AspectRatio( + aspectRatio: 9 / 16, + child: ClipRect( + child: FittedBox( + fit: BoxFit.cover, + child: SizedBox( + width: mainCameraController + .cameraController!.value.previewSize!.height, + height: mainCameraController + .cameraController!.value.previewSize!.width, + child: Stack( + children: [ Positioned( top: mainCameraController.focusPointOffset!.dy - 40, left: @@ -65,14 +87,14 @@ class MainCameraPreview extends StatelessWidget { ), ), ), - ) - ], + ), + ], + ), ), ), ), ), - ), - ), + ], ), ), ); diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index fe387ee..b5ae1b7 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -27,8 +27,8 @@ import 'package:twonly/src/views/camera/camera_preview_components/permissions_vi import 'package:twonly/src/views/camera/camera_preview_components/send_to.dart'; import 'package:twonly/src/views/camera/camera_preview_components/video_recording_time.dart'; import 'package:twonly/src/views/camera/camera_preview_components/zoom_selector.dart'; -import 'package:twonly/src/views/camera/image_editor/action_button.dart'; -import 'package:twonly/src/views/camera/share_image_editor_view.dart'; +import 'package:twonly/src/views/camera/share_image_editor/action_button.dart'; +import 'package:twonly/src/views/camera/share_image_editor.view.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/loader.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; diff --git a/lib/src/views/camera/camera_preview_components/face_filters.dart b/lib/src/views/camera/camera_preview_components/face_filters.dart index f4826a0..8c880a7 100644 --- a/lib/src/views/camera/camera_preview_components/face_filters.dart +++ b/lib/src/views/camera/camera_preview_components/face_filters.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:twonly/src/views/camera/painters/face_filters/beard_filter_painter.dart'; -import 'package:twonly/src/views/camera/painters/face_filters/dog_filter_painter.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/beard_filter_painter.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/dog_filter_painter.dart'; enum FaceFilterType { none, diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart index f710797..fb433ce 100644 --- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart @@ -19,10 +19,10 @@ import 'package:twonly/src/utils/qr.dart'; import 'package:twonly/src/utils/screenshot.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart'; import 'package:twonly/src/views/camera/camera_preview_components/face_filters.dart'; -import 'package:twonly/src/views/camera/painters/barcode_detector_painter.dart'; -import 'package:twonly/src/views/camera/painters/face_filters/beard_filter_painter.dart'; -import 'package:twonly/src/views/camera/painters/face_filters/dog_filter_painter.dart'; -import 'package:twonly/src/views/camera/painters/face_filters/face_filter_painter.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/barcode_detector_painter.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/beard_filter_painter.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/dog_filter_painter.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart'; class ScannedVerifiedContact { ScannedVerifiedContact({ diff --git a/lib/src/views/camera/painters/barcode_detector_painter.dart b/lib/src/views/camera/camera_preview_components/painters/barcode_detector_painter.dart similarity index 100% rename from lib/src/views/camera/painters/barcode_detector_painter.dart rename to lib/src/views/camera/camera_preview_components/painters/barcode_detector_painter.dart diff --git a/lib/src/views/camera/painters/coordinates_translator.dart b/lib/src/views/camera/camera_preview_components/painters/coordinates_translator.dart similarity index 100% rename from lib/src/views/camera/painters/coordinates_translator.dart rename to lib/src/views/camera/camera_preview_components/painters/coordinates_translator.dart diff --git a/lib/src/views/camera/painters/face_filters/beard_filter_painter.dart b/lib/src/views/camera/camera_preview_components/painters/face_filters/beard_filter_painter.dart similarity index 95% rename from lib/src/views/camera/painters/face_filters/beard_filter_painter.dart rename to lib/src/views/camera/camera_preview_components/painters/face_filters/beard_filter_painter.dart index 95f2b12..3477bb6 100644 --- a/lib/src/views/camera/painters/face_filters/beard_filter_painter.dart +++ b/lib/src/views/camera/camera_preview_components/painters/face_filters/beard_filter_painter.dart @@ -6,8 +6,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/views/camera/painters/coordinates_translator.dart'; -import 'package:twonly/src/views/camera/painters/face_filters/face_filter_painter.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/coordinates_translator.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart'; class BeardFilterPainter extends FaceFilterPainter { BeardFilterPainter( diff --git a/lib/src/views/camera/painters/face_filters/dog_filter_painter.dart b/lib/src/views/camera/camera_preview_components/painters/face_filters/dog_filter_painter.dart similarity index 96% rename from lib/src/views/camera/painters/face_filters/dog_filter_painter.dart rename to lib/src/views/camera/camera_preview_components/painters/face_filters/dog_filter_painter.dart index 5ca873c..3643c33 100644 --- a/lib/src/views/camera/painters/face_filters/dog_filter_painter.dart +++ b/lib/src/views/camera/camera_preview_components/painters/face_filters/dog_filter_painter.dart @@ -6,8 +6,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/views/camera/painters/coordinates_translator.dart'; -import 'package:twonly/src/views/camera/painters/face_filters/face_filter_painter.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/coordinates_translator.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart'; class DogFilterPainter extends FaceFilterPainter { DogFilterPainter( diff --git a/lib/src/views/camera/painters/face_filters/face_filter_painter.dart b/lib/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart similarity index 100% rename from lib/src/views/camera/painters/face_filters/face_filter_painter.dart rename to lib/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart diff --git a/lib/src/views/camera/camera_send_to_view.dart b/lib/src/views/camera/camera_send_to.view.dart similarity index 100% rename from lib/src/views/camera/camera_send_to_view.dart rename to lib/src/views/camera/camera_send_to.view.dart diff --git a/lib/src/views/camera/image_editor/data/data.dart b/lib/src/views/camera/image_editor/data/data.dart deleted file mode 100755 index a686020..0000000 --- a/lib/src/views/camera/image_editor/data/data.dart +++ /dev/null @@ -1,710 +0,0 @@ -Map emojiWeights = {}; - -List emojis = [ - '😀', - '😁', - '😂', - '🤣', - '😃', - '😄', - '😅', - '😆', - '😉', - '😊', - '😋', - '😎', - '😍', - '😘', - '🥰', - '😗', - '😙', - '😚', - '🙂️', - '🤗', - '🤩', - '🤔', - '🤔', - '🤨', - '😐', - '😑', - '😶', - '🙄', - '😏', - '😣', - '😥', - '😮', - '🤐', - '😯', - '😪', - '😫', - '😴', - '😌', - '😛', - '😜', - '😝', - '🤤', - '😒', - '😓', - '😔', - '😕', - '🙃', - '🤑', - '😲', - '🙁', - '😖', - '😞', - '😟', - '😤', - '😢', - '😭', - '😦', - '😧', - '😨', - '😩', - '🤯', - '😬', - '😰', - '😱', - '🥵', - '🥶', - '😳', - '🤪', - '😵', - '😡', - '😠', - '🤬', - '😷', - '🤒', - '🤕', - '🤢', - '🤮', - '🤧', - '😇', - '🤠', - '🤡', - '🥳', - '🥴', - '🤥', - '🤫', - '🤭', - '🤭', - '🧐', - '🤓', - '😈', - '👿', - '👹', - '👺', - '💀', - '👻', - '👽', - '🤖', - '💩', - '😺', - '😸', - '😹', - '😻', - '😼', - '😽', - '🙀', - '😿', - '😾', - '😾', - - /// People and Fantasy - '👶', - '👧', - '🧒', - '👩', - '🧑', - '👨', - '👵', - '👴', - '👲', - '👳‍♀️‍️', - '👳‍♂️️‍️', - '🧕️️‍️', - '🧔‍', - '👱‍♂️️‍', - '👱‍♀️️️‍', - '👨‍🦰️️️‍', - '👩‍🦰‍', - '👨‍🦱‍‍', - '👨‍🦲‍‍', - '👩‍🦲‍‍', - '👨‍🦳‍‍', - '👩‍🦳‍‍', - '🦸‍♀️‍‍', - '🦸‍♂️️‍‍', - '🦹‍♀️️️‍‍', - '🦹‍♂️️️️‍‍', - '👮‍♀️‍‍', - '👮‍♂️️‍‍', - '👷‍♀️️️‍‍', - '👷‍♂️️️️‍‍', - '💂‍♀️️️️️‍‍', - '💂‍♂️️️️️️‍‍', - '🕵️‍♀️️️️️️️‍‍', - '🕵️‍♂️️️️️️️️‍‍', - '👩‍⚕️️️️️️️️️‍‍', - '👨‍⚕️️️️️️️️️️‍‍', - '👩‍🌾️️️️️️️️️️‍‍', - '👨‍🌾‍‍', - '👩‍🍳‍‍', - '👨‍🍳‍‍', - '👩‍🎓‍‍', - '👨‍🎓‍‍', - '👩‍🎤‍‍', - '👨‍🎤‍‍', - '👩‍🏫‍‍', - '👨‍🏫‍‍', - '👩‍🏭‍‍', - '👨‍🏭‍‍', - '👩‍💻‍‍', - '👨‍💻‍‍', - '👩‍💼‍‍', - '👨‍💼‍‍', - '👩‍🔧‍‍', - '👨‍🔧‍‍', - '👩‍🔬‍‍', - '👨‍🔬‍‍', - '👩‍🎨‍‍', - '👨‍🎨‍‍', - '👩‍🚒‍‍', - '👨‍🚒‍‍', - '👩‍✈️‍‍', - '👨‍✈️️‍‍', - '👩‍🚀‍‍', - '👨‍🚀‍‍', - '👩‍⚖️‍‍', - '👨‍⚖️️‍‍', - '👰‍‍', - '🤵‍‍', - '👸‍‍', - '🤴‍‍', - '🤶‍‍', - '🎅‍‍', - '🧙‍♀️‍‍', - '🧙‍♂️️‍‍', - '🧝‍♀️️️‍‍', - '🧝‍♂️‍‍', - '🧛‍♀️️‍‍', - '🧛‍♂️️️‍‍', - '🧟‍♀️️️️‍‍', - '🧟‍♂️️️️️‍‍', - '🧞‍♀️️️️️️‍‍', - '🧞‍♂️️️️️️️‍‍', - '🧜‍♀️️️️️️️️‍‍', - '🧜‍♂️️️️️️️️️‍‍', - '🧚‍♀️️️️️️️️️️‍‍', - '🧚‍♂️️️️️️️️️️️‍‍', - '👼️️️️️️️️️️️‍‍', - '🤰‍‍', - '🤱‍‍', - '🙇‍♀️‍‍', - '🙇‍♂️‍‍', - '💁‍♀️️‍‍', - '💁‍♂️️️‍‍', - '🙅‍♀️️️️‍‍', - '🙅‍♂️‍‍', - '🙆‍♀️️‍‍', - '🙆‍♂️️️‍‍', - '🙋‍♀️️️️‍‍', - '🙋‍♂️‍‍', - '🤦‍♀️️‍‍', - '🤦‍♂️️️‍‍', - '🤷‍♀️️️️‍‍', - '🤷‍♂️️️️️‍‍', - '🙎‍♀️️️️️️‍‍', - '🙎‍♂️️️️️️️‍‍', - '🙍‍♀️️️️️️️️‍‍', - '🙍‍♂️️️️️️️️️‍‍', - '💇‍♀️️️️️️️️️️‍‍', - '💇‍♂️️️️️️️️️️️‍‍', - '💆‍♀️️️️️️️️️️️️‍‍', - '💆‍♂️️️️️️️️️️️️️‍‍', - '🧖‍♀️️️️️️️️️️️️️️‍‍', - '🧖‍♂️️️️️️️️️️️️️️️‍‍', - '💅️️️️️️️️️️️️️️️‍‍', - '🤳️️️️️️️️️️️️️️‍‍', - '💃️️️️️️️️️️️️️‍‍', - '🕺️️️️️️️️️️️️‍‍', - '👯‍♀️‍‍', - '👯‍♂️️‍‍', - '🕴️️‍‍', - '🚶‍♀️️‍‍', - '🚶‍♂️️️‍‍', - '🏃‍♀️️️️‍‍', - '🏃‍♂️‍‍', - '👫️‍‍', - '👭‍‍', - '👬‍‍', - '💑‍‍', - '👩‍❤️‍👩‍‍', - '👨‍❤️‍👨‍‍', - '💏‍‍', - '👩‍❤️‍💋‍👩‍‍', - '👨‍❤️‍💋‍👨‍‍', - '👪‍‍', - '👨‍👩‍👧‍‍', - '👨‍👩‍👧‍👦‍‍', - '👨‍👩‍👦‍👦‍‍', - '👨‍👩‍👧‍👧‍‍', - '👩‍👩‍👦‍‍', - '👩‍👩‍👧‍‍', - '👩‍👩‍👧‍👦‍‍', - '👩‍👩‍👦‍👦‍‍', - '👩‍👩‍👧‍👧‍‍', - '👨‍👨‍👦‍‍', - '👨‍👨‍👧‍‍', - '👨‍👨‍👧‍👦‍‍', - '👨‍👨‍👦‍👦‍‍', - '👨‍👨‍👧‍👧‍‍', - '👩‍👦‍‍', - '👩‍👧‍‍', - '👩‍👧‍👦‍‍', - '👩‍👦‍👦‍‍', - '👩‍👧‍👧‍‍', - '👨‍👦‍‍', - '👨‍👧‍‍', - '👨‍👧‍👦‍‍', - '👨‍👦‍👦‍‍', - '👨‍👧‍👧‍‍', - '🤲‍‍', - '👐‍‍', - '🙌‍‍', - '👏‍‍', - '🤝‍‍', - '👍‍‍', - '👎‍‍', - '👊‍‍', - '✊‍‍', - '🤛‍‍', - '🤜‍‍', - '🤞‍‍', - '✌️‍‍', - '🤟️‍‍', - '🤘‍‍', - '👌‍‍', - '👈‍‍', - '👉‍‍', - '👆‍‍', - '👇‍‍', - '☝️‍‍', - '✋️‍‍', - '🤚️‍‍', - '🤚️‍‍', - '🖐‍‍', - '🖖‍‍', - '👋‍‍', - '🤙‍‍', - '💪‍‍', - '🦵‍‍', - '🦶‍‍', - '🖕‍‍', - '✍️‍‍', - '🙏️‍‍', - '💍‍‍', - '💄‍‍', - '💋‍‍', - '👄‍‍', - '👅‍‍', - '👂‍‍', - '👃‍‍', - '👣‍‍', - '👁‍‍', - '👀‍‍', - '🧠‍‍', - '🦴‍‍', - '🦷‍‍', - '🗣‍‍', - '👤‍‍', - '👥‍‍', - '🧥‍‍', - '👚‍‍', - '👕‍‍', - '👖‍‍', - '👔‍‍', - '👗‍‍', - '👙‍‍', - '👘‍‍', - '👠‍‍', - '👡‍‍', - '👢‍‍', - '👞‍‍', - '👟‍‍', - '🥾‍‍', - '🥿‍‍', - '🧦‍‍', - '🧤‍‍', - '🧣‍‍', - '🎩‍‍', - '🧢‍‍', - '👒‍‍', - '🎓‍‍', - '⛑‍‍', - '👑‍‍', - '👝‍‍', - '👛‍‍', - '👜‍‍', - '💼‍‍', - '🎒‍‍', - '👓‍‍', - '🕶‍‍', - '🥽‍‍', - '🥼‍‍', - '🌂‍‍', - '🧵‍‍', - '🧶‍‍', - - /// Animals - '🐶‍‍', - '🐱‍‍', - '🐭‍‍', - '🐰‍‍', - '🦊‍‍', - '🦝‍‍', - '🐻‍‍', - '🦘‍‍', - '🦡‍‍', - '🐨‍‍', - '🐯‍‍', - '🦁‍‍', - '🐼‍‍', - '🐼‍‍', - '🐮‍‍', - '🐷‍‍', - '🐽‍‍', - '🐸‍‍', - '🐵‍‍', - '🙈‍‍', - '🙉‍‍', - '🙊‍‍', - '🐒‍‍', - '🐔‍‍', - '🐧‍‍', - '🐦‍‍', - '🐤‍‍', - '🐣‍‍', - '🐥‍‍', - '🦆‍‍', - '🦢‍‍', - '🦅‍‍', - '🦉‍‍', - '🦚‍‍', - '🦜‍‍', - '🦇‍‍', - '🐺‍‍', - '🐗‍‍', - '🐴‍‍', - '🦄‍‍', - '🐝‍‍', - '🐛‍‍', - '🦋‍‍', - '🐌‍‍', - '🐚‍‍', - '🐞‍‍', - '🐜‍‍', - '🦗‍‍', - '🕷‍‍', - '🕸‍‍', - '🦂‍‍', - '🦟‍‍', - '🦠‍‍', - '🐢‍‍', - '🐍‍‍', - '🦎‍‍', - '🦖‍‍', - '🦕‍‍', - '🐙‍‍', - '🦑‍‍', - '🦐‍‍', - '🦀‍‍', - '🐡‍‍', - '🐠‍‍', - '🐟‍‍', - '🐬‍‍', - '🐳‍‍', - '🐋‍‍', - '🦈‍‍', - '🐊‍‍', - '🐅‍‍', - '🐆‍‍', - '🦓‍‍', - '🦍‍‍', - '🐘‍‍', - '🦏‍‍', - '🦛‍‍', - '🐪‍‍', - '🐫‍‍', - '🦙‍‍', - '🦒‍‍', - '🐃‍‍', - '🐂‍‍', - '🐄‍‍', - '🐎‍‍', - '🐖‍‍', - '🐏‍‍', - '🐐‍‍', - '🦌‍‍', - '🐕‍‍', - '🐩‍‍', - '🐈‍‍', - '🐓‍‍', - '🦃‍‍', - '🕊‍‍', - '🐇‍‍', - '🐁‍‍', - '🐀‍‍', - '🐿‍‍', - '🦔‍‍', - '🐾‍', - '🐉‍', - '🐲‍', - '🌵‍', - '🎄‍', - '🌲‍', - '🌳‍', - '🌴‍', - '🌱‍', - '🌿‍', - '☘️‍', - '🎍️‍', - '🎋️‍', - '🍃‍', - '🍂‍', - '🍁‍', - '🍄‍', - '🌾️‍', - '💐️‍', - '🌷️‍', - '🌹‍', - '🥀‍', - '🌺‍', - '🌸‍', - '🌼‍', - '🌻️‍', - '🌞‍', - '🌝‍', - '🌛‍', - '🌜‍', - '🌚‍', - '🌕‍', - '🌖‍', - '🌗‍', - '🌘‍', - '🌑‍', - '🌒‍', - '🌔‍', - '🌙‍', - '🌎‍', - '🌍‍', - '🌏‍', - '💫‍', - '⭐️‍', - '🌟️‍', - '✨️‍', - '⚡️️‍', - '☄️️️‍', - '💥️️️‍', - '🔥‍', - '🌪‍', - '🌈‍', - '☀️‍', - '🌤️‍', - '⛅️️‍', - '🌥️️‍', - '☁️️‍', - '🌦️️‍', - '🌧️‍', - '⛈‍', - '🌩‍', - '🌨‍', - '❄️‍', - '☃️️‍', - '⛄️️️‍', - '🌬️️️‍', - '💨️️️‍', - '💧️️️‍', - '💦️️️‍', - '☔️️️️‍', - '☂️️️️️‍', - '🌊️️️️️‍', - '🌫️️️️‍', - - /// Foods - '🍏‍', - '🍎‍', - '🍐‍', - '🍊‍', - '🍋‍', - '🍌‍', - '🍉‍', - '🍇‍', - '🍓‍', - '🍈‍', - '🍒‍', - '🍑‍', - '🍍‍', - '🥭‍', - '🥥‍', - '🥝‍', - '🍅‍', - '🍆‍', - '🥑‍', - '🥦‍', - '🥒‍', - '🥬‍', - '🌶‍', - '🌽‍', - '🥕‍', - '🥔‍', - '🍠‍', - '🥐‍', - '🍞‍', - '🥖‍', - '🥨‍', - '🥯‍', - '🧀‍', - '🥚‍', - '🍳‍', - '🥞‍', - '🥓‍', - '🥩‍', - '🍗‍', - '🍖‍', - '🌭‍', - '🍔‍', - '🍟‍', - '🍕‍', - '🥪‍', - '🥙‍', - '🌮‍', - '🌯‍', - '🥗‍', - '🥘‍', - '🥫‍', - '🍝‍', - '🍜‍', - '🍲‍', - '🍛‍', - '🍣‍', - '🍱‍', - '🥟‍', - '🍤‍', - '🍙‍', - '🍚‍', - '🍘‍', - '🍥‍', - '🥮‍', - '🥠‍', - '🍢‍', - '🍧‍', - '🍨‍', - '🍦‍', - '🥧‍', - '🍰‍', - '🎂‍', - '🍮‍', - '🍭‍', - '🍬‍', - '🍫‍', - '🍿‍', - '🧂‍', - '🍩‍', - '🍪‍', - '🌰‍', - '🥜‍', - '🍯‍', - '🥛‍', - '🍼‍', - '☕️‍', - '🍵️‍', - '🥤️‍', - '🍶‍', - '🍺‍', - '🍻‍', - '🥂‍', - '🍷‍', - '🍸‍', - '🍹‍', - '🍾‍', - '🥄‍', - '🍴‍', - '🍽‍', - '🥣‍', - '🥡‍', - '🥢‍', - - /// Activity and Sports - '⚽️‍', - '🏀️‍', - '🏈‍', - '⚾️‍', - '🥎️‍', - '🏐️‍', - '🏉‍', - '🎾‍', - '🥏‍', - '🎱‍', - '🏓‍', - '🏸‍', - '🥅‍', - '🏒‍', - '🏑‍', - '🥍‍', - '🏏‍', - '⛳️‍', - '🏹️‍', - '🎣️‍', - '🥊‍', - '🥋‍', - '🎽‍', - '⛸‍', - '🥌‍', - '🛷‍', - '🛹‍', - '🎿‍', - '⛷‍', - '🏂‍', - '🏋️‍♀️‍', - '🏋🏼‍♀️‍', - '🏋🏽‍♀️️‍', - '🏋🏾‍♀️️️‍', - '🏋🏿‍♀️️️️‍', - '🏋️‍♂️️️️‍', - '🏋🏻‍♂️️️️‍', - '🏋🏼‍♂️️️️‍', - '🏋🏽‍♂️️️️‍', - '🏋🏾‍♂️️️️‍', - '🏋🏿‍♂️️️️‍', - '🤼‍♀️️️️‍', - '🤼‍♂️️️️‍', - '🤸‍♀️️️️‍', - '🤸🏻‍♀️️️️‍', - '🤸🏼‍♀️️️️‍', - '🤸🏽‍♀️️️️‍', - '🤸🏿‍♀️️️️️‍', - '🤸‍♂️️️️‍', - '🤸🏻‍♂️️️️‍', - '🤸🏼‍♂️️️️️‍', - '🤸🏽‍♂️️️️️️‍', - '🤸🏾‍♂️️️️️️‍', - '🤸🏿‍♂️️️️️️‍', - '⛹️‍♀️️️️️️‍', - '⛹🏻‍♀️️️️️️️‍', - '⛹🏼‍♀️️️️️️️️‍', - '⛹🏽‍♀️️️️️️️️️‍', - '⛹🏾‍♀️️️️️️️️️️‍', - '⛹🏿‍♀️️️️️️️️️️️‍', - '⛹️‍♂️️️️️️️️️️️️‍', - '⛹🏻‍♂️️️️️️️️️️️️️‍', - '⛹🏼‍♂️️️️️️️️️️️️️️‍', - '⛹🏽‍♂️️️️️️️️️️️️️️️‍', - '⛹🏾‍♂️️️️️️️️️️️️️️️️‍', - '⛹🏿‍♂️‍', - '🤺️‍', - '🤾‍♀️‍', - '🤾🏻‍♀️️‍', - '🤾🏼‍♀️️️‍', - '🤾🏾‍♀️️️️‍', -]; diff --git a/lib/src/views/camera/image_editor/layers/emoji_layer.dart b/lib/src/views/camera/image_editor/layers/emoji_layer.dart deleted file mode 100755 index 32ddcc3..0000000 --- a/lib/src/views/camera/image_editor/layers/emoji_layer.dart +++ /dev/null @@ -1,173 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:twonly/src/views/camera/image_editor/action_button.dart'; -import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; - -/// Emoji layer -class EmojiLayer extends StatefulWidget { - const EmojiLayer({ - required this.layerData, - super.key, - this.onUpdate, - }); - final EmojiLayerData layerData; - final VoidCallback? onUpdate; - - @override - State createState() => _EmojiLayerState(); -} - -class _EmojiLayerState extends State { - double initialRotation = 0; - Offset initialOffset = Offset.zero; - Offset initialFocalPoint = Offset.zero; - double initialScale = 1; - bool deleteLayer = false; - bool twoPointerWhereDown = false; - final GlobalKey outlineKey = GlobalKey(); - final GlobalKey emojiKey = GlobalKey(); - int pointers = 0; - bool display = false; - - @override - void initState() { - super.initState(); - - if (widget.layerData.offset.dy == 0) { - // Set the initial offset to the center of the screen - WidgetsBinding.instance.addPostFrameCallback((_) { - setState(() { - widget.layerData.offset = Offset( - MediaQuery.of(context).size.width / 2 - (153 / 2), - MediaQuery.of(context).size.height / 2 - (153 / 2) - 100, - ); - }); - display = true; - }); - } else { - display = true; - } - } - - @override - Widget build(BuildContext context) { - if (!display) return Container(); - if (widget.layerData.isDeleted) return Container(); - return Stack( - key: outlineKey, - children: [ - Positioned( - left: widget.layerData.offset.dx, - top: widget.layerData.offset.dy, - child: Listener( - onPointerUp: (details) { - setState(() { - pointers--; - if (pointers == 0) { - twoPointerWhereDown = false; - } - if (deleteLayer) { - widget.layerData.isDeleted = true; - widget.onUpdate!(); - } - }); - }, - onPointerDown: (details) { - setState(() { - pointers++; - }); - }, - child: GestureDetector( - onScaleStart: (details) { - initialScale = widget.layerData.size; - initialRotation = widget.layerData.rotation; - initialOffset = widget.layerData.offset; - initialFocalPoint = - Offset(details.focalPoint.dx, details.focalPoint.dy); - - setState(() {}); - }, - onScaleUpdate: (details) async { - if (twoPointerWhereDown && details.pointerCount != 2) { - return; - } - final outlineBox = - outlineKey.currentContext!.findRenderObject()! as RenderBox; - - final emojiBox = - emojiKey.currentContext!.findRenderObject()! as RenderBox; - - final isAtTheBottom = - (widget.layerData.offset.dy + emojiBox.size.height / 2) > - outlineBox.size.height - 80; - final isInTheCenter = MediaQuery.of(context).size.width / 2 - - 30 < - (widget.layerData.offset.dx + - emojiBox.size.width / 2) && - MediaQuery.of(context).size.width / 2 + 20 > - (widget.layerData.offset.dx + emojiBox.size.width / 2); - - if (isAtTheBottom && isInTheCenter) { - if (!deleteLayer) { - await HapticFeedback.heavyImpact(); - } - deleteLayer = true; - } else { - deleteLayer = false; - } - setState(() { - twoPointerWhereDown = details.pointerCount >= 2; - widget.layerData.size = initialScale * details.scale; - if (widget.layerData.size > 96) { - // https://github.com/twonlyapp/twonly-app/issues/349 - widget.layerData.size = 96; - } - // print(widget.layerData.size); - widget.layerData.rotation = - initialRotation + details.rotation; - - // Update the position based on the translation - final dx = (initialOffset.dx) + - (details.focalPoint.dx - initialFocalPoint.dx); - final dy = (initialOffset.dy) + - (details.focalPoint.dy - initialFocalPoint.dy); - widget.layerData.offset = Offset(dx, dy); - }); - }, - child: Transform.rotate( - angle: widget.layerData.rotation, - key: emojiKey, - child: Container( - padding: const EdgeInsets.all(44), - color: Colors.transparent, - child: Text( - widget.layerData.text, - style: TextStyle( - fontSize: widget.layerData.size, - ), - ), - ), - ), - ), - ), - ), - if (pointers > 0) - Positioned( - left: 0, - right: 0, - bottom: 20, - child: Center( - child: GestureDetector( - child: ActionButton( - FontAwesomeIcons.trashCan, - tooltipText: '', - color: deleteLayer ? Colors.red : Colors.white, - ), - ), - ), - ), - ], - ); - } -} diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_contact_selection.view.dart similarity index 99% rename from lib/src/views/camera/share_image_view.dart rename to lib/src/views/camera/share_image_contact_selection.view.dart index 0c22e03..0ecbbab 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_contact_selection.view.dart @@ -13,7 +13,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart'; +import 'package:twonly/src/views/camera/share_image_contact_selection/best_friends_selector.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/headline.dart'; diff --git a/lib/src/views/camera/share_image_components/best_friends_selector.dart b/lib/src/views/camera/share_image_contact_selection/best_friends_selector.dart similarity index 100% rename from lib/src/views/camera/share_image_components/best_friends_selector.dart rename to lib/src/views/camera/share_image_contact_selection/best_friends_selector.dart diff --git a/lib/src/views/camera/share_image_components/select_show_time.dart b/lib/src/views/camera/share_image_contact_selection/select_show_time.dart similarity index 100% rename from lib/src/views/camera/share_image_components/select_show_time.dart rename to lib/src/views/camera/share_image_contact_selection/select_show_time.dart diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor.view.dart similarity index 97% rename from lib/src/views/camera/share_image_editor_view.dart rename to lib/src/views/camera/share_image_editor.view.dart index 28e3cb8..cd4ecbf 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor.view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:collection'; + import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -18,13 +19,13 @@ import 'package:twonly/src/utils/screenshot.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart'; import 'package:twonly/src/views/camera/camera_preview_components/save_to_gallery.dart'; -import 'package:twonly/src/views/camera/image_editor/action_button.dart'; -import 'package:twonly/src/views/camera/image_editor/data/image_item.dart'; -import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; -import 'package:twonly/src/views/camera/image_editor/layers_viewer.dart'; -import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart'; -import 'package:twonly/src/views/camera/share_image_components/select_show_time.dart'; -import 'package:twonly/src/views/camera/share_image_view.dart'; +import 'package:twonly/src/views/camera/share_image_contact_selection.view.dart'; +import 'package:twonly/src/views/camera/share_image_contact_selection/select_show_time.dart'; +import 'package:twonly/src/views/camera/share_image_editor/action_button.dart'; +import 'package:twonly/src/views/camera/share_image_editor/data/image_item.dart'; +import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers_viewer.dart'; +import 'package:twonly/src/views/components/emoji_picker.bottom.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/components/notification_badge.dart'; import 'package:video_player/video_player.dart'; diff --git a/lib/src/views/camera/image_editor/action_button.dart b/lib/src/views/camera/share_image_editor/action_button.dart similarity index 100% rename from lib/src/views/camera/image_editor/action_button.dart rename to lib/src/views/camera/share_image_editor/action_button.dart diff --git a/lib/src/views/camera/image_editor/data/image_item.dart b/lib/src/views/camera/share_image_editor/data/image_item.dart similarity index 100% rename from lib/src/views/camera/image_editor/data/image_item.dart rename to lib/src/views/camera/share_image_editor/data/image_item.dart diff --git a/lib/src/views/camera/image_editor/data/layer.dart b/lib/src/views/camera/share_image_editor/data/layer.dart similarity index 95% rename from lib/src/views/camera/image_editor/data/layer.dart rename to lib/src/views/camera/share_image_editor/data/layer.dart index b8e8622..04b5e34 100755 --- a/lib/src/views/camera/image_editor/data/layer.dart +++ b/lib/src/views/camera/share_image_editor/data/layer.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hand_signature/signature.dart'; -import 'package:twonly/src/views/camera/image_editor/data/image_item.dart'; +import 'package:twonly/src/views/camera/share_image_editor/data/image_item.dart'; /// Layer class with some common properties class Layer { @@ -51,7 +51,7 @@ class EmojiLayerData extends Layer { EmojiLayerData({ required super.key, this.text = '', - this.size = 64, + this.size = 94, super.offset, super.opacity, super.rotation, diff --git a/lib/src/views/camera/image_editor/layers/background_layer.dart b/lib/src/views/camera/share_image_editor/layers/background.layer.dart similarity index 93% rename from lib/src/views/camera/image_editor/layers/background_layer.dart rename to lib/src/views/camera/share_image_editor/layers/background.layer.dart index 300ae17..e4158af 100755 --- a/lib/src/views/camera/image_editor/layers/background_layer.dart +++ b/lib/src/views/camera/share_image_editor/layers/background.layer.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; class BackgroundLayer extends StatefulWidget { const BackgroundLayer({ diff --git a/lib/src/views/camera/image_editor/layers/draw_layer.dart b/lib/src/views/camera/share_image_editor/layers/draw.layer.dart similarity index 79% rename from lib/src/views/camera/image_editor/layers/draw_layer.dart rename to lib/src/views/camera/share_image_editor/layers/draw.layer.dart index 4207c39..64d528e 100644 --- a/lib/src/views/camera/image_editor/layers/draw_layer.dart +++ b/lib/src/views/camera/share_image_editor/layers/draw.layer.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:hand_signature/signature.dart'; -// ignore: implementation_imports -import 'package:hand_signature/src/utils.dart'; + import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/screenshot.dart'; -import 'package:twonly/src/views/camera/image_editor/action_button.dart'; -import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/action_button.dart'; +import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/draw/custom_hand_signature.dart'; class DrawLayer extends StatefulWidget { const DrawLayer({ @@ -23,8 +22,6 @@ class DrawLayer extends StatefulWidget { class _DrawLayerState extends State { Color currentColor = Colors.red; - ScreenshotController screenshotController = ScreenshotController(); - List undoList = []; bool skipNextEvent = false; bool showMagnifyingGlass = false; @@ -85,17 +82,11 @@ class _DrawLayerState extends State { fit: StackFit.expand, children: [ Positioned.fill( - child: Container( - decoration: const BoxDecoration( - color: Colors.transparent, - ), - child: Screenshot( - controller: screenshotController, - child: HandSignature( - control: widget.layerData.control, - drawer: CustomSignatureDrawer(color: currentColor, width: 7), - ), - ), + child: CustomHandSignature( + control: widget.layerData.control, + isModificationEnabled: widget.layerData.isEditing, + currentColor: currentColor, + width: 7, ), ), if (widget.layerData.isEditing && widget.layerData.showCustomButtons) @@ -211,12 +202,12 @@ class _DrawLayerState extends State { top: 50 + (185 * _sliderValue), child: MagnifyingGlass(color: currentColor), ), - if (!widget.layerData.isEditing) - Positioned.fill( - child: Container( - color: Colors.transparent, - ), - ), + // if (!widget.layerData.isEditing) + // Positioned.fill( + // child: Container( + // color: Colors.transparent, + // ), + // ), ], ); } @@ -244,33 +235,3 @@ class MagnifyingGlass extends StatelessWidget { ); } } - -class CustomSignatureDrawer extends HandSignatureDrawer { - const CustomSignatureDrawer({ - this.width = 1.0, - this.color = Colors.black, - }); - final Color color; - final double width; - - @override - void paint(Canvas canvas, Size size, List paths) { - for (final path in paths) { - var lineColor = color; - if (path.setup.args!['color'] != null) { - lineColor = path.setup.args!['color'] as Color; - } else { - path.setup.args!['color'] = color; - } - final paint = Paint() - ..color = lineColor - ..style = PaintingStyle.stroke - ..strokeCap = StrokeCap.round - ..strokeJoin = StrokeJoin.round - ..strokeWidth = width; - if (path.isFilled) { - canvas.drawPath(PathUtil.toLinePath(path.lines), paint); - } - } - } -} diff --git a/lib/src/views/camera/share_image_editor/layers/draw/custom_hand_signature.dart b/lib/src/views/camera/share_image_editor/layers/draw/custom_hand_signature.dart new file mode 100644 index 0000000..e5f0e74 --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/draw/custom_hand_signature.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:hand_signature/signature.dart'; +// ignore: implementation_imports +import 'package:hand_signature/src/utils.dart'; + +class CustomHandSignature extends StatelessWidget { + const CustomHandSignature({ + required this.control, + required this.isModificationEnabled, + required this.currentColor, + required this.width, + super.key, + }); + + /// The controller that manages the creation and manipulation of signature paths. + final HandSignatureControl control; + final bool isModificationEnabled; + final Color currentColor; + final double width; + + @override + Widget build(BuildContext context) { + control.params = SignaturePaintParams( + color: currentColor, + strokeWidth: 7, + ); + + final drawer = CustomSignatureDrawer(color: currentColor, width: width); + + if (isModificationEnabled) { + return HandSignature( + control: control, + drawer: drawer, + ); + } + + return IgnorePointer( + child: ClipRRect( + child: HandSignaturePaint( + control: control, + drawer: drawer, + onSize: control.notifyDimension, + ), + ), + ); + } +} + +class CustomSignatureDrawer extends HandSignatureDrawer { + const CustomSignatureDrawer({ + this.width = 1.0, + this.color = Colors.black, + }); + final Color color; + final double width; + + @override + void paint(Canvas canvas, Size size, List paths) { + for (final path in paths) { + var lineColor = color; + if (path.setup.args!['color'] != null) { + lineColor = path.setup.args!['color'] as Color; + } else { + path.setup.args!['color'] = color; + } + final paint = Paint() + ..color = lineColor + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..strokeWidth = width; + if (path.isFilled) { + canvas.drawPath(PathUtil.toLinePath(path.lines), paint); + } + } + } +} diff --git a/lib/src/views/camera/share_image_editor/layers/emoji.layer.dart b/lib/src/views/camera/share_image_editor/layers/emoji.layer.dart new file mode 100755 index 0000000..b82c1ee --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/emoji.layer.dart @@ -0,0 +1,248 @@ +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/views/camera/share_image_editor/action_button.dart'; +import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; + +/// Emoji layer +class EmojiLayer extends StatefulWidget { + const EmojiLayer({ + required this.layerData, + super.key, + this.onUpdate, + }); + final EmojiLayerData layerData; + final VoidCallback? onUpdate; + + @override + State createState() => _EmojiLayerState(); +} + +class _EmojiLayerState extends State { + double initialRotation = 0; + Offset initialOffset = Offset.zero; + Offset initialFocalPoint = Offset.zero; + double initialScale = 1; + bool deleteLayer = false; + bool twoPointerWhereDown = false; + final GlobalKey outlineKey = GlobalKey(); + final GlobalKey emojiKey = GlobalKey(); + int pointers = 0; + bool display = false; + + @override + void initState() { + super.initState(); + + if (widget.layerData.offset.dy == 0) { + // Set the initial offset to the center of the screen + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + widget.layerData.offset = Offset( + MediaQuery.of(context).size.width / 2 - (153 / 2), + MediaQuery.of(context).size.height / 2 - (153 / 2) - 100, + ); + }); + display = true; + }); + } else { + display = true; + } + } + + @override + Widget build(BuildContext context) { + if (!display) return Container(); + if (widget.layerData.isDeleted) return Container(); + return Stack( + key: outlineKey, + children: [ + Positioned( + left: widget.layerData.offset.dx, + top: widget.layerData.offset.dy, + child: PhysicalModel( + color: Colors.transparent, + borderRadius: BorderRadius.circular(180), + clipBehavior: Clip.antiAlias, + child: Listener( + onPointerUp: (details) { + setState(() { + pointers--; + if (pointers == 0) { + twoPointerWhereDown = false; + } + if (deleteLayer) { + widget.layerData.isDeleted = true; + widget.onUpdate!(); + } + }); + }, + onPointerDown: (details) { + setState(() { + pointers++; + }); + }, + child: GestureDetector( + onScaleStart: (details) { + initialScale = widget.layerData.size; + initialRotation = widget.layerData.rotation; + initialOffset = widget.layerData.offset; + initialFocalPoint = + Offset(details.focalPoint.dx, details.focalPoint.dy); + + setState(() {}); + }, + onScaleUpdate: (details) async { + if (twoPointerWhereDown && details.pointerCount != 2) { + return; + } + final outlineBox = outlineKey.currentContext! + .findRenderObject()! as RenderBox; + + final emojiBox = + emojiKey.currentContext!.findRenderObject()! as RenderBox; + + final isAtTheBottom = + (widget.layerData.offset.dy + emojiBox.size.height / 2) > + outlineBox.size.height - 80; + final isInTheCenter = + MediaQuery.of(context).size.width / 2 - 30 < + (widget.layerData.offset.dx + + emojiBox.size.width / 2) && + MediaQuery.of(context).size.width / 2 + 20 > + (widget.layerData.offset.dx + + emojiBox.size.width / 2); + + if (isAtTheBottom && isInTheCenter) { + if (!deleteLayer) { + await HapticFeedback.heavyImpact(); + } + deleteLayer = true; + } else { + deleteLayer = false; + } + setState(() { + twoPointerWhereDown = details.pointerCount >= 2; + widget.layerData.size = initialScale * details.scale; + // print(widget.layerData.size); + widget.layerData.rotation = + initialRotation + details.rotation; + + // Update the position based on the translation + final dx = (initialOffset.dx) + + (details.focalPoint.dx - initialFocalPoint.dx); + final dy = (initialOffset.dy) + + (details.focalPoint.dy - initialFocalPoint.dy); + widget.layerData.offset = Offset(dx, dy); + }); + }, + child: Transform.rotate( + angle: widget.layerData.rotation, + key: emojiKey, + child: Container( + padding: const EdgeInsets.all(44), + color: Colors.transparent, + child: ScreenshotEmoji( + emoji: widget.layerData.text, + displaySize: widget.layerData.size, + ), + ), + ), + ), + ), + ), + ), + if (pointers > 0) + Positioned( + left: 0, + right: 0, + bottom: 20, + child: Center( + child: GestureDetector( + child: ActionButton( + FontAwesomeIcons.trashCan, + tooltipText: '', + color: deleteLayer ? Colors.red : Colors.white, + ), + ), + ), + ), + ], + ); + } +} + +// Workaround: https://github.com/twonlyapp/twonly-app/issues/349 +class ScreenshotEmoji extends StatefulWidget { + const ScreenshotEmoji({ + required this.emoji, + required this.displaySize, + super.key, + }); + final String emoji; + final double displaySize; + + @override + State createState() => _ScreenshotEmojiState(); +} + +class _ScreenshotEmojiState extends State { + final GlobalKey _boundaryKey = GlobalKey(); + ui.Image? _capturedImage; + + @override + void initState() { + super.initState(); + // Capture the emoji immediately after the first frame + WidgetsBinding.instance.addPostFrameCallback((_) => _captureEmoji()); + } + + Future _captureEmoji() async { + try { + final boundary = _boundaryKey.currentContext?.findRenderObject() + as RenderRepaintBoundary?; + if (boundary == null) return; + + final image = await boundary.toImage(pixelRatio: 4); + setState(() { + _capturedImage = image; + }); + } catch (e) { + Log.error('Error capturing emoji: $e'); + } + } + + @override + Widget build(BuildContext context) { + if (_capturedImage != null) { + return SizedBox( + width: widget.displaySize, + height: widget.displaySize, + child: RawImage( + image: _capturedImage, + fit: BoxFit.contain, + ), + ); + } + + return Stack( + children: [ + Positioned( + top: -200, // hide from the user as the size changes with the image + child: RepaintBoundary( + key: _boundaryKey, + child: Text( + widget.emoji, + style: const TextStyle(fontSize: 94), + ), + ), + ), + SizedBox(width: widget.displaySize, height: widget.displaySize), + ], + ); + } +} diff --git a/lib/src/views/camera/image_editor/layers/filter_layer.dart b/lib/src/views/camera/share_image_editor/layers/filter.layer.dart similarity index 89% rename from lib/src/views/camera/image_editor/layers/filter_layer.dart rename to lib/src/views/camera/share_image_editor/layers/filter.layer.dart index 813ea66..1648dff 100644 --- a/lib/src/views/camera/image_editor/layers/filter_layer.dart +++ b/lib/src/views/camera/share_image_editor/layers/filter.layer.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; -import 'package:twonly/src/views/camera/image_editor/layers/filters/datetime_filter.dart'; -import 'package:twonly/src/views/camera/image_editor/layers/filters/image_filter.dart'; -import 'package:twonly/src/views/camera/image_editor/layers/filters/location_filter.dart'; +import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/filters/datetime_filter.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/filters/image_filter.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/filters/location_filter.dart'; /// Main layer class FilterLayer extends StatefulWidget { diff --git a/lib/src/views/camera/image_editor/layers/filters/datetime_filter.dart b/lib/src/views/camera/share_image_editor/layers/filters/datetime_filter.dart similarity index 89% rename from lib/src/views/camera/image_editor/layers/filters/datetime_filter.dart rename to lib/src/views/camera/share_image_editor/layers/filters/datetime_filter.dart index d85e8b7..78bae80 100644 --- a/lib/src/views/camera/image_editor/layers/filters/datetime_filter.dart +++ b/lib/src/views/camera/share_image_editor/layers/filters/datetime_filter.dart @@ -1,7 +1,7 @@ import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:twonly/src/views/camera/image_editor/layers/filter_layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/filter.layer.dart'; class DateTimeFilter extends StatelessWidget { const DateTimeFilter({super.key, this.color = Colors.white}); diff --git a/lib/src/views/camera/image_editor/layers/filters/image_filter.dart b/lib/src/views/camera/share_image_editor/layers/filters/image_filter.dart similarity index 87% rename from lib/src/views/camera/image_editor/layers/filters/image_filter.dart rename to lib/src/views/camera/share_image_editor/layers/filters/image_filter.dart index 25e1b90..920a9e4 100644 --- a/lib/src/views/camera/image_editor/layers/filters/image_filter.dart +++ b/lib/src/views/camera/share_image_editor/layers/filters/image_filter.dart @@ -1,6 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:twonly/src/views/camera/image_editor/layers/filter_layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/filter.layer.dart'; class ImageFilter extends StatelessWidget { const ImageFilter({required this.imagePath, super.key}); diff --git a/lib/src/views/camera/image_editor/layers/filters/location_filter.dart b/lib/src/views/camera/share_image_editor/layers/filters/location_filter.dart similarity index 96% rename from lib/src/views/camera/image_editor/layers/filters/location_filter.dart rename to lib/src/views/camera/share_image_editor/layers/filters/location_filter.dart index fa38367..d2d36fa 100644 --- a/lib/src/views/camera/image_editor/layers/filters/location_filter.dart +++ b/lib/src/views/camera/share_image_editor/layers/filters/location_filter.dart @@ -11,8 +11,8 @@ import 'package:path_provider/path_provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/views/camera/image_editor/layers/filter_layer.dart'; -import 'package:twonly/src/views/camera/image_editor/layers/filters/datetime_filter.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/filter.layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/filters/datetime_filter.dart'; class LocationFilter extends StatefulWidget { const LocationFilter({super.key}); diff --git a/lib/src/views/camera/image_editor/layers/text_layer.dart b/lib/src/views/camera/share_image_editor/layers/text.layer.dart similarity index 98% rename from lib/src/views/camera/image_editor/layers/text_layer.dart rename to lib/src/views/camera/share_image_editor/layers/text.layer.dart index 5e9627f..3e1da6a 100755 --- a/lib/src/views/camera/image_editor/layers/text_layer.dart +++ b/lib/src/views/camera/share_image_editor/layers/text.layer.dart @@ -5,8 +5,8 @@ import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:twonly/src/providers/image_editor.provider.dart'; -import 'package:twonly/src/views/camera/image_editor/action_button.dart'; -import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/action_button.dart'; +import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; /// Text layer class TextLayer extends StatefulWidget { diff --git a/lib/src/views/camera/image_editor/layers_viewer.dart b/lib/src/views/camera/share_image_editor/layers_viewer.dart similarity index 62% rename from lib/src/views/camera/image_editor/layers_viewer.dart rename to lib/src/views/camera/share_image_editor/layers_viewer.dart index 11312d1..2cb51e7 100644 --- a/lib/src/views/camera/image_editor/layers_viewer.dart +++ b/lib/src/views/camera/share_image_editor/layers_viewer.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; -import 'package:twonly/src/views/camera/image_editor/layers/background_layer.dart'; -import 'package:twonly/src/views/camera/image_editor/layers/draw_layer.dart'; -import 'package:twonly/src/views/camera/image_editor/layers/emoji_layer.dart'; -import 'package:twonly/src/views/camera/image_editor/layers/filter_layer.dart'; -import 'package:twonly/src/views/camera/image_editor/layers/text_layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/background.layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/draw.layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/emoji.layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/filter.layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/text.layer.dart'; /// View stacked layers (unbounded height, width) class LayersViewer extends StatelessWidget { @@ -37,7 +37,9 @@ class LayersViewer extends StatelessWidget { ...layers .where( (layerItem) => - layerItem is EmojiLayerData || layerItem is DrawLayerData, + layerItem is EmojiLayerData || + layerItem is DrawLayerData || + layerItem is TextLayerData, ) .map((layerItem) { if (layerItem is EmojiLayerData) { @@ -52,16 +54,15 @@ class LayersViewer extends StatelessWidget { layerData: layerItem, onUpdate: onUpdate, ); + } else if (layerItem is TextLayerData) { + return TextLayer( + key: layerItem.key, + layerData: layerItem, + onUpdate: onUpdate, + ); } return Container(); }), - ...layers.whereType().map((layerItem) { - return TextLayer( - key: layerItem.key, - layerData: layerItem, - onUpdate: onUpdate, - ); - }), ], ); } diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index a87e7d8..7a6cbbb 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -9,7 +9,7 @@ import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/camera_send_to_view.dart'; +import 'package:twonly/src/views/camera/camera_send_to.view.dart'; import 'package:twonly/src/views/chats/chat_list_components/last_message_time.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart'; diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 62a9456..210c6ba 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -14,8 +14,8 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dar import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; -import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart'; +import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; +import 'package:twonly/src/views/components/emoji_picker.bottom.dart'; import 'package:twonly/src/views/chats/message_info.view.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/context_menu.component.dart'; diff --git a/lib/src/views/chats/chat_messages_components/message_input.dart b/lib/src/views/chats/chat_messages_components/message_input.dart index 24d252a..6c195c7 100644 --- a/lib/src/views/chats/chat_messages_components/message_input.dart +++ b/lib/src/views/chats/chat_messages_components/message_input.dart @@ -14,7 +14,7 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/camera_send_to_view.dart'; +import 'package:twonly/src/views/camera/camera_send_to.view.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart'; class MessageInput extends StatefulWidget { diff --git a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart index 95485ba..a2cec19 100644 --- a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart +++ b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart @@ -112,7 +112,7 @@ class _MessageSendStateIconState extends State { case MessageSendState.receivedOpened: icon = Icon(Icons.crop_square, size: 14, color: color); if (message.content != null) { - if (isEmoji(message.content!)) { + if (isOneEmoji(message.content!)) { icon = Text( message.content!, style: const TextStyle(fontSize: 12), diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index 9544731..7760c61 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -20,7 +20,7 @@ import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/camera_send_to_view.dart'; +import 'package:twonly/src/views/camera/camera_send_to.view.dart'; import 'package:twonly/src/views/chats/media_viewer_components/reaction_buttons.component.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/loader.dart'; diff --git a/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart b/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart index 5da9f3d..827c954 100644 --- a/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart +++ b/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart @@ -5,8 +5,8 @@ import 'package:flutter/scheduler.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; -import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart'; +import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; +import 'package:twonly/src/views/components/emoji_picker.bottom.dart'; import 'package:twonly/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; diff --git a/lib/src/views/components/animate_icon.dart b/lib/src/views/components/animate_icon.dart index 28f6462..eb77cd9 100644 --- a/lib/src/views/components/animate_icon.dart +++ b/lib/src/views/components/animate_icon.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; -import 'package:twonly/src/views/camera/image_editor/data/data.dart'; // animations from: https://googlefonts.github.io/noto-emoji-animation/ // from https://github.com/eitanliu/emoji_regex/tree/master @@ -9,11 +8,8 @@ RegExp emojiRegex() => RegExp( r'[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26D3\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26F9(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC3\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC08\uDC26](?:\u200D\u2B1B)?|[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD74\uDD90](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC25\uDC27-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEDC-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDC41\uFE0F?(?:\u200D\uD83D\uDDE8\uFE0F?)?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE])))?))?|\uDC6F(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDD75(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?)|\uD83E(?:[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF8](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE7C\uDE80-\uDE88\uDE90-\uDEBD\uDEBF-\uDEC2\uDECE-\uDEDB\uDEE0-\uDEE8]|\uDD3C(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF])?|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?)', ); -bool isEmoji(String character) { +bool isOneEmoji(String character) { final matches = emojiRegex().allMatches(character); - if (emojis.contains(character)) { - return true; - } if (matches.length == 1) { final match = matches.first; return match.start == 0 && match.end == character.length; @@ -226,7 +222,7 @@ class EmojiAnimation extends StatelessWidget { static bool supported(String emoji) { if (emoji.length > 4) return false; - return animatedIcons.containsKey(emoji) || isEmoji(emoji); + return animatedIcons.containsKey(emoji) || isOneEmoji(emoji); } @override @@ -239,7 +235,7 @@ class EmojiAnimation extends StatelessWidget { 'assets/animated_icons/${animatedIcons[emoji]}', repeat: repeat, ); - } else if (isEmoji(emoji)) { + } else if (isOneEmoji(emoji)) { return Text( emoji, style: const TextStyle(fontSize: 60), diff --git a/lib/src/views/camera/image_editor/modules/all_emojis.dart b/lib/src/views/components/emoji_picker.bottom.dart similarity index 97% rename from lib/src/views/camera/image_editor/modules/all_emojis.dart rename to lib/src/views/components/emoji_picker.bottom.dart index a19d30d..cbca1a4 100755 --- a/lib/src/views/camera/image_editor/modules/all_emojis.dart +++ b/lib/src/views/components/emoji_picker.bottom.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/material.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; class EmojiPickerBottom extends StatelessWidget { const EmojiPickerBottom({super.key}); diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index b19210c..16018e1 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -14,7 +14,7 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart'; import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart'; -import 'package:twonly/src/views/camera/share_image_editor_view.dart'; +import 'package:twonly/src/views/camera/share_image_editor.view.dart'; import 'package:twonly/src/views/chats/chat_list.view.dart'; import 'package:twonly/src/views/memories/memories.view.dart'; diff --git a/lib/src/views/memories/memories_photo_slider.view.dart b/lib/src/views/memories/memories_photo_slider.view.dart index b0cfaf5..2b1c9b4 100644 --- a/lib/src/views/memories/memories_photo_slider.view.dart +++ b/lib/src/views/memories/memories_photo_slider.view.dart @@ -9,7 +9,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/camera_preview_components/save_to_gallery.dart'; -import 'package:twonly/src/views/camera/share_image_editor_view.dart'; +import 'package:twonly/src/views/camera/share_image_editor.view.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/components/video_player_wrapper.dart'; diff --git a/lib/src/views/settings/help/credits.view.dart b/lib/src/views/settings/help/credits.view.dart index ca50aa8..4a3cfaa 100644 --- a/lib/src/views/settings/help/credits.view.dart +++ b/lib/src/views/settings/help/credits.view.dart @@ -4,7 +4,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/image_editor/layers/filters/location_filter.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/filters/location_filter.dart'; import 'package:url_launcher/url_launcher.dart'; class UrlListTitle extends StatelessWidget { diff --git a/test/unit_test.dart b/test/unit_test.dart index c8eb739..80a4db4 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -8,10 +8,10 @@ import 'package:twonly/src/views/components/animate_icon.dart'; void main() { group('testing utils', () { test('test isEmoji function', () { - expect(isEmoji('Hallo'), false); - expect(isEmoji('😂'), true); - expect(isEmoji('😂😂'), false); - expect(isEmoji('Hallo 😂'), false); + expect(isOneEmoji('Hallo'), false); + expect(isOneEmoji('😂'), true); + expect(isOneEmoji('😂😂'), false); + expect(isOneEmoji('Hallo 😂'), false); }); test('test proof-of-work simple', () async { From 15c5a44b7d0eade30d1c0d120c87ab005f1fbbe4 Mon Sep 17 00:00:00 2001 From: otsmr Date: Tue, 20 Jan 2026 00:44:32 +0100 Subject: [PATCH 05/17] starting with #366 --- CHANGELOG.md | 4 +- lib/src/services/intent/links.intent.dart | 10 +- .../camera_preview_controller_view.dart | 17 +- .../main_camera_controller.dart | 7 + .../camera_preview_components/send_to.dart | 25 +-- .../views/camera/share_image_editor.view.dart | 12 +- .../{data => }/image_item.dart | 0 .../{data/layer.dart => layer_data.dart} | 15 +- .../layers/background.layer.dart | 2 +- .../share_image_editor/layers/draw.layer.dart | 2 +- .../layers/emoji.layer.dart | 2 +- .../layers/filter.layer.dart | 2 +- .../layers/link_preview.layer.dart | 25 +++ .../layers/link_preview/parse_link.dart | 185 ++++++++++++++++++ .../layers/link_preview/parser/base.dart | 29 +++ .../link_preview/parser/html_parser.dart | 40 ++++ .../link_preview/parser/json_ld_parser.dart | 80 ++++++++ .../layers/link_preview/parser/og_parser.dart | 35 ++++ .../link_preview/parser/other_parser.dart | 39 ++++ .../link_preview/parser/twitter_parser.dart | 48 +++++ .../layers/link_preview/parser/util.dart | 38 ++++ .../link_preview/parser/youtube_parser.dart | 80 ++++++++ .../layers/link_preview/utils.dart | 62 ++++++ .../share_image_editor/layers/text.layer.dart | 2 +- .../share_image_editor/layers_viewer.dart | 10 +- .../message_context_menu.dart | 2 +- .../reaction_buttons.component.dart | 2 +- .../views/components/emoji_picker.bottom.dart | 2 +- lib/src/views/home.view.dart | 16 +- pubspec.lock | 2 +- pubspec.yaml | 1 + test/features/link_parser_test.dart | 50 +++++ 32 files changed, 806 insertions(+), 40 deletions(-) rename lib/src/views/camera/share_image_editor/{data => }/image_item.dart (100%) rename lib/src/views/camera/share_image_editor/{data/layer.dart => layer_data.dart} (88%) create mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview.layer.dart create mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart create mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart create mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview/parser/html_parser.dart create mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld_parser.dart create mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview/parser/og_parser.dart create mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview/parser/other_parser.dart create mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview/parser/twitter_parser.dart create mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart create mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview/parser/youtube_parser.dart create mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview/utils.dart create mode 100644 test/features/link_parser_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index aa14e5a..e222556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,9 @@ ## 0.0.89 - Adds option to manual focus in the camera -- Adds support to switch between front and back camera during video recording +- Adds support to switch between front and back cameras during video recording - Adds basic face filters -- Improves image editor like emojies or text under a drawing can be moved +- Improves image editor, like emojis or text under a drawing can be moved - Fixes issue with emojis disappearing in the image editor ## 0.0.86 diff --git a/lib/src/services/intent/links.intent.dart b/lib/src/services/intent/links.intent.dart index 055eae8..305ae93 100644 --- a/lib/src/services/intent/links.intent.dart +++ b/lib/src/services/intent/links.intent.dart @@ -148,10 +148,8 @@ Future handleIntentMediaFile( ); } -Future handleIntentSharedFile( - BuildContext context, - List files, -) async { +Future handleIntentSharedFile(BuildContext context, + List files, void Function(Uri) onUrlCallBack) async { for (final file in files) { if (file.value == null) { Log.error( @@ -163,7 +161,9 @@ Future handleIntentSharedFile( switch (file.type) { case SharedMediaType.URL: - // await handleIntentUrl(context, Uri.parse(file.value!)); + if (file.value?.startsWith('http') ?? false) { + onUrlCallBack(Uri.parse(file.value!)); + } case SharedMediaType.IMAGE: var type = MediaType.image; if (file.value!.endsWith('.gif')) { diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index b5ae1b7..c7dbfa1 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; + import 'package:camera/camera.dart'; import 'package:clock/clock.dart'; import 'package:device_info_plus/device_info_plus.dart'; @@ -27,8 +28,8 @@ import 'package:twonly/src/views/camera/camera_preview_components/permissions_vi import 'package:twonly/src/views/camera/camera_preview_components/send_to.dart'; import 'package:twonly/src/views/camera/camera_preview_components/video_recording_time.dart'; import 'package:twonly/src/views/camera/camera_preview_components/zoom_selector.dart'; -import 'package:twonly/src/views/camera/share_image_editor/action_button.dart'; import 'package:twonly/src/views/camera/share_image_editor.view.dart'; +import 'package:twonly/src/views/camera/share_image_editor/action_button.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/loader.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; @@ -351,6 +352,7 @@ class _CameraPreviewViewState extends State { sendToGroup: widget.sendToGroup, mediaFileService: mediaFileService, mainCameraController: mc, + previewLink: mc.sharedLinkForPreview, ), transitionsBuilder: (context, animation, secondaryAnimation, child) { return child; @@ -631,7 +633,18 @@ class _CameraPreviewViewState extends State { if (!_sharePreviewIsShown && widget.sendToGroup != null && !_isVideoRecording) - SendToWidget(sendTo: widget.sendToGroup!.groupName), + ShowTitleText( + title: widget.sendToGroup!.groupName, + desc: context.lang.cameraPreviewSendTo, + ), + if (!_sharePreviewIsShown && + mc.sharedLinkForPreview != null && + !_isVideoRecording) + ShowTitleText( + title: mc.sharedLinkForPreview?.host ?? '', + desc: 'Link', + isLink: true, + ), if (!_sharePreviewIsShown && !_isVideoRecording && !widget.hideControllers) diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart index fb433ce..50bc290 100644 --- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart @@ -55,6 +55,13 @@ class MainCameraController { GlobalKey cameraPreviewKey = GlobalKey(); bool isSelectingFaceFilters = false; + Uri? sharedLinkForPreview; + + void setSharedLinkForPreview(Uri url) { + sharedLinkForPreview = url; + setState(); + } + final BarcodeScanner _barcodeScanner = BarcodeScanner(); final FaceDetector _faceDetector = FaceDetector( options: FaceDetectorOptions( diff --git a/lib/src/views/camera/camera_preview_components/send_to.dart b/lib/src/views/camera/camera_preview_components/send_to.dart index 76e7a7c..5b55bc0 100644 --- a/lib/src/views/camera/camera_preview_components/send_to.dart +++ b/lib/src/views/camera/camera_preview_components/send_to.dart @@ -1,22 +1,25 @@ import 'package:flutter/material.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/utils/misc.dart'; -class SendToWidget extends StatelessWidget { - const SendToWidget({ - required this.sendTo, +class ShowTitleText extends StatelessWidget { + const ShowTitleText({ + required this.desc, + required this.title, + this.isLink = false, super.key, }); - final String sendTo; + final String title; + final String desc; + final bool isLink; @override Widget build(BuildContext context) { - const textStyle = TextStyle( + final textStyle = TextStyle( color: Colors.white, fontWeight: FontWeight.bold, - fontSize: 24, + fontSize: isLink ? 14 : 24, decoration: TextDecoration.none, - shadows: [ + shadows: const [ Shadow( color: Color.fromARGB(122, 0, 0, 0), blurRadius: 5, @@ -26,7 +29,7 @@ class SendToWidget extends StatelessWidget { final boldTextStyle = textStyle.copyWith( fontWeight: FontWeight.normal, - fontSize: 28, + fontSize: isLink ? 17 : 28, ); return Positioned( @@ -36,12 +39,12 @@ class SendToWidget extends StatelessWidget { child: Column( children: [ Text( - context.lang.cameraPreviewSendTo, + desc, textAlign: TextAlign.center, style: textStyle, ), Text( - substringBy(sendTo, 20), + substringBy(title, isLink ? 30 : 20), textAlign: TextAlign.center, style: boldTextStyle, // Use the bold text style here ), diff --git a/lib/src/views/camera/share_image_editor.view.dart b/lib/src/views/camera/share_image_editor.view.dart index cd4ecbf..87dcecb 100644 --- a/lib/src/views/camera/share_image_editor.view.dart +++ b/lib/src/views/camera/share_image_editor.view.dart @@ -22,8 +22,8 @@ import 'package:twonly/src/views/camera/camera_preview_components/save_to_galler import 'package:twonly/src/views/camera/share_image_contact_selection.view.dart'; import 'package:twonly/src/views/camera/share_image_contact_selection/select_show_time.dart'; import 'package:twonly/src/views/camera/share_image_editor/action_button.dart'; -import 'package:twonly/src/views/camera/share_image_editor/data/image_item.dart'; -import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/image_item.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers_viewer.dart'; import 'package:twonly/src/views/components/emoji_picker.bottom.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; @@ -38,6 +38,7 @@ class ShareImageEditorView extends StatefulWidget { const ShareImageEditorView({ required this.sharedFromGallery, required this.mediaFileService, + this.previewLink, super.key, this.imageBytesFuture, this.sendToGroup, @@ -48,6 +49,7 @@ class ShareImageEditorView extends StatefulWidget { final bool sharedFromGallery; final MediaFileService mediaFileService; final MainCameraController? mainCameraController; + final Uri? previewLink; @override State createState() => _ShareImageEditorView(); } @@ -78,6 +80,12 @@ class _ShareImageEditorView extends State { layers.add(FilterLayerData(key: GlobalKey())); } + if (widget.previewLink != null) { + layers.add( + LinkPreviewLayerData(key: GlobalKey(), link: widget.previewLink!), + ); + } + if (widget.sendToGroup != null) { selectedGroupIds.add(widget.sendToGroup!.groupId); } diff --git a/lib/src/views/camera/share_image_editor/data/image_item.dart b/lib/src/views/camera/share_image_editor/image_item.dart similarity index 100% rename from lib/src/views/camera/share_image_editor/data/image_item.dart rename to lib/src/views/camera/share_image_editor/image_item.dart diff --git a/lib/src/views/camera/share_image_editor/data/layer.dart b/lib/src/views/camera/share_image_editor/layer_data.dart similarity index 88% rename from lib/src/views/camera/share_image_editor/data/layer.dart rename to lib/src/views/camera/share_image_editor/layer_data.dart index 04b5e34..d28b967 100755 --- a/lib/src/views/camera/share_image_editor/data/layer.dart +++ b/lib/src/views/camera/share_image_editor/layer_data.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hand_signature/signature.dart'; -import 'package:twonly/src/views/camera/share_image_editor/data/image_item.dart'; +import 'package:twonly/src/views/camera/share_image_editor/image_item.dart'; /// Layer class with some common properties class Layer { @@ -28,7 +28,6 @@ class Layer { bool showCustomButtons; } -/// Attributes used by [BackgroundLayer] class BackgroundLayerData extends Layer { BackgroundLayerData({ required super.key, @@ -38,6 +37,14 @@ class BackgroundLayerData extends Layer { bool imageLoaded = false; } +class LinkPreviewLayerData extends Layer { + LinkPreviewLayerData({ + required super.key, + required this.link, + }); + Uri link; +} + class FilterLayerData extends Layer { FilterLayerData({ required super.key, @@ -46,7 +53,6 @@ class FilterLayerData extends Layer { int page = 1; } -/// Attributes used by [EmojiLayer] class EmojiLayerData extends Layer { EmojiLayerData({ required super.key, @@ -62,7 +68,6 @@ class EmojiLayerData extends Layer { double size; } -/// Attributes used by [TextLayer] class TextLayerData extends Layer { TextLayerData({ required super.key, @@ -78,9 +83,7 @@ class TextLayerData extends Layer { int textLayersBefore; } -/// Attributes used by [DrawLayer] class DrawLayerData extends Layer { - // String text; DrawLayerData({ required super.key, super.offset, diff --git a/lib/src/views/camera/share_image_editor/layers/background.layer.dart b/lib/src/views/camera/share_image_editor/layers/background.layer.dart index e4158af..f12816c 100755 --- a/lib/src/views/camera/share_image_editor/layers/background.layer.dart +++ b/lib/src/views/camera/share_image_editor/layers/background.layer.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; class BackgroundLayer extends StatefulWidget { const BackgroundLayer({ diff --git a/lib/src/views/camera/share_image_editor/layers/draw.layer.dart b/lib/src/views/camera/share_image_editor/layers/draw.layer.dart index 64d528e..aabddfd 100644 --- a/lib/src/views/camera/share_image_editor/layers/draw.layer.dart +++ b/lib/src/views/camera/share_image_editor/layers/draw.layer.dart @@ -4,7 +4,7 @@ import 'package:hand_signature/signature.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/share_image_editor/action_button.dart'; -import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/draw/custom_hand_signature.dart'; class DrawLayer extends StatefulWidget { diff --git a/lib/src/views/camera/share_image_editor/layers/emoji.layer.dart b/lib/src/views/camera/share_image_editor/layers/emoji.layer.dart index b82c1ee..ac6020f 100755 --- a/lib/src/views/camera/share_image_editor/layers/emoji.layer.dart +++ b/lib/src/views/camera/share_image_editor/layers/emoji.layer.dart @@ -6,7 +6,7 @@ import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/views/camera/share_image_editor/action_button.dart'; -import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; /// Emoji layer class EmojiLayer extends StatefulWidget { diff --git a/lib/src/views/camera/share_image_editor/layers/filter.layer.dart b/lib/src/views/camera/share_image_editor/layers/filter.layer.dart index 1648dff..8ee46fe 100644 --- a/lib/src/views/camera/share_image_editor/layers/filter.layer.dart +++ b/lib/src/views/camera/share_image_editor/layers/filter.layer.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/filters/datetime_filter.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/filters/image_filter.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/filters/location_filter.dart'; diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview.layer.dart b/lib/src/views/camera/share_image_editor/layers/link_preview.layer.dart new file mode 100644 index 0000000..029a536 --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/link_preview.layer.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; + +class LinkPreviewLayer extends StatefulWidget { + const LinkPreviewLayer({ + required this.layerData, + super.key, + this.onUpdate, + }); + final LinkPreviewLayerData layerData; + final VoidCallback? onUpdate; + + @override + State createState() => _LinkPreviewLayerState(); +} + +class _LinkPreviewLayerState extends State { + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.zero, + child: Text(widget.layerData.link.toString()), + ); + } +} diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart new file mode 100644 index 0000000..aa75259 --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart @@ -0,0 +1,185 @@ +// Based on: https://github.com/sur950/any_link_preview +// Copyright (c) 2020-2024 Konakanchi Venkata Suresh Babu + +import 'dart:convert'; + +import 'package:html/dom.dart' show Document; +import 'package:html/parser.dart'; +import 'package:http/http.dart' as http; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/html_parser.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld_parser.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/og_parser.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/other_parser.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/twitter_parser.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/youtube_parser.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/utils.dart'; + +Future getMetadata(String link) async { + const userAgent = 'WhatsApp/2.21.12.21 A'; + try { + final linkToFetch = link.trim(); + final info = await getInfo(linkToFetch, userAgent); + + final img = info?.image ?? ''; + if (img.isNotEmpty) { + info?.image = resolveImageUrl(link, img); + } + + return info; + } catch (error) { + return null; + } +} + +String resolveImageUrl(String baseUrl, String imageUrl) { + try { + final baseUri = Uri.parse(baseUrl); + return baseUri.resolve(imageUrl).toString(); + } catch (e) { + return imageUrl; + } +} + +Future getInfo( + String url, + String userAgent, +) async { + Metadata? info; + + info = Metadata() + ..title = getDomain(url) + ..desc = url + ..siteName = getDomain(url) + ..url = url; + + try { + final videoId = getYouTubeVideoId(url); + final response = videoId == null + ? await fetchWithRedirects( + Uri.parse(url), + userAgent: userAgent, + ) + : await getYoutubeData( + videoId, + userAgent, + ); + + final headerContentType = response.headers['content-type']; + + if (headerContentType != null && headerContentType.startsWith('image/')) { + info + ..title = '' + ..desc = '' + ..siteName = '' + ..image = url; + return info; + } + + final document = responseToDocument(response); + if (document == null) return info; + + final data_ = _parse(document, url: url); + + return data_; + } catch (error) { + Log.warn('Error in $url response ($error)'); + return info; + } +} + +Document? responseToDocument(http.Response response) { + if (response.statusCode != 200) return null; + + Document? document; + try { + document = parse(utf8.decode(response.bodyBytes)); + } catch (err) { + return document; + } + + return document; +} + +Metadata _parse(Document? document, {String? url}) { + final output = Metadata(); + + final parsers = [ + _openGraph(document), + _twitterCard(document), + _youtubeCard(document), + _jsonLdSchema(document), + _htmlMeta(document), + _otherParser(document), + ]; + + for (final p in parsers) { + if (p == null) continue; + + output.title ??= p.title; + output.desc ??= p.desc; + output.image ??= p.image; + output.siteName ??= p.siteName; + output.url ??= p.url ?? url; + + if (output.hasAllMetadata) break; + } + + final url_ = output.url ?? url; + final image = output.image; + if (url_ != null && image != null) { + output.image = Uri.parse(url_).resolve(image).toString(); + } + + return output; +} + +Metadata? _openGraph(Document? document) { + try { + return OpenGraphParser(document).parse(); + } catch (e) { + return null; + } +} + +Metadata? _htmlMeta(Document? document) { + try { + return HtmlMetaParser(document).parse(); + } catch (e) { + return null; + } +} + +Metadata? _jsonLdSchema(Document? document) { + try { + return JsonLdParser(document).parse(); + } catch (e) { + return null; + } +} + +Metadata? _youtubeCard(Document? document) { + try { + return YoutubeParser(document).parse(); + } catch (e) { + return null; + } +} + +Metadata? _twitterCard(Document? document) { + try { + return TwitterParser(document).parse(); + } catch (e) { + return null; + } +} + +Metadata? _otherParser(Document? document) { + try { + return OtherParser(document).parse(); + } catch (e) { + return null; + } +} diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart new file mode 100644 index 0000000..37a4e0e --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart @@ -0,0 +1,29 @@ +mixin BaseMetaInfo { + String? title; + String? desc; + String? image; + String? url; + String? siteName; + + /// Returns `true` if any parameter other than [url] is filled. + bool get hasData => + ((title?.isNotEmpty ?? false) && title != 'null') || + ((desc?.isNotEmpty ?? false) && desc != 'null') || + ((image?.isNotEmpty ?? false) && image != 'null'); + + Metadata parse() { + return Metadata() + ..title = title + ..desc = desc + ..image = image + ..url = url + ..siteName = siteName; + } +} + +/// Container class for Metadata. +class Metadata with BaseMetaInfo { + bool get hasAllMetadata { + return title != null && desc != null && image != null && url != null; + } +} diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/html_parser.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/html_parser.dart new file mode 100644 index 0000000..78d53c4 --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/html_parser.dart @@ -0,0 +1,40 @@ +import 'package:html/dom.dart'; + +import 'base.dart'; +import 'util.dart'; + +/// Parses [Metadata] from ``, ``, and `<img>` tags. +class HtmlMetaParser with BaseMetaInfo { + HtmlMetaParser(this._document); + + /// The [Document] to parse. + final Document? _document; + + /// Get the [Metadata.title] from the <title> tag. + @override + String? get title => _document?.head?.querySelector('title')?.text; + + /// Get the [Metadata.desc] from the content of the + /// <meta name="description"> tag. + @override + String? get desc => _document?.head + ?.querySelector("meta[name='description']") + ?.attributes + .get('content'); + + /// Get the [Metadata.image] from the first <img> tag in the body. + @override + String? get image => + _document?.body?.querySelector('img')?.attributes.get('src'); + + /// Get the [Metadata.siteName] from the content of the + /// <meta name="site_name"> meta tag. + @override + String? get siteName => _document?.head + ?.querySelector("meta[name='site_name']") + ?.attributes + .get('content'); + + @override + String toString() => parse().toString(); +} diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld_parser.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld_parser.dart new file mode 100644 index 0000000..7a0c497 --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld_parser.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; + +import 'package:html/dom.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/og_parser.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart'; + +/// Parses [Metadata] from `json-ld` data in `<script>` tags. +class JsonLdParser with BaseMetaInfo { + JsonLdParser(this.document) { + _jsonData = _parseToJson(document); + } + + /// The [Document] to parse. + Document? document; + dynamic _jsonData; + + dynamic _parseToJson(Document? document) { + final data = document?.head + ?.querySelector("script[type='application/ld+json']") + ?.innerHtml; + if (data == null) return null; + // For multiline json file + // Replacing all new line characters with empty space + // before performing json decode on data + return jsonDecode(data.replaceAll('\n', ' ')); + } + + /// Get the [Metadata.title] from the <title> tag. + @override + String? get title { + final data = _jsonData; + if (data is Map<String, dynamic>) { + return data['name'] as String? ?? data['headline'] as String?; + } + return null; + } + + /// Get the [Metadata.desc] from the content of the + /// <meta name="description"> tag. + @override + String? get desc { + final data = _jsonData; + if (data is List<Map<String, dynamic>>) { + return data.first['description'] as String? ?? + data.first['headline'] as String?; + } else if (data is Map<String, dynamic>) { + return data['description'] as String? ?? data['description'] as String?; + } + return null; + } + + /// Get the [Metadata.image] from the first <img> tag in the body. + @override + String? get image { + final data = _jsonData; + if (data is List && data.isNotEmpty) { + return _imgResultToStr(data.first['logo'] ?? data.first['image']); + } else if (data is Map) { + return _imgResultToStr( + data.getDynamic('logo') ?? data.getDynamic('image'), + ); + } + return null; + } + + /// JSON LD does not have a siteName property, so we get it from + /// [og:site_name] if available. + @override + String? get siteName => OpenGraphParser(document).siteName; + + String? _imgResultToStr(dynamic result) { + if (result is List && result.isNotEmpty) result = result.first; + if (result is String) return result; + return null; + } + + @override + String toString() => parse().toString(); +} diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/og_parser.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/og_parser.dart new file mode 100644 index 0000000..a1a0efd --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/og_parser.dart @@ -0,0 +1,35 @@ +import 'package:html/dom.dart'; + +import 'base.dart'; +import 'util.dart'; + +/// Parses [Metadata] from `<meta property='og:*'>` tags. +class OpenGraphParser with BaseMetaInfo { + OpenGraphParser(this._document); + + /// The [Document] to parse. + final Document? _document; + + /// Get [Metadata.title] from 'og:title'. + @override + String? get title => getProperty(_document, property: 'og:title'); + + /// Get [Metadata.desc] from 'og:description'. + @override + String? get desc => getProperty(_document, property: 'og:description'); + + /// Get [Metadata.image] from 'og:image'. + @override + String? get image => getProperty(_document, property: 'og:image'); + + /// Get [Metadata.siteName] from 'og:site_name'. + @override + String? get siteName => getProperty(_document, property: 'og:site_name'); + + /// Get [Metadata.url] from 'og:url'. + @override + String? get url => getProperty(_document, property: 'og:url'); + + @override + String toString() => parse().toString(); +} diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/other_parser.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/other_parser.dart new file mode 100644 index 0000000..132ce19 --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/other_parser.dart @@ -0,0 +1,39 @@ +import 'package:html/dom.dart'; + +import 'base.dart'; +import 'util.dart'; + +/// Parses [Metadata] from `<meta attribute: 'name' property='*'>` tags. +class OtherParser with BaseMetaInfo { + OtherParser(this._document); + + /// The [Document] to be parse + final Document? _document; + + /// Get [Metadata.title] from 'title'. + @override + String? get title => + getProperty(_document, attribute: 'name', property: 'title'); + + /// Get [Metadata.desc] from 'description'. + @override + String? get desc => + getProperty(_document, attribute: 'name', property: 'description'); + + /// Get [Metadata.image] from 'image'. + @override + String? get image => + getProperty(_document, attribute: 'name', property: 'image'); + + /// Get [Metadata.siteName] from 'description'. + @override + String? get siteName => + getProperty(_document, attribute: 'name', property: 'site_name'); + + /// Get [Metadata.url] from 'url'. + @override + String? get url => getProperty(_document, attribute: 'name', property: 'url'); + + @override + String toString() => parse().toString(); +} diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/twitter_parser.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/twitter_parser.dart new file mode 100644 index 0000000..07147e6 --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/twitter_parser.dart @@ -0,0 +1,48 @@ +import 'package:html/dom.dart'; + +import 'base.dart'; +import 'og_parser.dart'; +import 'util.dart'; + +/// Parses [Metadata] from `<meta property='twitter:*'>` tags. +class TwitterParser with BaseMetaInfo { + TwitterParser(this._document); + + /// The [Document] to parse. + final Document? _document; + + /// Get [Metadata.title] from 'twitter:title' + @override + String? get title => + getProperty(_document, attribute: 'name', property: 'twitter:title') ?? + getProperty(_document, property: 'twitter:title'); + + /// Get [Metadata.desc] from 'twitter:description' + @override + String? get desc => + getProperty( + _document, + attribute: 'name', + property: 'twitter:description', + ) ?? + getProperty(_document, property: 'twitter:description'); + + /// Get [Metadata.image] from 'twitter:image' + @override + String? get image => + getProperty(_document, attribute: 'name', property: 'twitter:image') ?? + getProperty(_document, property: 'twitter:image'); + + /// Twitter Cards do not have a siteName property, so we get it from + /// [og:site_name] if available. + @override + String? get siteName => OpenGraphParser(_document).siteName; + + /// Twitter Cards do not have a url property, so we get the url from + /// [og:url] if available. + @override + String? get url => OpenGraphParser(_document).url; + + @override + String toString() => parse().toString(); +} diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart new file mode 100644 index 0000000..66f9d51 --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart @@ -0,0 +1,38 @@ +import 'package:html/dom.dart'; + +// ignore: strict_raw_type +extension GetMethod on Map { + String? get(dynamic key) { + final value = this[key]; + if (value is List<String>) return value.first; + return value?.toString(); + } + + dynamic getDynamic(dynamic key) { + return this[key]; + } +} + +String? getDomain(String url) { + return Uri.parse(url).host.split('.')[0]; +} + +String? getProperty( + Document? document, { + String tag = 'meta', + String attribute = 'property', + String? property, + String key = 'content', +}) { + final value_ = document + ?.getElementsByTagName(tag) + .cast<Element?>() + .firstWhere( + (element) => element?.attributes[attribute] == property, + orElse: () => null, + ) + ?.attributes + .get(key); + + return value_; +} diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/youtube_parser.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/youtube_parser.dart new file mode 100644 index 0000000..9fa887a --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/youtube_parser.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; +import 'package:html/dom.dart'; +import 'base.dart'; +import 'util.dart'; + +class YoutubeParser with BaseMetaInfo { + YoutubeParser(this.document) { + _jsonData = _parseToJson(document); + } + + Document? document; + dynamic _jsonData; + + dynamic _parseToJson(Document? document) { + final data = document?.outerHtml + .replaceAll('<html><head></head><body>', '') + .replaceAll('</body></html>', ''); + if (data == null) return null; + /* For multiline json file */ + // Replacing all new line characters with empty space + // before performing json decode on data + final d = jsonDecode(data.replaceAll('\n', ' ')); + return d; + } + + /// Get the [Metadata.title] from the [<title>] tag + @override + String? get title { + final data = _jsonData; + if (data is List<Map<String, dynamic>>) { + return data.first['title'] as String?; + } else if (data is Map) { + return data.get('title'); + } + return null; + } + + /// Get the [Metadata.image] from the first <img> tag in the body + @override + String? get image { + final data = _jsonData; + if (data is List && data.isNotEmpty) { + return _imgResultToStr(data.first['thumbnail_url']); + } else if (data is Map) { + return _imgResultToStr(data.getDynamic('thumbnail_url')); + } + return null; + } + + @override + String? get siteName { + final data = _jsonData; + if (data is List<Map<String, dynamic>>) { + return data.first['provider_name'] as String?; + } else if (data is Map) { + return data.get('provider_name'); + } + return null; + } + + @override + String? get url { + final data = _jsonData; + if (data is List<Map<String, dynamic>>) { + return data.first['provider_url'] as String?; + } else if (data is Map) { + return data.get('provider_url'); + } + return null; + } + + String? _imgResultToStr(dynamic result) { + if (result is List && result.isNotEmpty) result = result.first; + if (result is String) return result; + return null; + } + + @override + String toString() => parse().toString(); +} diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/utils.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/utils.dart new file mode 100644 index 0000000..4729f17 --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/utils.dart @@ -0,0 +1,62 @@ +import 'package:http/http.dart' as http; + +Future<http.Response> fetchWithRedirects( + Uri uri, { + int maxRedirects = 7, + Map<String, String> headers = const {}, + String? userAgent, +}) async { + const userAgentFallback = 'WhatsApp/2.21.12.21 A'; + final allHeaders = <String, String>{ + ...headers, + 'User-Agent': userAgent ?? userAgentFallback, + }; + var response = await http.get(uri, headers: allHeaders); + var redirectCount = 0; + + while (_isRedirect(response) && redirectCount < maxRedirects) { + final location = response.headers['location']; + if (location == null) { + throw Exception('HTTP redirect without Location header'); + } + + response = await http.get(Uri.parse(location), headers: allHeaders); + redirectCount++; + } + + if (redirectCount >= maxRedirects) { + throw Exception('Maximum redirect limit reached'); + } + + return response; +} + +bool _isRedirect(http.Response response) { + return [301, 302, 303, 307, 308].contains(response.statusCode); +} + +Future<http.Response> getYoutubeData(String videoId, String userAgent) async { + final response = await http.get( + Uri.parse( + 'https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=$videoId&format=json', + ), + headers: { + 'User-Agent': userAgent, + }, + ); + return response; +} + +String? getYouTubeVideoId(String url) { + // Regular expression pattern to detect YouTube URLs + // with or without a proxy prefix + final regExp = RegExp( + r'(?:https?:\/\/)?(?:[^\/]+\.)?(?:youtube\.com\/(?:watch\?v=|embed\/|v\/|v\/|.+\?v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})', + ); + + // Apply the regex to the URL + final match = regExp.firstMatch(url); + + // If a match is found, return the first capture group, which is the video ID + return match?.group(1); +} diff --git a/lib/src/views/camera/share_image_editor/layers/text.layer.dart b/lib/src/views/camera/share_image_editor/layers/text.layer.dart index 3e1da6a..7294b00 100755 --- a/lib/src/views/camera/share_image_editor/layers/text.layer.dart +++ b/lib/src/views/camera/share_image_editor/layers/text.layer.dart @@ -6,7 +6,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:twonly/src/providers/image_editor.provider.dart'; import 'package:twonly/src/views/camera/share_image_editor/action_button.dart'; -import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; /// Text layer class TextLayer extends StatefulWidget { diff --git a/lib/src/views/camera/share_image_editor/layers_viewer.dart b/lib/src/views/camera/share_image_editor/layers_viewer.dart index 2cb51e7..e53e3a8 100644 --- a/lib/src/views/camera/share_image_editor/layers_viewer.dart +++ b/lib/src/views/camera/share_image_editor/layers_viewer.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/background.layer.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/draw.layer.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/emoji.layer.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/filter.layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview.layer.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/text.layer.dart'; /// View stacked layers (unbounded height, width) @@ -39,6 +40,7 @@ class LayersViewer extends StatelessWidget { (layerItem) => layerItem is EmojiLayerData || layerItem is DrawLayerData || + layerItem is LinkPreviewLayerData || layerItem is TextLayerData, ) .map((layerItem) { @@ -60,6 +62,12 @@ class LayersViewer extends StatelessWidget { layerData: layerItem, onUpdate: onUpdate, ); + } else if (layerItem is LinkPreviewLayerData) { + return LinkPreviewLayer( + key: layerItem.key, + layerData: layerItem, + onUpdate: onUpdate, + ); } return Container(); }), diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 210c6ba..44af049 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -14,7 +14,7 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dar import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; import 'package:twonly/src/views/components/emoji_picker.bottom.dart'; import 'package:twonly/src/views/chats/message_info.view.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; diff --git a/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart b/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart index 827c954..53b12d5 100644 --- a/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart +++ b/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart @@ -5,7 +5,7 @@ import 'package:flutter/scheduler.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; import 'package:twonly/src/views/components/emoji_picker.bottom.dart'; import 'package:twonly/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; diff --git a/lib/src/views/components/emoji_picker.bottom.dart b/lib/src/views/components/emoji_picker.bottom.dart index cbca1a4..6139966 100755 --- a/lib/src/views/components/emoji_picker.bottom.dart +++ b/lib/src/views/components/emoji_picker.bottom.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/material.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/share_image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; class EmojiPickerBottom extends StatelessWidget { const EmojiPickerBottom({super.key}); diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index 16018e1..355d38e 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -120,7 +120,13 @@ class HomeViewState extends State<HomeView> { _intentStreamSub = FlutterSharingIntent.instance.getMediaStream().listen( (f) { - if (mounted) handleIntentSharedFile(context, f); + if (mounted) { + handleIntentSharedFile( + context, + f, + _mainCameraController.setSharedLinkForPreview, + ); + } }, // ignore: inference_failure_on_untyped_parameter onError: (err) { @@ -129,7 +135,13 @@ class HomeViewState extends State<HomeView> { ); FlutterSharingIntent.instance.getInitialSharing().then((f) { - if (mounted) handleIntentSharedFile(context, f); + if (mounted) { + handleIntentSharedFile( + context, + f, + _mainCameraController.setSharedLinkForPreview, + ); + } }); } diff --git a/pubspec.lock b/pubspec.lock index cc26ee5..6b30fbb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -914,7 +914,7 @@ packages: source: path version: "3.0.1" html: - dependency: transitive + dependency: "direct main" description: name: html sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" diff --git a/pubspec.yaml b/pubspec.yaml index a0d05a5..0cb22ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: convert: ^3.1.2 crypto: ^3.0.7 clock: ^1.1.2 + html: ^0.15.6 # Trusted publisher flutter.dev diff --git a/test/features/link_parser_test.dart b/test/features/link_parser_test.dart new file mode 100644 index 0000000..97f1cef --- /dev/null +++ b/test/features/link_parser_test.dart @@ -0,0 +1,50 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart'; + +class LinkParserTest { + LinkParserTest({ + required this.url, + required this.title, + required this.siteName, + required this.desc, + this.image, + }); + + final String url; + final String title; + final String siteName; + final String desc; + final String? image; +} + +void main() { + test('testing different urls', () async { + final testCases = [ + LinkParserTest( + url: 'https://mastodon.social/@islieb/115883317936171927', + title: 'islieb? (@islieb@mastodon.social)', + siteName: 'Mastodon', + desc: 'Attached: 1 image', + image: + 'https://files.mastodon.social/media_attachments/files/115/883/317/526/523/824/original/6fa7ef90ec68f1f1.jpg', + ), + LinkParserTest( + url: 'https://chaos.social/@netzpolitik_feed/115921534467938262', + title: 'netzpolitik.org (@netzpolitik_feed@chaos.social)', + siteName: 'chaos.social', + desc: + 'Die EU-Kommission erkennt Open Source als entscheidend für die digitale Souveränität an und wünscht sich mehr Kommerzialisierung. Bis April will Brüssel eine neue Strategie veröffentlichen. In einer laufenden Konsultation bekräftigen Stimmen aus ganz Europa, welche Vorteile sie in offenem Quellcode sehen.\n' + '\n' + 'https://netzpolitik.org/2026/konsultation-eu-kommission-arbeitet-an-neuer-open-source-strategie/', + ), + ]; + + for (final testCase in testCases) { + final metadata = (await getMetadata(testCase.url))!; + expect(metadata.title, testCase.title); + expect(metadata.siteName, testCase.siteName); + expect(metadata.desc, testCase.desc); + expect(metadata.image, testCase.image); + } + }); +} From f5d4f97c028ce58a2b5c0c71d585028d9bf5ffa6 Mon Sep 17 00:00:00 2001 From: otsmr <git@tsmr.eu> Date: Wed, 21 Jan 2026 23:14:23 +0100 Subject: [PATCH 06/17] improving link parser --- .../layers/link_preview/parse_link.dart | 122 ++++++------------ .../layers/link_preview/parser/base.dart | 22 ++-- .../{html_parser.dart => html.parser.dart} | 3 - .../link_preview/parser/json_ld.parser.dart | 98 ++++++++++++++ .../link_preview/parser/json_ld_parser.dart | 80 ------------ .../link_preview/parser/mastodon.parser.dart | 15 +++ .../parser/{og_parser.dart => og.parser.dart} | 7 - .../{other_parser.dart => other.parser.dart} | 12 -- ...witter_parser.dart => twitter.parser.dart} | 29 ++--- ...outube_parser.dart => youtube.parser.dart} | 44 +++---- test/features/link_parser_test.dart | 90 +++++++++++-- 11 files changed, 276 insertions(+), 246 deletions(-) rename lib/src/views/camera/share_image_editor/layers/link_preview/parser/{html_parser.dart => html.parser.dart} (95%) create mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld.parser.dart delete mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld_parser.dart create mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview/parser/mastodon.parser.dart rename lib/src/views/camera/share_image_editor/layers/link_preview/parser/{og_parser.dart => og.parser.dart} (82%) rename lib/src/views/camera/share_image_editor/layers/link_preview/parser/{other_parser.dart => other.parser.dart} (62%) rename lib/src/views/camera/share_image_editor/layers/link_preview/parser/{twitter_parser.dart => twitter.parser.dart} (52%) rename lib/src/views/camera/share_image_editor/layers/link_preview/parser/{youtube_parser.dart => youtube.parser.dart} (61%) diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart index aa75259..0923f5c 100644 --- a/lib/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart @@ -8,13 +8,14 @@ import 'package:html/parser.dart'; import 'package:http/http.dart' as http; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; -import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/html_parser.dart'; -import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld_parser.dart'; -import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/og_parser.dart'; -import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/other_parser.dart'; -import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/twitter_parser.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/html.parser.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld.parser.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/mastodon.parser.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/og.parser.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/other.parser.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/twitter.parser.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart'; -import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/youtube_parser.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/youtube.parser.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/utils.dart'; Future<Metadata?> getMetadata(String link) async { @@ -81,7 +82,7 @@ Future<Metadata?> getInfo( final document = responseToDocument(response); if (document == null) return info; - final data_ = _parse(document, url: url); + final data_ = _parse(document, url); return data_; } catch (error) { @@ -103,83 +104,44 @@ Document? responseToDocument(http.Response response) { return document; } -Metadata _parse(Document? document, {String? url}) { - final output = Metadata(); +Metadata _parse(Document? document, String url) { + final output = Metadata()..url = url; - final parsers = [ - _openGraph(document), - _twitterCard(document), - _youtubeCard(document), - _jsonLdSchema(document), - _htmlMeta(document), - _otherParser(document), + final allParsers = [ + // start with vendor specific to parse the vendor type + MastodonParser(document), + YoutubeParser(document, url), + TwitterParser(document, url), + + JsonLdParser(document), + OpenGraphParser(document), + HtmlMetaParser(document), + OtherParser(document), ]; - for (final p in parsers) { - if (p == null) continue; - - output.title ??= p.title; - output.desc ??= p.desc; - output.image ??= p.image; - output.siteName ??= p.siteName; - output.url ??= p.url ?? url; - - if (output.hasAllMetadata) break; - } - - final url_ = output.url ?? url; - final image = output.image; - if (url_ != null && image != null) { - output.image = Uri.parse(url_).resolve(image).toString(); + for (final parser in allParsers) { + try { + output.vendor ??= parser.vendor; + output.title ??= parser.title; + output.desc ??= parser.desc; + if (output.vendor == Vendor.twitterPosting) { + if (output.image == null) { + if (parser.image?.contains('/media/') ?? false) { + output.image ??= parser.image; + } + } + } else { + output.image ??= parser.image; + } + output.siteName ??= parser.siteName; + output.publishDate ??= parser.publishDate; + output.likeAction ??= parser.likeAction; + output.shareAction ??= parser.shareAction; + if (output.hasAllMetadata) break; + } catch (e) { + Log.error(e); + } } return output; } - -Metadata? _openGraph(Document? document) { - try { - return OpenGraphParser(document).parse(); - } catch (e) { - return null; - } -} - -Metadata? _htmlMeta(Document? document) { - try { - return HtmlMetaParser(document).parse(); - } catch (e) { - return null; - } -} - -Metadata? _jsonLdSchema(Document? document) { - try { - return JsonLdParser(document).parse(); - } catch (e) { - return null; - } -} - -Metadata? _youtubeCard(Document? document) { - try { - return YoutubeParser(document).parse(); - } catch (e) { - return null; - } -} - -Metadata? _twitterCard(Document? document) { - try { - return TwitterParser(document).parse(); - } catch (e) { - return null; - } -} - -Metadata? _otherParser(Document? document) { - try { - return OtherParser(document).parse(); - } catch (e) { - return null; - } -} diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart index 37a4e0e..da3ba60 100644 --- a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart @@ -1,29 +1,29 @@ +enum Vendor { mastodonSocialMediaPosting, youtubeVideo, twitterPosting } + mixin BaseMetaInfo { + late String url; String? title; String? desc; String? image; - String? url; String? siteName; + Vendor? vendor; + + DateTime? publishDate; + int? likeAction; // https://schema.org/LikeAction + int? shareAction; // https://schema.org/ShareAction + /// Returns `true` if any parameter other than [url] is filled. bool get hasData => ((title?.isNotEmpty ?? false) && title != 'null') || ((desc?.isNotEmpty ?? false) && desc != 'null') || ((image?.isNotEmpty ?? false) && image != 'null'); - - Metadata parse() { - return Metadata() - ..title = title - ..desc = desc - ..image = image - ..url = url - ..siteName = siteName; - } } /// Container class for Metadata. class Metadata with BaseMetaInfo { + Metadata(); bool get hasAllMetadata { - return title != null && desc != null && image != null && url != null; + return title != null && desc != null && image != null; } } diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/html_parser.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/html.parser.dart similarity index 95% rename from lib/src/views/camera/share_image_editor/layers/link_preview/parser/html_parser.dart rename to lib/src/views/camera/share_image_editor/layers/link_preview/parser/html.parser.dart index 78d53c4..b386ab6 100644 --- a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/html_parser.dart +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/html.parser.dart @@ -34,7 +34,4 @@ class HtmlMetaParser with BaseMetaInfo { ?.querySelector("meta[name='site_name']") ?.attributes .get('content'); - - @override - String toString() => parse().toString(); } diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld.parser.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld.parser.dart new file mode 100644 index 0000000..e946065 --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld.parser.dart @@ -0,0 +1,98 @@ +import 'dart:convert'; + +import 'package:html/dom.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/og.parser.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart'; + +/// Parses [Metadata] from `json-ld` data in `<script>` tags. +class JsonLdParser with BaseMetaInfo { + JsonLdParser(this.document) { + _parseToJson(document); + } + + Document? document; + Map<String, dynamic>? _jsonData; + + void _parseToJson(Document? document) { + try { + final data = document?.head + ?.querySelector("script[type='application/ld+json']") + ?.innerHtml; + if (data == null) return; + // For multiline json file + // Replacing all new line characters with empty space + // before performing json decode on data + _jsonData = + jsonDecode(data.replaceAll('\n', ' ')) as Map<String, dynamic>; + // ignore: empty_catches + } catch (e) {} + } + + /// Get the [Metadata.title] from the <title> tag. + @override + String? get title { + final data = _jsonData; + if (data is Map<String, dynamic>) { + return data['name'] as String? ?? data['headline'] as String?; + } + return null; + } + + @override + int? get shareAction { + final statistics = _jsonData?['interactionStatistic'] as List<dynamic>?; + if (statistics != null) { + for (final statsDy in statistics) { + final stats = statsDy as Map<String, dynamic>?; + if (stats != null) { + if (stats['interactionType'] == 'https://schema.org/ShareAction') { + return stats['userInteractionCount'] as int?; + } + } + } + } + return null; + } + + @override + int? get likeAction { + final statistics = _jsonData?['interactionStatistic'] as List<dynamic>?; + if (statistics != null) { + for (final statsDy in statistics) { + final stats = statsDy as Map<String, dynamic>?; + if (stats != null) { + if (stats['interactionType'] == 'https://schema.org/LikeAction') { + return stats['userInteractionCount'] as int?; + } + } + } + } + return null; + } + + @override + String? get desc { + return _jsonData?['description'] as String?; + } + + /// Get the [Metadata.image] from the first <img> tag in the body. + @override + String? get image { + final data = _jsonData; + return _imgResultToStr( + data?.getDynamic('logo') ?? data?.getDynamic('image'), + ); + } + + /// JSON LD does not have a siteName property, so we get it from + /// [og:site_name] if available. + @override + String? get siteName => OpenGraphParser(document).siteName; + + String? _imgResultToStr(dynamic result) { + if (result is List && result.isNotEmpty) result = result.first; + if (result is String) return result; + return null; + } +} diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld_parser.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld_parser.dart deleted file mode 100644 index 7a0c497..0000000 --- a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld_parser.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'dart:convert'; - -import 'package:html/dom.dart'; -import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; -import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/og_parser.dart'; -import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart'; - -/// Parses [Metadata] from `json-ld` data in `<script>` tags. -class JsonLdParser with BaseMetaInfo { - JsonLdParser(this.document) { - _jsonData = _parseToJson(document); - } - - /// The [Document] to parse. - Document? document; - dynamic _jsonData; - - dynamic _parseToJson(Document? document) { - final data = document?.head - ?.querySelector("script[type='application/ld+json']") - ?.innerHtml; - if (data == null) return null; - // For multiline json file - // Replacing all new line characters with empty space - // before performing json decode on data - return jsonDecode(data.replaceAll('\n', ' ')); - } - - /// Get the [Metadata.title] from the <title> tag. - @override - String? get title { - final data = _jsonData; - if (data is Map<String, dynamic>) { - return data['name'] as String? ?? data['headline'] as String?; - } - return null; - } - - /// Get the [Metadata.desc] from the content of the - /// <meta name="description"> tag. - @override - String? get desc { - final data = _jsonData; - if (data is List<Map<String, dynamic>>) { - return data.first['description'] as String? ?? - data.first['headline'] as String?; - } else if (data is Map<String, dynamic>) { - return data['description'] as String? ?? data['description'] as String?; - } - return null; - } - - /// Get the [Metadata.image] from the first <img> tag in the body. - @override - String? get image { - final data = _jsonData; - if (data is List && data.isNotEmpty) { - return _imgResultToStr(data.first['logo'] ?? data.first['image']); - } else if (data is Map) { - return _imgResultToStr( - data.getDynamic('logo') ?? data.getDynamic('image'), - ); - } - return null; - } - - /// JSON LD does not have a siteName property, so we get it from - /// [og:site_name] if available. - @override - String? get siteName => OpenGraphParser(document).siteName; - - String? _imgResultToStr(dynamic result) { - if (result is List && result.isNotEmpty) result = result.first; - if (result is String) return result; - return null; - } - - @override - String toString() => parse().toString(); -} diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/mastodon.parser.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/mastodon.parser.dart new file mode 100644 index 0000000..a2f466b --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/mastodon.parser.dart @@ -0,0 +1,15 @@ +import 'package:html/dom.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; + +class MastodonParser with BaseMetaInfo { + MastodonParser(this._document); + final Document? _document; + + @override + Vendor? get vendor => ((_document?.head?.innerHtml + .contains('"repository":"mastodon/mastodon"') ?? + false) && + (_document?.head?.innerHtml.contains('SocialMediaPosting') ?? false)) + ? Vendor.mastodonSocialMediaPosting + : null; +} diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/og_parser.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/og.parser.dart similarity index 82% rename from lib/src/views/camera/share_image_editor/layers/link_preview/parser/og_parser.dart rename to lib/src/views/camera/share_image_editor/layers/link_preview/parser/og.parser.dart index a1a0efd..62952de 100644 --- a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/og_parser.dart +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/og.parser.dart @@ -25,11 +25,4 @@ class OpenGraphParser with BaseMetaInfo { /// Get [Metadata.siteName] from 'og:site_name'. @override String? get siteName => getProperty(_document, property: 'og:site_name'); - - /// Get [Metadata.url] from 'og:url'. - @override - String? get url => getProperty(_document, property: 'og:url'); - - @override - String toString() => parse().toString(); } diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/other_parser.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/other.parser.dart similarity index 62% rename from lib/src/views/camera/share_image_editor/layers/link_preview/parser/other_parser.dart rename to lib/src/views/camera/share_image_editor/layers/link_preview/parser/other.parser.dart index 132ce19..d8d1dd8 100644 --- a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/other_parser.dart +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/other.parser.dart @@ -7,33 +7,21 @@ import 'util.dart'; class OtherParser with BaseMetaInfo { OtherParser(this._document); - /// The [Document] to be parse final Document? _document; - /// Get [Metadata.title] from 'title'. @override String? get title => getProperty(_document, attribute: 'name', property: 'title'); - /// Get [Metadata.desc] from 'description'. @override String? get desc => getProperty(_document, attribute: 'name', property: 'description'); - /// Get [Metadata.image] from 'image'. @override String? get image => getProperty(_document, attribute: 'name', property: 'image'); - /// Get [Metadata.siteName] from 'description'. @override String? get siteName => getProperty(_document, attribute: 'name', property: 'site_name'); - - /// Get [Metadata.url] from 'url'. - @override - String? get url => getProperty(_document, attribute: 'name', property: 'url'); - - @override - String toString() => parse().toString(); } diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/twitter_parser.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/twitter.parser.dart similarity index 52% rename from lib/src/views/camera/share_image_editor/layers/link_preview/parser/twitter_parser.dart rename to lib/src/views/camera/share_image_editor/layers/link_preview/parser/twitter.parser.dart index 07147e6..39cba4a 100644 --- a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/twitter_parser.dart +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/twitter.parser.dart @@ -1,23 +1,18 @@ import 'package:html/dom.dart'; - -import 'base.dart'; -import 'og_parser.dart'; -import 'util.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart'; /// Parses [Metadata] from `<meta property='twitter:*'>` tags. class TwitterParser with BaseMetaInfo { - TwitterParser(this._document); - - /// The [Document] to parse. + TwitterParser(this._document, this._url); final Document? _document; + final String _url; - /// Get [Metadata.title] from 'twitter:title' @override String? get title => getProperty(_document, attribute: 'name', property: 'twitter:title') ?? getProperty(_document, property: 'twitter:title'); - /// Get [Metadata.desc] from 'twitter:description' @override String? get desc => getProperty( @@ -27,22 +22,14 @@ class TwitterParser with BaseMetaInfo { ) ?? getProperty(_document, property: 'twitter:description'); - /// Get [Metadata.image] from 'twitter:image' @override String? get image => getProperty(_document, attribute: 'name', property: 'twitter:image') ?? getProperty(_document, property: 'twitter:image'); - /// Twitter Cards do not have a siteName property, so we get it from - /// [og:site_name] if available. @override - String? get siteName => OpenGraphParser(_document).siteName; - - /// Twitter Cards do not have a url property, so we get the url from - /// [og:url] if available. - @override - String? get url => OpenGraphParser(_document).url; - - @override - String toString() => parse().toString(); + Vendor? get vendor => + _url.startsWith('https://x.com/') && _url.contains('/status/') + ? Vendor.twitterPosting + : null; } diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/youtube_parser.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/youtube.parser.dart similarity index 61% rename from lib/src/views/camera/share_image_editor/layers/link_preview/parser/youtube_parser.dart rename to lib/src/views/camera/share_image_editor/layers/link_preview/parser/youtube.parser.dart index 9fa887a..30e976a 100644 --- a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/youtube_parser.dart +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/youtube.parser.dart @@ -4,26 +4,32 @@ import 'base.dart'; import 'util.dart'; class YoutubeParser with BaseMetaInfo { - YoutubeParser(this.document) { + YoutubeParser(this.document, this.url) { _jsonData = _parseToJson(document); } + @override + String url; + Document? document; dynamic _jsonData; dynamic _parseToJson(Document? document) { - final data = document?.outerHtml - .replaceAll('<html><head></head><body>', '') - .replaceAll('</body></html>', ''); - if (data == null) return null; - /* For multiline json file */ - // Replacing all new line characters with empty space - // before performing json decode on data - final d = jsonDecode(data.replaceAll('\n', ' ')); - return d; + try { + final data = document?.outerHtml + .replaceAll('<html><head></head><body>', '') + .replaceAll('</body></html>', ''); + if (data == null) return null; + /* For multiline json file */ + // Replacing all new line characters with empty space + // before performing json decode on data + final d = jsonDecode(data.replaceAll('\n', ' ')); + return d; + } catch (e) { + return null; + } } - /// Get the [Metadata.title] from the [<title>] tag @override String? get title { final data = _jsonData; @@ -35,7 +41,6 @@ class YoutubeParser with BaseMetaInfo { return null; } - /// Get the [Metadata.image] from the first <img> tag in the body @override String? get image { final data = _jsonData; @@ -59,22 +64,13 @@ class YoutubeParser with BaseMetaInfo { } @override - String? get url { - final data = _jsonData; - if (data is List<Map<String, dynamic>>) { - return data.first['provider_url'] as String?; - } else if (data is Map) { - return data.get('provider_url'); - } - return null; - } + Vendor? get vendor => (Uri.parse(url).host.contains('youtube.com')) + ? Vendor.youtubeVideo + : null; String? _imgResultToStr(dynamic result) { if (result is List && result.isNotEmpty) result = result.first; if (result is String) return result; return null; } - - @override - String toString() => parse().toString(); } diff --git a/test/features/link_parser_test.dart b/test/features/link_parser_test.dart index 97f1cef..3361df6 100644 --- a/test/features/link_parser_test.dart +++ b/test/features/link_parser_test.dart @@ -1,20 +1,30 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; class LinkParserTest { LinkParserTest({ - required this.url, required this.title, - required this.siteName, - required this.desc, + required this.url, + this.desc, this.image, + this.siteName, + this.vendor, + this.publishDate, + this.likeAction, + this.shareAction, }); + String title; + String? desc; + String? image; + String url; + String? siteName; - final String url; - final String title; - final String siteName; - final String desc; - final String? image; + Vendor? vendor; + + DateTime? publishDate; + int? likeAction; // https://schema.org/LikeAction + int? shareAction; } void main() { @@ -27,6 +37,9 @@ void main() { desc: 'Attached: 1 image', image: 'https://files.mastodon.social/media_attachments/files/115/883/317/526/523/824/original/6fa7ef90ec68f1f1.jpg', + vendor: Vendor.mastodonSocialMediaPosting, + shareAction: 90, + likeAction: 290, ), LinkParserTest( url: 'https://chaos.social/@netzpolitik_feed/115921534467938262', @@ -36,7 +49,53 @@ void main() { 'Die EU-Kommission erkennt Open Source als entscheidend für die digitale Souveränität an und wünscht sich mehr Kommerzialisierung. Bis April will Brüssel eine neue Strategie veröffentlichen. In einer laufenden Konsultation bekräftigen Stimmen aus ganz Europa, welche Vorteile sie in offenem Quellcode sehen.\n' '\n' 'https://netzpolitik.org/2026/konsultation-eu-kommission-arbeitet-an-neuer-open-source-strategie/', + vendor: Vendor.mastodonSocialMediaPosting, + shareAction: 70, + likeAction: 90, ), + LinkParserTest( + title: 'Kuketz-Blog 🛡 (@kuketzblog@social.tchncs.de)', + url: 'https://social.tchncs.de/@kuketzblog/115898752560771936', + siteName: 'Mastodon', + desc: + 'AWS verspricht jetzt »Souveränität« mit einem »europäischen« Cloud-Angebot – Standort Deutschland, großes Vertrauens-Theater.\n' + '\n' + 'Nur: Souveränität ist keine Postleitzahl. Wenn der Anbieter Amazon heißt, bleibt es dasselbe Märchen mit neuem Umschlag: Der Cloud Act, FISA etc. gilt trotzdem. US-Recht schlägt Geografie. Das Gerede von »Souveränität« ist kein Konzept, sondern Marketing.\n' + '\n' + 'https://www.heise.de/news/AWS-verspricht-Souveraenitaet-mit-europaeischem-Cloudangebot-11141800.html', + vendor: Vendor.mastodonSocialMediaPosting, + shareAction: 15, + likeAction: 190, + ), + LinkParserTest( + title: + 'David Kriesel: Traue keinem Scan, den du nicht selbst gefälscht hast', + url: 'https://www.youtube.com/watch?v=7FeqF1-Z1g0', + siteName: 'YouTube', + vendor: Vendor.youtubeVideo, + image: 'https://i.ytimg.com/vi/7FeqF1-Z1g0/hqdefault.jpg', + ), + LinkParserTest( + title: 'netzpolitik.org (@netzpolitik_org) on X', + url: 'https://x.com/netzpolitik_org/status/1782791019412529665', + siteName: 'X (formerly Twitter)', + desc: + 'Jetzt ist wirklich Schluss: Wir verlassen als Redaktion das zur Plattform für Rechtsradikale verkommene Twitter – und freuen uns, wenn ihr uns woanders folgt.\n' + '\n' + 'https://t.co/8W0hGly5bL', + vendor: Vendor.twitterPosting, + ), + LinkParserTest( + title: 'netzpolitik.org (@netzpolitik_org) on X', + url: 'https://x.com/netzpolitik_org/status/1162346968124968960', + siteName: 'X (formerly Twitter)', + desc: + 'Weil unsere Datenanalyse zum Twitter-Account von Maaßen rechte Millieus und ihre Verbindungen offengelegt hat, haben wir einen rechten Shitstorm an der Backe. Klar ist: Wir lassen uns nicht einschüchtern und freuen uns auf Unterstützung! \n' + '\n' + 'https://t.co/MQZ7ulHakF', + image: 'https://pbs.twimg.com/media/ECF8Z5KWwAIBZ6o.jpg:large', + vendor: Vendor.twitterPosting, + ) ]; for (final testCase in testCases) { @@ -44,7 +103,22 @@ void main() { expect(metadata.title, testCase.title); expect(metadata.siteName, testCase.siteName); expect(metadata.desc, testCase.desc); + expect(metadata.url, testCase.url); expect(metadata.image, testCase.image); + expect(metadata.vendor, testCase.vendor, reason: metadata.url); + if (testCase.shareAction != null) { + expect( + metadata.shareAction, + greaterThanOrEqualTo(testCase.shareAction!), + ); + } + if (testCase.shareAction != null) { + expect( + metadata.likeAction, + greaterThanOrEqualTo(testCase.likeAction!), + ); + } + expect(metadata.publishDate, testCase.publishDate); } }); } From 8c3ea92b856b6bce1027b82b233eae5be9d62db3 Mon Sep 17 00:00:00 2001 From: otsmr <git@tsmr.eu> Date: Thu, 22 Jan 2026 20:07:43 +0100 Subject: [PATCH 07/17] adds user study --- lib/src/model/json/userdata.dart | 13 + lib/src/model/json/userdata.g.dart | 13 +- lib/src/services/api.service.dart | 6 + lib/src/utils/keyvalue.dart | 31 +- lib/src/views/chats/chat_list.view.dart | 57 ++-- lib/src/views/settings/help/help.view.dart | 17 ++ .../user_study_data_collection.dart | 78 ++++++ .../user_study_questionnaire.view.dart | 265 ++++++++++++++++++ .../user_study/user_study_welcome.view.dart | 120 ++++++++ 9 files changed, 566 insertions(+), 34 deletions(-) create mode 100644 lib/src/views/user_study/user_study_data_collection.dart create mode 100644 lib/src/views/user_study/user_study_questionnaire.view.dart create mode 100644 lib/src/views/user_study/user_study_welcome.view.dart diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index eb26ef3..9945264 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -108,6 +108,19 @@ class UserData { DateTime? nextTimeToShowBackupNotice; BackupServer? backupServer; TwonlySafeBackup? twonlySafeBackup; + + // For my master thesis I want to create a anonymous user study: + // - users in the "Tester" Plan can, if they want, take part of the user study + + @JsonKey(defaultValue: false) + bool askedForUserStudyPermission = false; + + // So update data can be assigned. If set the user choose to participate. + String? userStudyParticipantsToken; + + // Once a day the anonymous data is collected and send to the server + DateTime? lastUserStudyDataUpload; + Map<String, dynamic> toJson() => _$UserDataToJson(this); } diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index a478117..3dc2fbb 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -77,7 +77,14 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData( ..twonlySafeBackup = json['twonlySafeBackup'] == null ? null : TwonlySafeBackup.fromJson( - json['twonlySafeBackup'] as Map<String, dynamic>); + json['twonlySafeBackup'] as Map<String, dynamic>) + ..askedForUserStudyPermission = + json['askedForUserStudyPermission'] as bool? ?? false + ..userStudyParticipantsToken = + json['userStudyParticipantsToken'] as String? + ..lastUserStudyDataUpload = json['lastUserStudyDataUpload'] == null + ? null + : DateTime.parse(json['lastUserStudyDataUpload'] as String); Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{ 'userId': instance.userId, @@ -122,6 +129,10 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{ instance.nextTimeToShowBackupNotice?.toIso8601String(), 'backupServer': instance.backupServer, 'twonlySafeBackup': instance.twonlySafeBackup, + 'askedForUserStudyPermission': instance.askedForUserStudyPermission, + 'userStudyParticipantsToken': instance.userStudyParticipantsToken, + 'lastUserStudyDataUpload': + instance.lastUserStudyDataUpload?.toIso8601String(), }; const _$ThemeModeEnumMap = { diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 68e7489..8ba0a5c 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -41,6 +41,7 @@ import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; +import 'package:twonly/src/views/user_study/user_study_data_collection.dart'; import 'package:web_socket_channel/io.dart'; final lockConnecting = Mutex(); @@ -100,6 +101,11 @@ class ApiService { unawaited(fetchGroupStatesForUnjoinedGroups()); unawaited(fetchMissingGroupPublicKey()); unawaited(checkForDeletedUsernames()); + + if (gUser.userStudyParticipantsToken != null) { + // In case the user participates in the user study, call the handler after authenticated, to be sure there is a internet connection + unawaited(handleUserStudyUpload()); + } } } diff --git a/lib/src/utils/keyvalue.dart b/lib/src/utils/keyvalue.dart index 05e5639..e52274d 100644 --- a/lib/src/utils/keyvalue.dart +++ b/lib/src/utils/keyvalue.dart @@ -4,38 +4,41 @@ import 'package:path_provider/path_provider.dart'; import 'package:twonly/src/utils/log.dart'; class KeyValueStore { - static Future<String> _getFilePath(String key) async { + static Future<File> _getFilePath(String key) async { final directory = await getApplicationSupportDirectory(); - return '${directory.path}/keyvalue/$key.json'; + return File('${directory.path}/keyvalue/$key.json'); + } + + static Future<void> delete(String key) async { + try { + final file = await _getFilePath(key); + if (file.existsSync()) { + file.deleteSync(); + } + } catch (e) { + Log.error('Error deleting file: $e'); + } } static Future<Map<String, dynamic>?> get(String key) async { try { - final filePath = await _getFilePath(key); - final file = File(filePath); - - // Check if the file exists + final file = await _getFilePath(key); if (file.existsSync()) { final contents = await file.readAsString(); return jsonDecode(contents) as Map<String, dynamic>; } else { - return null; // File does not exist + return null; } } catch (e) { - Log.error('Error reading file: $e'); + Log.warn('Error reading file: $e'); return null; } } static Future<void> put(String key, Map<String, dynamic> value) async { try { - final filePath = await _getFilePath(key); - final file = File(filePath); - - // Create the directory if it doesn't exist + final file = await _getFilePath(key); await file.parent.create(recursive: true); - - // Write the JSON data to the file await file.writeAsString(jsonEncode(value)); } catch (e) { Log.error('Error writing file: $e'); diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index 663a088..9bcf0ed 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -25,6 +25,7 @@ import 'package:twonly/src/views/settings/help/changelog.view.dart'; import 'package:twonly/src/views/settings/profile/profile.view.dart'; import 'package:twonly/src/views/settings/settings_main.view.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; +import 'package:twonly/src/views/user_study/user_study_welcome.view.dart'; class ChatListView extends StatefulWidget { const ChatListView({super.key}); @@ -58,31 +59,49 @@ class _ChatListViewState extends State<ChatListView> { }); }); - final changeLog = await rootBundle.loadString('CHANGELOG.md'); - final changeLogHash = - (await compute(Sha256().hash, changeLog.codeUnits)).bytes; - if (!gUser.hideChangeLog && - gUser.lastChangeLogHash.toString() != changeLogHash.toString()) { - await updateUserdata((u) { - u.lastChangeLogHash = changeLogHash; - return u; - }); - if (!mounted) return; - // only show changelog to people who already have contacts - // this prevents that this is shown directly after the user registered - if (_groupsNotPinned.isNotEmpty) { + // In case the user is already a Tester, ask him for permission. + + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (gUser.subscriptionPlan == SubscriptionPlan.Tester.name && + !gUser.askedForUserStudyPermission) { await Navigator.push( context, MaterialPageRoute( builder: (context) { - return ChangeLogView( - changeLog: changeLog, + return const UserStudyWelcomeView( + wasOpenedAutomatic: true, ); }, ), ); } - } + + final changeLog = await rootBundle.loadString('CHANGELOG.md'); + final changeLogHash = + (await compute(Sha256().hash, changeLog.codeUnits)).bytes; + if (!gUser.hideChangeLog && + gUser.lastChangeLogHash.toString() != changeLogHash.toString()) { + await updateUserdata((u) { + u.lastChangeLogHash = changeLogHash; + return u; + }); + if (!mounted) return; + // only show changelog to people who already have contacts + // this prevents that this is shown directly after the user registered + if (_groupsNotPinned.isNotEmpty) { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return ChangeLogView( + changeLog: changeLog, + ); + }, + ), + ); + } + } + }); } @override @@ -295,8 +314,8 @@ class _ChatListViewState extends State<ChatListView> { child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ - FloatingActionButton.small( - backgroundColor: context.color.primary, + IconButton.filled( + color: context.color.primary, onPressed: () { Navigator.push( context, @@ -307,7 +326,7 @@ class _ChatListViewState extends State<ChatListView> { ), ); }, - child: FaIcon( + icon: FaIcon( FontAwesomeIcons.qrcode, color: isDarkMode(context) ? Colors.black : Colors.white, ), diff --git a/lib/src/views/settings/help/help.view.dart b/lib/src/views/settings/help/help.view.dart index 5dee0ff..89b3dd2 100644 --- a/lib/src/views/settings/help/help.view.dart +++ b/lib/src/views/settings/help/help.view.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -10,6 +11,7 @@ import 'package:twonly/src/views/settings/help/contact_us.view.dart'; import 'package:twonly/src/views/settings/help/credits.view.dart'; import 'package:twonly/src/views/settings/help/diagnostics.view.dart'; import 'package:twonly/src/views/settings/help/faq.view.dart'; +import 'package:twonly/src/views/user_study/user_study_welcome.view.dart'; import 'package:url_launcher/url_launcher.dart'; class HelpView extends StatefulWidget { @@ -109,6 +111,21 @@ class _HelpViewState extends State<HelpView> { }, ), const Divider(), + if (gUser.userStudyParticipantsToken == null || kDebugMode) + ListTile( + title: const Text('Teilnahme an Nutzerstudie'), + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const UserStudyWelcomeView(); + }, + ), + ); + setState(() {}); // gUser has changed + }, + ), FutureBuilder( future: PackageInfo.fromPlatform(), builder: (context, snap) { diff --git a/lib/src/views/user_study/user_study_data_collection.dart b/lib/src/views/user_study/user_study_data_collection.dart new file mode 100644 index 0000000..f4efbe3 --- /dev/null +++ b/lib/src/views/user_study/user_study_data_collection.dart @@ -0,0 +1,78 @@ +// ignore_for_file: avoid_dynamic_calls + +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/utils/keyvalue.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/storage.dart'; + +const userStudySurveyKey = 'user_study_survey'; + +// LEASE DO NOT SPAM OR TRY SENDING DIRECTLY TO THIS URL! +// You're just making my master's thesis more difficult and destroy scientific data. :/ +const surveyUrlBase = 'https://survey.twonly.org/upload.php'; + +Future<void> handleUserStudyUpload() async { + try { + final token = gUser.userStudyParticipantsToken; + if (token == null) return; + + // in case the survey was taken offline try again + final userStudySurvey = await KeyValueStore.get(userStudySurveyKey); + if (userStudySurvey != null) { + final response = await http.post( + Uri.parse('$surveyUrlBase/create/$token'), + body: jsonEncode(userStudySurvey), + headers: {'Content-Type': 'application/json'}, + ); + if (response.statusCode != 200) { + Log.warn( + 'Got different status code for survey upload: ${response.statusCode}', + ); + return; + } + await KeyValueStore.delete(userStudySurveyKey); + } + + if (gUser.lastUserStudyDataUpload + ?.isAfter(DateTime.now().subtract(const Duration(days: 1))) ?? + false) { + // Only send updates once a day. + // This enables to see if improvements to actually work. + return; + } + + final contacts = await twonlyDB.contactsDao.getAllContacts(); + + final dataCollection = { + 'total_contacts': contacts.length, + 'accepted_contacts': contacts.where((c) => c.accepted).length, + 'verified_contacts': contacts.where((c) => c.verified).length, + }; + + final response = await http.post( + Uri.parse('$surveyUrlBase/push/$token'), + body: jsonEncode(dataCollection), + headers: {'Content-Type': 'application/json'}, + ); + if (response.statusCode == 200) { + await updateUserdata((u) { + u.lastUserStudyDataUpload = DateTime.now(); + return u; + }); + } + if (response.statusCode == 404) { + // Token is unknown to the server... + await updateUserdata((u) { + u + ..lastUserStudyDataUpload = null + ..userStudyParticipantsToken = null; + return u; + }); + } + } catch (e) { + Log.error(e); + } +} diff --git a/lib/src/views/user_study/user_study_questionnaire.view.dart b/lib/src/views/user_study/user_study_questionnaire.view.dart new file mode 100644 index 0000000..a0cb9e6 --- /dev/null +++ b/lib/src/views/user_study/user_study_questionnaire.view.dart @@ -0,0 +1,265 @@ +// ignore_for_file: avoid_dynamic_calls + +import 'package:flutter/material.dart'; +import 'package:twonly/src/utils/keyvalue.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/storage.dart'; +import 'package:twonly/src/views/user_study/user_study_data_collection.dart'; + +class UserStudyQuestionnaire extends StatefulWidget { + const UserStudyQuestionnaire({super.key}); + + @override + State<UserStudyQuestionnaire> createState() => _UserStudyQuestionnaireState(); +} + +class _UserStudyQuestionnaireState extends State<UserStudyQuestionnaire> { + final Map<String, dynamic> _responses = { + 'gender': null, + 'gender_free': '', + 'age': null, + 'education': null, + 'education_free': '', + 'vocational': null, + 'vocational_free': '', + 'enrolled': null, + 'study_program': '', + 'working': null, + 'work_field': '', + 'smartphone_2years': null, + 'comp_knowledge': null, + 'security_knowledge': null, + 'messengers': [], + }; + + final List<String> _messengerOptions = [ + 'WhatsApp', + 'Signal', + 'Telegram', + 'Facebook Messenger', + 'iMessage', + 'Teams', + 'Viber', + 'Element', + 'Andere', + ]; + + Future<void> _submitData() async { + await KeyValueStore.put(userStudySurveyKey, _responses); + + await updateUserdata((u) { + // generate a random participants id to identify data send later while keeping the user anonym + u.userStudyParticipantsToken = getRandomString(25); + return u; + }); + + await handleUserStudyUpload(); + + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Vielen Dank für deine Teilnahme!')), + ); + + Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Befragung')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionTitle('Demografische Daten'), + _questionText('Was ist dein Geschlecht?'), + _buildRadioList( + ['Männlich', 'Weiblich', 'Divers', 'Keine Angabe'], + 'gender', + ), + _buildTextField( + 'Freitext (optional)', + (val) => _responses['gender_free'] = val, + ), + _questionText('Wie alt bist du?'), + _buildRadioList( + [ + '18-22', + '23-27', + '28-32', + '33-37', + '38-42', + '43-47', + '48-52', + '53-57', + '58-62', + '63-67', + '68-72', + '73-77', + '77 oder älter', + 'Keine Angabe', + ], + 'age', + ), + _questionText('Was ist dein höchster Schulabschluss?'), + _buildRadioList( + [ + 'Noch in der Schule', + 'Hauptschulabschluss', + 'POS (Polytechnische Oberschule)', + 'Realschulabschluss', + 'Abitur / Hochschulreife', + 'Kein Abschluss', + 'Keine Angabe', + ], + 'education', + ), + _buildTextField( + 'Freitext (optional)', + (val) => _responses['education_free'] = val, + ), + _questionText('Was ist dein höchster beruflicher Abschluss?'), + _buildRadioList( + [ + 'Berufsausbildung / Duales System', + 'Fachschulabschluss', + 'Fachschulabschluss (ehem. DDR)', + 'Bachelor', + 'Master', + 'Diplom', + 'Promotion (PhD)', + 'Kein beruflicher Abschluss', + 'Keine Angabe', + ], + 'vocational', + ), + _buildTextField( + 'Freitext (optional)', + (val) => _responses['vocational_free'] = val, + ), + _questionText( + 'Bist du derzeit in einem Studiengang eingeschrieben? (Bachelor, Master, Diplom, Staatsexamen, Magister)', + ), + _buildRadioList(['Ja', 'Nein', 'Keine Angabe'], 'enrolled'), + _questionText('Wenn ja, welcher Studiengang?'), + _buildTextField( + 'Studiengang eingeben', + (val) => _responses['study_program'] = val, + ), + _questionText('Bist du derzeit berufstätig?'), + _buildRadioList(['Ja', 'Nein', 'Keine Angabe'], 'working'), + _questionText('Wenn ja, in welchem Bereich arbeiten Sie?'), + _buildTextField( + 'Arbeitsbereich eingeben', + (val) => _responses['work_field'] = val, + ), + const SizedBox(height: 30), + // const Divider(), + _sectionTitle('Technisches Wissen'), + _questionText( + 'Nutzt du seit mehr als zwei Jahren ein Smartphone?', + ), + _buildRadioList(['Ja', 'Nein'], 'smartphone_2years'), + _questionText( + 'Wie schätzt du deine allgemeinen Computerkenntnisse ein?', + ), + _buildRadioList( + ['Anfänger', 'Mittel', 'Fortgeschritten'], + 'comp_knowledge', + ), + _questionText( + 'Wie schätzt du dein Wissen im Bereich IT-Sicherheit ein?', + ), + _buildRadioList( + ['Anfänger', 'Mittel', 'Fortgeschritten'], + 'security_knowledge', + ), + _questionText( + 'Welche der folgenden Messenger hast du schon einmal benutzt?', + ), + ..._messengerOptions.map( + (m) => CheckboxListTile( + title: Text(m), + visualDensity: const VisualDensity(horizontal: 0, vertical: -4), + value: (_responses['messengers'] as List<dynamic>).contains(m), + onChanged: (bool? value) { + setState(() { + value! + ? _responses['messengers'].add(m) + : _responses['messengers'].remove(m); + }); + }, + ), + ), + const SizedBox(height: 30), + Center( + child: FilledButton( + onPressed: _submitData, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15), + child: + Text('Jetzt teilnehmen', style: TextStyle(fontSize: 18)), + ), + ), + ), + const SizedBox(height: 50), + ], + ), + ), + ); + } + + // Hilfsmethoden für das UI + Widget _sectionTitle(String title) => Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Text( + title, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ); + + Widget _questionText(String text) => Padding( + padding: const EdgeInsets.only(top: 20, bottom: 5), + child: Text( + text, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + ); + + Widget _buildRadioList(List<String> options, String key) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: DropdownButtonFormField<String>( + decoration: const InputDecoration( + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + labelText: 'Bitte wählen...', + ), + initialValue: _responses[key] as String?, + items: options.map((String value) { + return DropdownMenuItem<String>( + value: value, + child: Text(value), + ); + }).toList(), + onChanged: (val) { + setState(() { + _responses[key] = val; + }); + }, + ), + ); + } + + Widget _buildTextField(String hint, void Function(String) onChanged) { + return TextField( + decoration: + InputDecoration(hintText: hint, border: const OutlineInputBorder()), + onChanged: onChanged, + ); + } +} diff --git a/lib/src/views/user_study/user_study_welcome.view.dart b/lib/src/views/user_study/user_study_welcome.view.dart new file mode 100644 index 0000000..ed8adb1 --- /dev/null +++ b/lib/src/views/user_study/user_study_welcome.view.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/utils/storage.dart'; +import 'package:twonly/src/views/user_study/user_study_questionnaire.view.dart'; + +class UserStudyWelcomeView extends StatefulWidget { + const UserStudyWelcomeView({super.key, this.wasOpenedAutomatic = false}); + + final bool wasOpenedAutomatic; + + @override + State<UserStudyWelcomeView> createState() => _UserStudyWelcomeViewState(); +} + +class _UserStudyWelcomeViewState extends State<UserStudyWelcomeView> { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Teilnahme an Nutzerstudie'), + ), + body: Padding( + padding: const EdgeInsets.all(12), + child: ListView( + children: [ + const SizedBox(height: 30), + const Text( + 'Es dauert nur ein paar Minuten.', + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ), + const SizedBox(height: 20), + const Text( + 'Im Rahmen meiner Masterarbeit möchte ich die Benutzerfreundlichkeit von anonymen und dezentralen Messenger-Diensten verbessern.', + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + const Text( + 'Zu diesem Zweck werden in den nächsten Monaten verschiedene Änderungen an der App vorgenommen. Um die Wirksamkeit der Änderungen zu messen, möchte ich einige Daten über deine Nutzung der App sammeln sowie eine kurze Befragung durchführen.', + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + const Text( + 'Die Daten bestehen ausschließlich aus Zahlen, z. B. Anzahl deiner Kontakte. Alle Daten werden anonym übermittelt und können nicht mit deinem Benutzerkonto verknüpft werden.', + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + const Text( + 'Die Masterarbeit und damit die Nutzerstudie wird bis September durchgeführt. Nach Abschluss erhältst du eine Benachrichtigung und wirst über die Ergebnisse informiert.', + textAlign: TextAlign.center, + ), + const SizedBox(height: 40), + Center( + child: FilledButton( + onPressed: () { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) { + return const UserStudyQuestionnaire(); + }, + ), + ); + }, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15), + child: Text( + 'Weiter zur Befragung', + style: TextStyle(fontSize: 18), + ), + ), + ), + ), + const SizedBox(height: 10), + if (widget.wasOpenedAutomatic) + Center( + child: OutlinedButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Text( + 'Frag mich später noch mal', + style: TextStyle(fontSize: 18), + ), + ), + ), + ), + const SizedBox(height: 10), + if (widget.wasOpenedAutomatic) + Center( + child: GestureDetector( + onTap: () async { + await updateUserdata((u) { + u.askedForUserStudyPermission = true; + return u; + }); + if (context.mounted) Navigator.pop(context); + }, + child: const Text( + 'Nicht mehr anzeigen', + style: TextStyle(fontSize: 12), + ), + ), + ), + const SizedBox(height: 10), + const Text( + 'PS: twonly ist Open Source, wenn du also genau wissen willst, welche Daten übertragen werden, schau dir einfach die Datei "lib/src/views/user_study/user_study_data_collection.dart" im Repository an :).', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 10), + ), + ], + ), + ), + ); + } +} From 4c06bcc751db820eef0cfa11807067de78ae487e Mon Sep 17 00:00:00 2001 From: otsmr <git@tsmr.eu> Date: Thu, 22 Jan 2026 20:14:05 +0100 Subject: [PATCH 08/17] fix analyzer --- lib/src/services/intent/links.intent.dart | 7 +++++-- .../camera_preview_components/camera_preview.dart | 1 - .../camera/share_image_editor/layer_data.dart | 2 -- .../layers/link_preview/parser/json_ld.parser.dart | 5 ++++- .../layers/link_preview/parser/youtube.parser.dart | 14 ++++++++------ .../message_context_menu.dart | 2 +- .../reaction_buttons.component.dart | 3 ++- .../user_study/user_study_data_collection.dart | 2 -- .../user_study/user_study_questionnaire.view.dart | 6 +++--- test/features/link_parser_test.dart | 2 +- 10 files changed, 24 insertions(+), 20 deletions(-) diff --git a/lib/src/services/intent/links.intent.dart b/lib/src/services/intent/links.intent.dart index 305ae93..cf4e7fa 100644 --- a/lib/src/services/intent/links.intent.dart +++ b/lib/src/services/intent/links.intent.dart @@ -148,8 +148,11 @@ Future<void> handleIntentMediaFile( ); } -Future<void> handleIntentSharedFile(BuildContext context, - List<SharedFile> files, void Function(Uri) onUrlCallBack) async { +Future<void> handleIntentSharedFile( + BuildContext context, + List<SharedFile> files, + void Function(Uri) onUrlCallBack, +) async { for (final file in files) { if (file.value == null) { Log.error( diff --git a/lib/src/views/camera/camera_preview_components/camera_preview.dart b/lib/src/views/camera/camera_preview_components/camera_preview.dart index 991748d..10783db 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview.dart @@ -82,7 +82,6 @@ class MainCameraPreview extends StatelessWidget { decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( - width: 1, color: Colors.white.withAlpha(150), ), ), diff --git a/lib/src/views/camera/share_image_editor/layer_data.dart b/lib/src/views/camera/share_image_editor/layer_data.dart index d28b967..504df26 100755 --- a/lib/src/views/camera/share_image_editor/layer_data.dart +++ b/lib/src/views/camera/share_image_editor/layer_data.dart @@ -1,5 +1,3 @@ -// ignore_for_file: comment_references - import 'package:flutter/material.dart'; import 'package:hand_signature/signature.dart'; import 'package:twonly/src/views/camera/share_image_editor/image_item.dart'; diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld.parser.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld.parser.dart index e946065..beb8ef3 100644 --- a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld.parser.dart +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld.parser.dart @@ -91,7 +91,10 @@ class JsonLdParser with BaseMetaInfo { String? get siteName => OpenGraphParser(document).siteName; String? _imgResultToStr(dynamic result) { - if (result is List && result.isNotEmpty) result = result.first; + if (result is List && result.isNotEmpty) { + final tmp = result.first; + if (tmp is String) return tmp; + } if (result is String) return result; return null; } diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/youtube.parser.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/youtube.parser.dart index 30e976a..85ef9e2 100644 --- a/lib/src/views/camera/share_image_editor/layers/link_preview/parser/youtube.parser.dart +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/parser/youtube.parser.dart @@ -4,12 +4,11 @@ import 'base.dart'; import 'util.dart'; class YoutubeParser with BaseMetaInfo { - YoutubeParser(this.document, this.url) { + YoutubeParser(this.document, this._url) { _jsonData = _parseToJson(document); } - @override - String url; + final String _url; Document? document; dynamic _jsonData; @@ -44,7 +43,7 @@ class YoutubeParser with BaseMetaInfo { @override String? get image { final data = _jsonData; - if (data is List && data.isNotEmpty) { + if (data is List<Map<String, dynamic>> && data.isNotEmpty) { return _imgResultToStr(data.first['thumbnail_url']); } else if (data is Map) { return _imgResultToStr(data.getDynamic('thumbnail_url')); @@ -64,12 +63,15 @@ class YoutubeParser with BaseMetaInfo { } @override - Vendor? get vendor => (Uri.parse(url).host.contains('youtube.com')) + Vendor? get vendor => (Uri.parse(_url).host.contains('youtube.com')) ? Vendor.youtubeVideo : null; String? _imgResultToStr(dynamic result) { - if (result is List && result.isNotEmpty) result = result.first; + if (result is List && result.isNotEmpty) { + final tmp = result.first; + if (tmp is String) return tmp; + } if (result is String) return result; return null; } diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 44af049..eb868e4 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -15,10 +15,10 @@ import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; -import 'package:twonly/src/views/components/emoji_picker.bottom.dart'; import 'package:twonly/src/views/chats/message_info.view.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/context_menu.component.dart'; +import 'package:twonly/src/views/components/emoji_picker.bottom.dart'; import 'package:twonly/src/views/memories/memories_photo_slider.view.dart'; class MessageContextMenu extends StatelessWidget { diff --git a/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart b/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart index 53b12d5..108beac 100644 --- a/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart +++ b/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart @@ -1,14 +1,15 @@ import 'dart:async'; import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; -import 'package:twonly/src/views/components/emoji_picker.bottom.dart'; import 'package:twonly/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; +import 'package:twonly/src/views/components/emoji_picker.bottom.dart'; class ReactionButtons extends StatefulWidget { const ReactionButtons({ diff --git a/lib/src/views/user_study/user_study_data_collection.dart b/lib/src/views/user_study/user_study_data_collection.dart index f4efbe3..c38f213 100644 --- a/lib/src/views/user_study/user_study_data_collection.dart +++ b/lib/src/views/user_study/user_study_data_collection.dart @@ -1,5 +1,3 @@ -// ignore_for_file: avoid_dynamic_calls - import 'dart:convert'; import 'package:http/http.dart' as http; diff --git a/lib/src/views/user_study/user_study_questionnaire.view.dart b/lib/src/views/user_study/user_study_questionnaire.view.dart index a0cb9e6..bfdf4d1 100644 --- a/lib/src/views/user_study/user_study_questionnaire.view.dart +++ b/lib/src/views/user_study/user_study_questionnaire.view.dart @@ -68,7 +68,7 @@ class _UserStudyQuestionnaireState extends State<UserStudyQuestionnaire> { return Scaffold( appBar: AppBar(title: const Text('Befragung')), body: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -181,7 +181,7 @@ class _UserStudyQuestionnaireState extends State<UserStudyQuestionnaire> { ..._messengerOptions.map( (m) => CheckboxListTile( title: Text(m), - visualDensity: const VisualDensity(horizontal: 0, vertical: -4), + visualDensity: const VisualDensity(vertical: -4), value: (_responses['messengers'] as List<dynamic>).contains(m), onChanged: (bool? value) { setState(() { @@ -232,7 +232,7 @@ class _UserStudyQuestionnaireState extends State<UserStudyQuestionnaire> { Widget _buildRadioList(List<String> options, String key) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), + padding: const EdgeInsets.symmetric(vertical: 8), child: DropdownButtonFormField<String>( decoration: const InputDecoration( border: OutlineInputBorder(), diff --git a/test/features/link_parser_test.dart b/test/features/link_parser_test.dart index 3361df6..53f1c9f 100644 --- a/test/features/link_parser_test.dart +++ b/test/features/link_parser_test.dart @@ -95,7 +95,7 @@ void main() { 'https://t.co/MQZ7ulHakF', image: 'https://pbs.twimg.com/media/ECF8Z5KWwAIBZ6o.jpg:large', vendor: Vendor.twitterPosting, - ) + ), ]; for (final testCase in testCases) { From b48df1baa5728673e8b635ef67cd82c10bcd7d3f Mon Sep 17 00:00:00 2001 From: otsmr <git@tsmr.eu> Date: Thu, 22 Jan 2026 23:26:23 +0100 Subject: [PATCH 09/17] fix #366 --- .../schemas/twonly_db/drift_schema_v7.json | 1 + lib/src/database/tables/messages.table.dart | 6 +- lib/src/database/twonly.db.dart | 78 +- lib/src/database/twonly.db.g.dart | 447 +- lib/src/database/twonly.db.steps.dart | 434 ++ lib/src/model/protobuf/client/data.proto | 11 + .../protobuf/client/generated/data.pb.dart | 99 + .../client/generated/data.pbenum.dart | 35 + .../client/generated/data.pbjson.dart | 49 + .../client/generated/messages.pb.dart | 15 + .../client/generated/messages.pbjson.dart | 66 +- lib/src/model/protobuf/client/messages.proto | 2 + .../services/api/client2client/media.c2c.dart | 5 + .../api/mediafiles/upload.service.dart | 9 +- lib/src/services/api/messages.dart | 21 +- .../camera_preview_controller_view.dart | 8 +- .../share_image_contact_selection.view.dart | 4 + .../views/camera/share_image_editor.view.dart | 15 + .../camera/share_image_editor/layer_data.dart | 3 + .../layers/link_preview.layer.dart | 50 +- .../link_preview/cards/custom.card.dart | 87 + .../link_preview/cards/mastodon.card.dart | 110 + .../link_preview/cards/twitter.card.dart | 101 + .../link_preview/cards/youtube.card.dart | 90 + .../entries/friendly_message_time.comp.dart | 2 +- lib/src/views/chats/media_viewer.view.dart | 48 +- .../additional_message_content.dart | 54 + scripts/generate_proto.sh | 1 + test/drift/twonly_db/generated/schema.dart | 5 +- test/drift/twonly_db/generated/schema_v7.dart | 6582 +++++++++++++++++ test/features/link_parser_test.dart | 7 + 31 files changed, 8313 insertions(+), 132 deletions(-) create mode 100644 lib/src/database/schemas/twonly_db/drift_schema_v7.json create mode 100644 lib/src/model/protobuf/client/data.proto create mode 100644 lib/src/model/protobuf/client/generated/data.pb.dart create mode 100644 lib/src/model/protobuf/client/generated/data.pbenum.dart create mode 100644 lib/src/model/protobuf/client/generated/data.pbjson.dart create mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview/cards/custom.card.dart create mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart create mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview/cards/twitter.card.dart create mode 100644 lib/src/views/camera/share_image_editor/layers/link_preview/cards/youtube.card.dart create mode 100644 lib/src/views/chats/media_viewer_components/additional_message_content.dart create mode 100644 test/drift/twonly_db/generated/schema_v7.dart diff --git a/lib/src/database/schemas/twonly_db/drift_schema_v7.json b/lib/src/database/schemas/twonly_db/drift_schema_v7.json new file mode 100644 index 0000000..f77953a --- /dev/null +++ b/lib/src/database/schemas/twonly_db/drift_schema_v7.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"contacts","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"username","getter_name":"username","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"display_name","getter_name":"displayName","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"nick_name","getter_name":"nickName","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"avatar_svg_compressed","getter_name":"avatarSvgCompressed","moor_type":"blob","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"sender_profile_counter","getter_name":"senderProfileCounter","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"accepted","getter_name":"accepted","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"accepted\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"accepted\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_by_user","getter_name":"deletedByUser","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"deleted_by_user\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"deleted_by_user\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"requested","getter_name":"requested","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"requested\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"requested\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"blocked","getter_name":"blocked","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"blocked\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"blocked\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"verified","getter_name":"verified","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"verified\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"verified\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"account_deleted","getter_name":"accountDeleted","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"account_deleted\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"account_deleted\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["user_id"]}},{"id":1,"references":[],"type":"table","data":{"name":"groups","was_declared_in_moor":false,"columns":[{"name":"group_id","getter_name":"groupId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_group_admin","getter_name":"isGroupAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_group_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_group_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"is_direct_chat","getter_name":"isDirectChat","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_direct_chat\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_direct_chat\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"pinned","getter_name":"pinned","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"pinned\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"pinned\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"archived","getter_name":"archived","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"archived\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"archived\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"joined_group","getter_name":"joinedGroup","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"joined_group\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"joined_group\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"left_group","getter_name":"leftGroup","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"left_group\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"left_group\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_content","getter_name":"deletedContent","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"deleted_content\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"deleted_content\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"state_version_id","getter_name":"stateVersionId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"state_encryption_key","getter_name":"stateEncryptionKey","moor_type":"blob","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"my_group_private_key","getter_name":"myGroupPrivateKey","moor_type":"blob","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"group_name","getter_name":"groupName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"draft_message","getter_name":"draftMessage","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"total_media_counter","getter_name":"totalMediaCounter","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"also_best_friend","getter_name":"alsoBestFriend","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"also_best_friend\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"also_best_friend\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"delete_messages_after_milliseconds","getter_name":"deleteMessagesAfterMilliseconds","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('86400000')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]},{"name":"last_message_send","getter_name":"lastMessageSend","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_message_received","getter_name":"lastMessageReceived","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_flame_counter_change","getter_name":"lastFlameCounterChange","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_flame_sync","getter_name":"lastFlameSync","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"flame_counter","getter_name":"flameCounter","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"max_flame_counter","getter_name":"maxFlameCounter","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"max_flame_counter_from","getter_name":"maxFlameCounterFrom","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_message_exchange","getter_name":"lastMessageExchange","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["group_id"]}},{"id":2,"references":[],"type":"table","data":{"name":"media_files","was_declared_in_moor":false,"columns":[{"name":"media_id","getter_name":"mediaId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter<MediaType>(MediaType.values)","dart_type_name":"MediaType"}},{"name":"upload_state","getter_name":"uploadState","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter<UploadState>(UploadState.values)","dart_type_name":"UploadState"}},{"name":"download_state","getter_name":"downloadState","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter<DownloadState>(DownloadState.values)","dart_type_name":"DownloadState"}},{"name":"requires_authentication","getter_name":"requiresAuthentication","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"requires_authentication\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"requires_authentication\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"stored","getter_name":"stored","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"stored\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"stored\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"is_draft_media","getter_name":"isDraftMedia","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_draft_media\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_draft_media\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"reupload_requested_by","getter_name":"reuploadRequestedBy","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"IntListTypeConverter()","dart_type_name":"List<int>"}},{"name":"display_limit_in_milliseconds","getter_name":"displayLimitInMilliseconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"remove_audio","getter_name":"removeAudio","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"remove_audio\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"remove_audio\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"download_token","getter_name":"downloadToken","moor_type":"blob","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"encryption_key","getter_name":"encryptionKey","moor_type":"blob","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"encryption_mac","getter_name":"encryptionMac","moor_type":"blob","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"encryption_nonce","getter_name":"encryptionNonce","moor_type":"blob","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"stored_file_hash","getter_name":"storedFileHash","moor_type":"blob","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["media_id"]}},{"id":3,"references":[1,0,2],"type":"table","data":{"name":"messages","was_declared_in_moor":false,"columns":[{"name":"group_id","getter_name":"groupId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES \"groups\" (group_id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES \"groups\" (group_id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"groups","column":"group_id"},"initially_deferred":false,"on_update":null,"on_delete":"cascade"}}]},{"name":"message_id","getter_name":"messageId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"sender_id","getter_name":"senderId","moor_type":"int","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES contacts (user_id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES contacts (user_id)"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"contacts","column":"user_id"},"initially_deferred":false,"on_update":null,"on_delete":null}}]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter<MessageType>(MessageType.values)","dart_type_name":"MessageType"}},{"name":"content","getter_name":"content","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"media_id","getter_name":"mediaId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES media_files (media_id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES media_files (media_id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"media_files","column":"media_id"},"initially_deferred":false,"on_update":null,"on_delete":"setNull"}}]},{"name":"additional_message_data","getter_name":"additionalMessageData","moor_type":"blob","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"media_stored","getter_name":"mediaStored","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"media_stored\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"media_stored\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"media_reopened","getter_name":"mediaReopened","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"media_reopened\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"media_reopened\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"download_token","getter_name":"downloadToken","moor_type":"blob","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quotes_message_id","getter_name":"quotesMessageId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_deleted_from_sender","getter_name":"isDeletedFromSender","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_deleted_from_sender\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_deleted_from_sender\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"opened_at","getter_name":"openedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"opened_by_all","getter_name":"openedByAll","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]},{"name":"modified_at","getter_name":"modifiedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"ack_by_user","getter_name":"ackByUser","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"ack_by_server","getter_name":"ackByServer","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["message_id"]}},{"id":4,"references":[3],"type":"table","data":{"name":"message_histories","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"message_id","getter_name":"messageId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES messages (message_id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES messages (message_id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"messages","column":"message_id"},"initially_deferred":false,"on_update":null,"on_delete":"cascade"}}]},{"name":"contact_id","getter_name":"contactId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":5,"references":[3,0],"type":"table","data":{"name":"reactions","was_declared_in_moor":false,"columns":[{"name":"message_id","getter_name":"messageId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES messages (message_id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES messages (message_id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"messages","column":"message_id"},"initially_deferred":false,"on_update":null,"on_delete":"cascade"}}]},{"name":"emoji","getter_name":"emoji","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"sender_id","getter_name":"senderId","moor_type":"int","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES contacts (user_id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES contacts (user_id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"contacts","column":"user_id"},"initially_deferred":false,"on_update":null,"on_delete":"cascade"}}]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["message_id","sender_id","emoji"]}},{"id":6,"references":[1,0],"type":"table","data":{"name":"group_members","was_declared_in_moor":false,"columns":[{"name":"group_id","getter_name":"groupId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES \"groups\" (group_id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES \"groups\" (group_id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"groups","column":"group_id"},"initially_deferred":false,"on_update":null,"on_delete":"cascade"}}]},{"name":"contact_id","getter_name":"contactId","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES contacts (user_id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES contacts (user_id)"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"contacts","column":"user_id"},"initially_deferred":false,"on_update":null,"on_delete":null}}]},{"name":"member_state","getter_name":"memberState","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter<MemberState>(MemberState.values)","dart_type_name":"MemberState"}},{"name":"group_public_key","getter_name":"groupPublicKey","moor_type":"blob","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_message","getter_name":"lastMessage","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["group_id","contact_id"]}},{"id":7,"references":[0,3],"type":"table","data":{"name":"receipts","was_declared_in_moor":false,"columns":[{"name":"receipt_id","getter_name":"receiptId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"contact_id","getter_name":"contactId","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES contacts (user_id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES contacts (user_id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"contacts","column":"user_id"},"initially_deferred":false,"on_update":null,"on_delete":"cascade"}}]},{"name":"message_id","getter_name":"messageId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES messages (message_id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES messages (message_id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"messages","column":"message_id"},"initially_deferred":false,"on_update":null,"on_delete":"cascade"}}]},{"name":"message","getter_name":"message","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"contact_will_sends_receipt","getter_name":"contactWillSendsReceipt","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"contact_will_sends_receipt\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"contact_will_sends_receipt\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"mark_for_retry","getter_name":"markForRetry","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"mark_for_retry_after_accepted","getter_name":"markForRetryAfterAccepted","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"ack_by_server_at","getter_name":"ackByServerAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"retry_count","getter_name":"retryCount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"last_retry","getter_name":"lastRetry","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["receipt_id"]}},{"id":8,"references":[],"type":"table","data":{"name":"received_receipts","was_declared_in_moor":false,"columns":[{"name":"receipt_id","getter_name":"receiptId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["receipt_id"]}},{"id":9,"references":[],"type":"table","data":{"name":"signal_identity_key_stores","was_declared_in_moor":false,"columns":[{"name":"device_id","getter_name":"deviceId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"identity_key","getter_name":"identityKey","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["device_id","name"]}},{"id":10,"references":[],"type":"table","data":{"name":"signal_pre_key_stores","was_declared_in_moor":false,"columns":[{"name":"pre_key_id","getter_name":"preKeyId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"pre_key","getter_name":"preKey","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["pre_key_id"]}},{"id":11,"references":[],"type":"table","data":{"name":"signal_sender_key_stores","was_declared_in_moor":false,"columns":[{"name":"sender_key_name","getter_name":"senderKeyName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"sender_key","getter_name":"senderKey","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["sender_key_name"]}},{"id":12,"references":[],"type":"table","data":{"name":"signal_session_stores","was_declared_in_moor":false,"columns":[{"name":"device_id","getter_name":"deviceId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"session_record","getter_name":"sessionRecord","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["device_id","name"]}},{"id":13,"references":[0],"type":"table","data":{"name":"signal_contact_pre_keys","was_declared_in_moor":false,"columns":[{"name":"contact_id","getter_name":"contactId","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES contacts (user_id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES contacts (user_id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"contacts","column":"user_id"},"initially_deferred":false,"on_update":null,"on_delete":"cascade"}}]},{"name":"pre_key_id","getter_name":"preKeyId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"pre_key","getter_name":"preKey","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["contact_id","pre_key_id"]}},{"id":14,"references":[0],"type":"table","data":{"name":"signal_contact_signed_pre_keys","was_declared_in_moor":false,"columns":[{"name":"contact_id","getter_name":"contactId","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES contacts (user_id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES contacts (user_id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"contacts","column":"user_id"},"initially_deferred":false,"on_update":null,"on_delete":"cascade"}}]},{"name":"signed_pre_key_id","getter_name":"signedPreKeyId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"signed_pre_key","getter_name":"signedPreKey","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"signed_pre_key_signature","getter_name":"signedPreKeySignature","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["contact_id"]}},{"id":15,"references":[3],"type":"table","data":{"name":"message_actions","was_declared_in_moor":false,"columns":[{"name":"message_id","getter_name":"messageId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES messages (message_id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES messages (message_id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"messages","column":"message_id"},"initially_deferred":false,"on_update":null,"on_delete":"cascade"}}]},{"name":"contact_id","getter_name":"contactId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter<MessageActionType>(MessageActionType.values)","dart_type_name":"MessageActionType"}},{"name":"action_at","getter_name":"actionAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["message_id","contact_id","type"]}},{"id":16,"references":[1,0],"type":"table","data":{"name":"group_histories","was_declared_in_moor":false,"columns":[{"name":"group_history_id","getter_name":"groupHistoryId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"group_id","getter_name":"groupId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES \"groups\" (group_id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES \"groups\" (group_id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"groups","column":"group_id"},"initially_deferred":false,"on_update":null,"on_delete":"cascade"}}]},{"name":"contact_id","getter_name":"contactId","moor_type":"int","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES contacts (user_id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES contacts (user_id)"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"contacts","column":"user_id"},"initially_deferred":false,"on_update":null,"on_delete":null}}]},{"name":"affected_contact_id","getter_name":"affectedContactId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"old_group_name","getter_name":"oldGroupName","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"new_group_name","getter_name":"newGroupName","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"new_delete_messages_after_milliseconds","getter_name":"newDeleteMessagesAfterMilliseconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter<GroupActionType>(GroupActionType.values)","dart_type_name":"GroupActionType"}},{"name":"action_at","getter_name":"actionAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["group_history_id"]}}]} \ No newline at end of file diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index 6739432..89e2975 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -22,6 +22,8 @@ class Messages extends Table { .nullable() .references(MediaFiles, #mediaId, onDelete: KeyAction.setNull)(); + BlobColumn get additionalMessageData => blob().nullable()(); + BoolColumn get mediaStored => boolean().withDefault(const Constant(false))(); BoolColumn get mediaReopened => boolean().withDefault(const Constant(false))(); @@ -56,7 +58,7 @@ class MessageActions extends Table { text().references(Messages, #messageId, onDelete: KeyAction.cascade)(); IntColumn get contactId => - integer().references(Contacts, #contactId, onDelete: KeyAction.cascade)(); + integer().references(Contacts, #userId, onDelete: KeyAction.cascade)(); TextColumn get type => textEnum<MessageActionType>()(); @@ -75,7 +77,7 @@ class MessageHistories extends Table { IntColumn get contactId => integer() .nullable() - .references(Contacts, #contactId, onDelete: KeyAction.cascade)(); + .references(Contacts, #userId, onDelete: KeyAction.cascade)(); TextColumn get content => text().nullable()(); diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index 3cace23..07ec778 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -68,7 +68,7 @@ class TwonlyDB extends _$TwonlyDB { TwonlyDB.forTesting(DatabaseConnection super.connection); @override - int get schemaVersion => 6; + int get schemaVersion => 7; static QueryExecutor _openConnection() { return driftDatabase( @@ -85,39 +85,49 @@ class TwonlyDB extends _$TwonlyDB { beforeOpen: (details) async { await customStatement('PRAGMA foreign_keys = ON'); }, - onUpgrade: stepByStep( - from1To2: (m, schema) async { - await m.addColumn(schema.messages, schema.messages.mediaReopened); - await m.dropColumn(schema.mediaFiles, 'reopen_by_contact'); - }, - from2To3: (m, schema) async { - await m.addColumn(schema.groups, schema.groups.draftMessage); - }, - from3To4: (m, schema) async { - await m.alterTable( - TableMigration( - schema.groupHistories, - columnTransformer: { - schema.groupHistories.affectedContactId: - schema.groupHistories.affectedContactId, - }, - ), - ); - }, - from4To5: (m, schema) async { - await m.addColumn(schema.receipts, schema.receipts.markForRetry); - await m.addColumn( - schema.mediaFiles, - schema.mediaFiles.storedFileHash, - ); - }, - from5To6: (m, schema) async { - await m.addColumn( - schema.receipts, - schema.receipts.markForRetryAfterAccepted, - ); - }, - ), + onUpgrade: (m, from, to) async { + // disable foreign_keys before migrations + await customStatement('PRAGMA foreign_keys = OFF'); + return stepByStep( + from1To2: (m, schema) async { + await m.addColumn(schema.messages, schema.messages.mediaReopened); + await m.dropColumn(schema.mediaFiles, 'reopen_by_contact'); + }, + from2To3: (m, schema) async { + await m.addColumn(schema.groups, schema.groups.draftMessage); + }, + from3To4: (m, schema) async { + await m.alterTable( + TableMigration( + schema.groupHistories, + columnTransformer: { + schema.groupHistories.affectedContactId: + schema.groupHistories.affectedContactId, + }, + ), + ); + }, + from4To5: (m, schema) async { + await m.addColumn(schema.receipts, schema.receipts.markForRetry); + await m.addColumn( + schema.mediaFiles, + schema.mediaFiles.storedFileHash, + ); + }, + from5To6: (m, schema) async { + await m.addColumn( + schema.receipts, + schema.receipts.markForRetryAfterAccepted, + ); + }, + from6To7: (m, schema) async { + await m.addColumn( + schema.messages, + schema.messages.additionalMessageData, + ); + }, + )(m, from, to); + }, ); } diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 0ae2daf..bad66a1 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -2796,6 +2796,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( 'REFERENCES media_files (media_id) ON DELETE SET NULL')); + static const VerificationMeta _additionalMessageDataMeta = + const VerificationMeta('additionalMessageData'); + @override + late final GeneratedColumn<Uint8List> additionalMessageData = + GeneratedColumn<Uint8List>('additional_message_data', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); static const VerificationMeta _mediaStoredMeta = const VerificationMeta('mediaStored'); @override @@ -2884,6 +2890,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { type, content, mediaId, + additionalMessageData, mediaStored, mediaReopened, downloadToken, @@ -2930,6 +2937,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { context.handle(_mediaIdMeta, mediaId.isAcceptableOrUnknown(data['media_id']!, _mediaIdMeta)); } + if (data.containsKey('additional_message_data')) { + context.handle( + _additionalMessageDataMeta, + additionalMessageData.isAcceptableOrUnknown( + data['additional_message_data']!, _additionalMessageDataMeta)); + } if (data.containsKey('media_stored')) { context.handle( _mediaStoredMeta, @@ -3013,6 +3026,8 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { .read(DriftSqlType.string, data['${effectivePrefix}content']), mediaId: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}media_id']), + additionalMessageData: attachedDatabase.typeMapping.read( + DriftSqlType.blob, data['${effectivePrefix}additional_message_data']), mediaStored: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}media_stored'])!, mediaReopened: attachedDatabase.typeMapping @@ -3054,6 +3069,7 @@ class Message extends DataClass implements Insertable<Message> { final MessageType type; final String? content; final String? mediaId; + final Uint8List? additionalMessageData; final bool mediaStored; final bool mediaReopened; final Uint8List? downloadToken; @@ -3072,6 +3088,7 @@ class Message extends DataClass implements Insertable<Message> { required this.type, this.content, this.mediaId, + this.additionalMessageData, required this.mediaStored, required this.mediaReopened, this.downloadToken, @@ -3100,6 +3117,10 @@ class Message extends DataClass implements Insertable<Message> { if (!nullToAbsent || mediaId != null) { map['media_id'] = Variable<String>(mediaId); } + if (!nullToAbsent || additionalMessageData != null) { + map['additional_message_data'] = + Variable<Uint8List>(additionalMessageData); + } map['media_stored'] = Variable<bool>(mediaStored); map['media_reopened'] = Variable<bool>(mediaReopened); if (!nullToAbsent || downloadToken != null) { @@ -3142,6 +3163,9 @@ class Message extends DataClass implements Insertable<Message> { mediaId: mediaId == null && nullToAbsent ? const Value.absent() : Value(mediaId), + additionalMessageData: additionalMessageData == null && nullToAbsent + ? const Value.absent() + : Value(additionalMessageData), mediaStored: Value(mediaStored), mediaReopened: Value(mediaReopened), downloadToken: downloadToken == null && nullToAbsent @@ -3181,6 +3205,8 @@ class Message extends DataClass implements Insertable<Message> { .fromJson(serializer.fromJson<String>(json['type'])), content: serializer.fromJson<String?>(json['content']), mediaId: serializer.fromJson<String?>(json['mediaId']), + additionalMessageData: + serializer.fromJson<Uint8List?>(json['additionalMessageData']), mediaStored: serializer.fromJson<bool>(json['mediaStored']), mediaReopened: serializer.fromJson<bool>(json['mediaReopened']), downloadToken: serializer.fromJson<Uint8List?>(json['downloadToken']), @@ -3206,6 +3232,8 @@ class Message extends DataClass implements Insertable<Message> { serializer.toJson<String>($MessagesTable.$convertertype.toJson(type)), 'content': serializer.toJson<String?>(content), 'mediaId': serializer.toJson<String?>(mediaId), + 'additionalMessageData': + serializer.toJson<Uint8List?>(additionalMessageData), 'mediaStored': serializer.toJson<bool>(mediaStored), 'mediaReopened': serializer.toJson<bool>(mediaReopened), 'downloadToken': serializer.toJson<Uint8List?>(downloadToken), @@ -3227,6 +3255,7 @@ class Message extends DataClass implements Insertable<Message> { MessageType? type, Value<String?> content = const Value.absent(), Value<String?> mediaId = const Value.absent(), + Value<Uint8List?> additionalMessageData = const Value.absent(), bool? mediaStored, bool? mediaReopened, Value<Uint8List?> downloadToken = const Value.absent(), @@ -3245,6 +3274,9 @@ class Message extends DataClass implements Insertable<Message> { type: type ?? this.type, content: content.present ? content.value : this.content, mediaId: mediaId.present ? mediaId.value : this.mediaId, + additionalMessageData: additionalMessageData.present + ? additionalMessageData.value + : this.additionalMessageData, mediaStored: mediaStored ?? this.mediaStored, mediaReopened: mediaReopened ?? this.mediaReopened, downloadToken: @@ -3268,6 +3300,9 @@ class Message extends DataClass implements Insertable<Message> { type: data.type.present ? data.type.value : this.type, content: data.content.present ? data.content.value : this.content, mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, + additionalMessageData: data.additionalMessageData.present + ? data.additionalMessageData.value + : this.additionalMessageData, mediaStored: data.mediaStored.present ? data.mediaStored.value : this.mediaStored, mediaReopened: data.mediaReopened.present @@ -3303,6 +3338,7 @@ class Message extends DataClass implements Insertable<Message> { ..write('type: $type, ') ..write('content: $content, ') ..write('mediaId: $mediaId, ') + ..write('additionalMessageData: $additionalMessageData, ') ..write('mediaStored: $mediaStored, ') ..write('mediaReopened: $mediaReopened, ') ..write('downloadToken: $downloadToken, ') @@ -3326,6 +3362,7 @@ class Message extends DataClass implements Insertable<Message> { type, content, mediaId, + $driftBlobEquality.hash(additionalMessageData), mediaStored, mediaReopened, $driftBlobEquality.hash(downloadToken), @@ -3347,6 +3384,8 @@ class Message extends DataClass implements Insertable<Message> { other.type == this.type && other.content == this.content && other.mediaId == this.mediaId && + $driftBlobEquality.equals( + other.additionalMessageData, this.additionalMessageData) && other.mediaStored == this.mediaStored && other.mediaReopened == this.mediaReopened && $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && @@ -3367,6 +3406,7 @@ class MessagesCompanion extends UpdateCompanion<Message> { final Value<MessageType> type; final Value<String?> content; final Value<String?> mediaId; + final Value<Uint8List?> additionalMessageData; final Value<bool> mediaStored; final Value<bool> mediaReopened; final Value<Uint8List?> downloadToken; @@ -3386,6 +3426,7 @@ class MessagesCompanion extends UpdateCompanion<Message> { this.type = const Value.absent(), this.content = const Value.absent(), this.mediaId = const Value.absent(), + this.additionalMessageData = const Value.absent(), this.mediaStored = const Value.absent(), this.mediaReopened = const Value.absent(), this.downloadToken = const Value.absent(), @@ -3406,6 +3447,7 @@ class MessagesCompanion extends UpdateCompanion<Message> { required MessageType type, this.content = const Value.absent(), this.mediaId = const Value.absent(), + this.additionalMessageData = const Value.absent(), this.mediaStored = const Value.absent(), this.mediaReopened = const Value.absent(), this.downloadToken = const Value.absent(), @@ -3428,6 +3470,7 @@ class MessagesCompanion extends UpdateCompanion<Message> { Expression<String>? type, Expression<String>? content, Expression<String>? mediaId, + Expression<Uint8List>? additionalMessageData, Expression<bool>? mediaStored, Expression<bool>? mediaReopened, Expression<Uint8List>? downloadToken, @@ -3448,6 +3491,8 @@ class MessagesCompanion extends UpdateCompanion<Message> { if (type != null) 'type': type, if (content != null) 'content': content, if (mediaId != null) 'media_id': mediaId, + if (additionalMessageData != null) + 'additional_message_data': additionalMessageData, if (mediaStored != null) 'media_stored': mediaStored, if (mediaReopened != null) 'media_reopened': mediaReopened, if (downloadToken != null) 'download_token': downloadToken, @@ -3471,6 +3516,7 @@ class MessagesCompanion extends UpdateCompanion<Message> { Value<MessageType>? type, Value<String?>? content, Value<String?>? mediaId, + Value<Uint8List?>? additionalMessageData, Value<bool>? mediaStored, Value<bool>? mediaReopened, Value<Uint8List?>? downloadToken, @@ -3490,6 +3536,8 @@ class MessagesCompanion extends UpdateCompanion<Message> { type: type ?? this.type, content: content ?? this.content, mediaId: mediaId ?? this.mediaId, + additionalMessageData: + additionalMessageData ?? this.additionalMessageData, mediaStored: mediaStored ?? this.mediaStored, mediaReopened: mediaReopened ?? this.mediaReopened, downloadToken: downloadToken ?? this.downloadToken, @@ -3527,6 +3575,10 @@ class MessagesCompanion extends UpdateCompanion<Message> { if (mediaId.present) { map['media_id'] = Variable<String>(mediaId.value); } + if (additionalMessageData.present) { + map['additional_message_data'] = + Variable<Uint8List>(additionalMessageData.value); + } if (mediaStored.present) { map['media_stored'] = Variable<bool>(mediaStored.value); } @@ -3575,6 +3627,7 @@ class MessagesCompanion extends UpdateCompanion<Message> { ..write('type: $type, ') ..write('content: $content, ') ..write('mediaId: $mediaId, ') + ..write('additionalMessageData: $additionalMessageData, ') ..write('mediaStored: $mediaStored, ') ..write('mediaReopened: $mediaReopened, ') ..write('downloadToken: $downloadToken, ') @@ -3621,7 +3674,10 @@ class $MessageHistoriesTable extends MessageHistories @override late final GeneratedColumn<int> contactId = GeneratedColumn<int>( 'contact_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES contacts (user_id) ON DELETE CASCADE')); static const VerificationMeta _contentMeta = const VerificationMeta('content'); @override @@ -6964,7 +7020,10 @@ class $MessageActionsTable extends MessageActions @override late final GeneratedColumn<int> contactId = GeneratedColumn<int>( 'contact_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); + type: DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES contacts (user_id) ON DELETE CASCADE')); @override late final GeneratedColumnWithTypeConverter<MessageActionType, String> type = GeneratedColumn<String>('type', aliasedName, false, @@ -7837,6 +7896,13 @@ abstract class _$TwonlyDB extends GeneratedDatabase { TableUpdate('message_histories', kind: UpdateKind.delete), ], ), + WritePropagation( + on: TableUpdateQuery.onTableName('contacts', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('message_histories', kind: UpdateKind.delete), + ], + ), WritePropagation( on: TableUpdateQuery.onTableName('messages', limitUpdateKind: UpdateKind.delete), @@ -7894,6 +7960,13 @@ abstract class _$TwonlyDB extends GeneratedDatabase { TableUpdate('message_actions', kind: UpdateKind.delete), ], ), + WritePropagation( + on: TableUpdateQuery.onTableName('contacts', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('message_actions', kind: UpdateKind.delete), + ], + ), WritePropagation( on: TableUpdateQuery.onTableName('groups', limitUpdateKind: UpdateKind.delete), @@ -7955,6 +8028,23 @@ final class $$ContactsTableReferences manager.$state.copyWith(prefetchedData: cache)); } + static MultiTypedResultKey<$MessageHistoriesTable, List<MessageHistory>> + _messageHistoriesRefsTable(_$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.messageHistories, + aliasName: $_aliasNameGenerator( + db.contacts.userId, db.messageHistories.contactId)); + + $$MessageHistoriesTableProcessedTableManager get messageHistoriesRefs { + final manager = + $$MessageHistoriesTableTableManager($_db, $_db.messageHistories).filter( + (f) => f.contactId.userId.sqlEquals($_itemColumn<int>('user_id')!)); + + final cache = + $_typedResult.readTableOrNull(_messageHistoriesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + static MultiTypedResultKey<$ReactionsTable, List<Reaction>> _reactionsRefsTable(_$TwonlyDB db) => MultiTypedResultKey.fromTable( db.reactions, @@ -8041,6 +8131,22 @@ final class $$ContactsTableReferences manager.$state.copyWith(prefetchedData: cache)); } + static MultiTypedResultKey<$MessageActionsTable, List<MessageAction>> + _messageActionsRefsTable(_$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.messageActions, + aliasName: $_aliasNameGenerator( + db.contacts.userId, db.messageActions.contactId)); + + $$MessageActionsTableProcessedTableManager get messageActionsRefs { + final manager = $$MessageActionsTableTableManager($_db, $_db.messageActions) + .filter( + (f) => f.contactId.userId.sqlEquals($_itemColumn<int>('user_id')!)); + + final cache = $_typedResult.readTableOrNull(_messageActionsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + static MultiTypedResultKey<$GroupHistoriesTable, List<GroupHistory>> _groupHistoriesRefsTable(_$TwonlyDB db) => MultiTypedResultKey.fromTable(db.groupHistories, @@ -8130,6 +8236,27 @@ class $$ContactsTableFilterComposer return f(composer); } + Expression<bool> messageHistoriesRefs( + Expression<bool> Function($$MessageHistoriesTableFilterComposer f) f) { + final $$MessageHistoriesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.messageHistories, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessageHistoriesTableFilterComposer( + $db: $db, + $table: $db.messageHistories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + Expression<bool> reactionsRefs( Expression<bool> Function($$ReactionsTableFilterComposer f) f) { final $$ReactionsTableFilterComposer composer = $composerBuilder( @@ -8239,6 +8366,27 @@ class $$ContactsTableFilterComposer return f(composer); } + Expression<bool> messageActionsRefs( + Expression<bool> Function($$MessageActionsTableFilterComposer f) f) { + final $$MessageActionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.messageActions, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessageActionsTableFilterComposer( + $db: $db, + $table: $db.messageActions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + Expression<bool> groupHistoriesRefs( Expression<bool> Function($$GroupHistoriesTableFilterComposer f) f) { final $$GroupHistoriesTableFilterComposer composer = $composerBuilder( @@ -8383,6 +8531,27 @@ class $$ContactsTableAnnotationComposer return f(composer); } + Expression<T> messageHistoriesRefs<T extends Object>( + Expression<T> Function($$MessageHistoriesTableAnnotationComposer a) f) { + final $$MessageHistoriesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.messageHistories, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessageHistoriesTableAnnotationComposer( + $db: $db, + $table: $db.messageHistories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + Expression<T> reactionsRefs<T extends Object>( Expression<T> Function($$ReactionsTableAnnotationComposer a) f) { final $$ReactionsTableAnnotationComposer composer = $composerBuilder( @@ -8493,6 +8662,27 @@ class $$ContactsTableAnnotationComposer return f(composer); } + Expression<T> messageActionsRefs<T extends Object>( + Expression<T> Function($$MessageActionsTableAnnotationComposer a) f) { + final $$MessageActionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.messageActions, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessageActionsTableAnnotationComposer( + $db: $db, + $table: $db.messageActions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + Expression<T> groupHistoriesRefs<T extends Object>( Expression<T> Function($$GroupHistoriesTableAnnotationComposer a) f) { final $$GroupHistoriesTableAnnotationComposer composer = $composerBuilder( @@ -8528,11 +8718,13 @@ class $$ContactsTableTableManager extends RootTableManager< Contact, PrefetchHooks Function( {bool messagesRefs, + bool messageHistoriesRefs, bool reactionsRefs, bool groupMembersRefs, bool receiptsRefs, bool signalContactPreKeysRefs, bool signalContactSignedPreKeysRefs, + bool messageActionsRefs, bool groupHistoriesRefs})> { $$ContactsTableTableManager(_$TwonlyDB db, $ContactsTable table) : super(TableManagerState( @@ -8610,22 +8802,26 @@ class $$ContactsTableTableManager extends RootTableManager< .toList(), prefetchHooksCallback: ( {messagesRefs = false, + messageHistoriesRefs = false, reactionsRefs = false, groupMembersRefs = false, receiptsRefs = false, signalContactPreKeysRefs = false, signalContactSignedPreKeysRefs = false, + messageActionsRefs = false, groupHistoriesRefs = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ if (messagesRefs) db.messages, + if (messageHistoriesRefs) db.messageHistories, if (reactionsRefs) db.reactions, if (groupMembersRefs) db.groupMembers, if (receiptsRefs) db.receipts, if (signalContactPreKeysRefs) db.signalContactPreKeys, if (signalContactSignedPreKeysRefs) db.signalContactSignedPreKeys, + if (messageActionsRefs) db.messageActions, if (groupHistoriesRefs) db.groupHistories ], addJoins: null, @@ -8643,6 +8839,19 @@ class $$ContactsTableTableManager extends RootTableManager< (item, referencedItems) => referencedItems .where((e) => e.senderId == item.userId), typedResults: items), + if (messageHistoriesRefs) + await $_getPrefetchedData<Contact, $ContactsTable, + MessageHistory>( + currentTable: table, + referencedTable: $$ContactsTableReferences + ._messageHistoriesRefsTable(db), + managerFromTypedResult: (p0) => + $$ContactsTableReferences(db, table, p0) + .messageHistoriesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.contactId == item.userId), + typedResults: items), if (reactionsRefs) await $_getPrefetchedData<Contact, $ContactsTable, Reaction>( @@ -8707,6 +8916,19 @@ class $$ContactsTableTableManager extends RootTableManager< (item, referencedItems) => referencedItems .where((e) => e.contactId == item.userId), typedResults: items), + if (messageActionsRefs) + await $_getPrefetchedData<Contact, $ContactsTable, + MessageAction>( + currentTable: table, + referencedTable: $$ContactsTableReferences + ._messageActionsRefsTable(db), + managerFromTypedResult: (p0) => + $$ContactsTableReferences(db, table, p0) + .messageActionsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.contactId == item.userId), + typedResults: items), if (groupHistoriesRefs) await $_getPrefetchedData<Contact, $ContactsTable, GroupHistory>( @@ -8740,11 +8962,13 @@ typedef $$ContactsTableProcessedTableManager = ProcessedTableManager< Contact, PrefetchHooks Function( {bool messagesRefs, + bool messageHistoriesRefs, bool reactionsRefs, bool groupMembersRefs, bool receiptsRefs, bool signalContactPreKeysRefs, bool signalContactSignedPreKeysRefs, + bool messageActionsRefs, bool groupHistoriesRefs})>; typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ required String groupId, @@ -9927,6 +10151,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ required MessageType type, Value<String?> content, Value<String?> mediaId, + Value<Uint8List?> additionalMessageData, Value<bool> mediaStored, Value<bool> mediaReopened, Value<Uint8List?> downloadToken, @@ -9947,6 +10172,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ Value<MessageType> type, Value<String?> content, Value<String?> mediaId, + Value<Uint8List?> additionalMessageData, Value<bool> mediaStored, Value<bool> mediaReopened, Value<Uint8List?> downloadToken, @@ -10096,6 +10322,10 @@ class $$MessagesTableFilterComposer ColumnFilters<String> get content => $composableBuilder( column: $table.content, builder: (column) => ColumnFilters(column)); + ColumnFilters<Uint8List> get additionalMessageData => $composableBuilder( + column: $table.additionalMessageData, + builder: (column) => ColumnFilters(column)); + ColumnFilters<bool> get mediaStored => $composableBuilder( column: $table.mediaStored, builder: (column) => ColumnFilters(column)); @@ -10294,6 +10524,10 @@ class $$MessagesTableOrderingComposer ColumnOrderings<String> get content => $composableBuilder( column: $table.content, builder: (column) => ColumnOrderings(column)); + ColumnOrderings<Uint8List> get additionalMessageData => $composableBuilder( + column: $table.additionalMessageData, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings<bool> get mediaStored => $composableBuilder( column: $table.mediaStored, builder: (column) => ColumnOrderings(column)); @@ -10410,6 +10644,9 @@ class $$MessagesTableAnnotationComposer GeneratedColumn<String> get content => $composableBuilder(column: $table.content, builder: (column) => column); + GeneratedColumn<Uint8List> get additionalMessageData => $composableBuilder( + column: $table.additionalMessageData, builder: (column) => column); + GeneratedColumn<bool> get mediaStored => $composableBuilder( column: $table.mediaStored, builder: (column) => column); @@ -10624,6 +10861,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value<MessageType> type = const Value.absent(), Value<String?> content = const Value.absent(), Value<String?> mediaId = const Value.absent(), + Value<Uint8List?> additionalMessageData = const Value.absent(), Value<bool> mediaStored = const Value.absent(), Value<bool> mediaReopened = const Value.absent(), Value<Uint8List?> downloadToken = const Value.absent(), @@ -10644,6 +10882,7 @@ class $$MessagesTableTableManager extends RootTableManager< type: type, content: content, mediaId: mediaId, + additionalMessageData: additionalMessageData, mediaStored: mediaStored, mediaReopened: mediaReopened, downloadToken: downloadToken, @@ -10664,6 +10903,7 @@ class $$MessagesTableTableManager extends RootTableManager< required MessageType type, Value<String?> content = const Value.absent(), Value<String?> mediaId = const Value.absent(), + Value<Uint8List?> additionalMessageData = const Value.absent(), Value<bool> mediaStored = const Value.absent(), Value<bool> mediaReopened = const Value.absent(), Value<Uint8List?> downloadToken = const Value.absent(), @@ -10684,6 +10924,7 @@ class $$MessagesTableTableManager extends RootTableManager< type: type, content: content, mediaId: mediaId, + additionalMessageData: additionalMessageData, mediaStored: mediaStored, mediaReopened: mediaReopened, downloadToken: downloadToken, @@ -10878,6 +11119,21 @@ final class $$MessageHistoriesTableReferences return ProcessedTableManager( manager.$state.copyWith(prefetchedData: [item])); } + + static $ContactsTable _contactIdTable(_$TwonlyDB db) => + db.contacts.createAlias($_aliasNameGenerator( + db.messageHistories.contactId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager? get contactId { + final $_column = $_itemColumn<int>('contact_id'); + if ($_column == null) return null; + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_contactIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } } class $$MessageHistoriesTableFilterComposer @@ -10892,9 +11148,6 @@ class $$MessageHistoriesTableFilterComposer ColumnFilters<int> get id => $composableBuilder( column: $table.id, builder: (column) => ColumnFilters(column)); - ColumnFilters<int> get contactId => $composableBuilder( - column: $table.contactId, builder: (column) => ColumnFilters(column)); - ColumnFilters<String> get content => $composableBuilder( column: $table.content, builder: (column) => ColumnFilters(column)); @@ -10920,6 +11173,26 @@ class $$MessageHistoriesTableFilterComposer )); return composer; } + + $$ContactsTableFilterComposer get contactId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } } class $$MessageHistoriesTableOrderingComposer @@ -10934,9 +11207,6 @@ class $$MessageHistoriesTableOrderingComposer ColumnOrderings<int> get id => $composableBuilder( column: $table.id, builder: (column) => ColumnOrderings(column)); - ColumnOrderings<int> get contactId => $composableBuilder( - column: $table.contactId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings<String> get content => $composableBuilder( column: $table.content, builder: (column) => ColumnOrderings(column)); @@ -10962,6 +11232,26 @@ class $$MessageHistoriesTableOrderingComposer )); return composer; } + + $$ContactsTableOrderingComposer get contactId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } } class $$MessageHistoriesTableAnnotationComposer @@ -10976,9 +11266,6 @@ class $$MessageHistoriesTableAnnotationComposer GeneratedColumn<int> get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn<int> get contactId => - $composableBuilder(column: $table.contactId, builder: (column) => column); - GeneratedColumn<String> get content => $composableBuilder(column: $table.content, builder: (column) => column); @@ -11004,6 +11291,26 @@ class $$MessageHistoriesTableAnnotationComposer )); return composer; } + + $$ContactsTableAnnotationComposer get contactId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } } class $$MessageHistoriesTableTableManager extends RootTableManager< @@ -11017,7 +11324,7 @@ class $$MessageHistoriesTableTableManager extends RootTableManager< $$MessageHistoriesTableUpdateCompanionBuilder, (MessageHistory, $$MessageHistoriesTableReferences), MessageHistory, - PrefetchHooks Function({bool messageId})> { + PrefetchHooks Function({bool messageId, bool contactId})> { $$MessageHistoriesTableTableManager( _$TwonlyDB db, $MessageHistoriesTable table) : super(TableManagerState( @@ -11063,7 +11370,7 @@ class $$MessageHistoriesTableTableManager extends RootTableManager< $$MessageHistoriesTableReferences(db, table, e) )) .toList(), - prefetchHooksCallback: ({messageId = false}) { + prefetchHooksCallback: ({messageId = false, contactId = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [], @@ -11091,6 +11398,17 @@ class $$MessageHistoriesTableTableManager extends RootTableManager< .messageId, ) as T; } + if (contactId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.contactId, + referencedTable: + $$MessageHistoriesTableReferences._contactIdTable(db), + referencedColumn: $$MessageHistoriesTableReferences + ._contactIdTable(db) + .userId, + ) as T; + } return state; }, @@ -11113,7 +11431,7 @@ typedef $$MessageHistoriesTableProcessedTableManager = ProcessedTableManager< $$MessageHistoriesTableUpdateCompanionBuilder, (MessageHistory, $$MessageHistoriesTableReferences), MessageHistory, - PrefetchHooks Function({bool messageId})>; + PrefetchHooks Function({bool messageId, bool contactId})>; typedef $$ReactionsTableCreateCompanionBuilder = ReactionsCompanion Function({ required String messageId, required String emoji, @@ -13581,6 +13899,21 @@ final class $$MessageActionsTableReferences return ProcessedTableManager( manager.$state.copyWith(prefetchedData: [item])); } + + static $ContactsTable _contactIdTable(_$TwonlyDB db) => + db.contacts.createAlias($_aliasNameGenerator( + db.messageActions.contactId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager get contactId { + final $_column = $_itemColumn<int>('contact_id')!; + + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_contactIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } } class $$MessageActionsTableFilterComposer @@ -13592,9 +13925,6 @@ class $$MessageActionsTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters<int> get contactId => $composableBuilder( - column: $table.contactId, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters<MessageActionType, MessageActionType, String> get type => $composableBuilder( column: $table.type, @@ -13622,6 +13952,26 @@ class $$MessageActionsTableFilterComposer )); return composer; } + + $$ContactsTableFilterComposer get contactId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } } class $$MessageActionsTableOrderingComposer @@ -13633,9 +13983,6 @@ class $$MessageActionsTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings<int> get contactId => $composableBuilder( - column: $table.contactId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings<String> get type => $composableBuilder( column: $table.type, builder: (column) => ColumnOrderings(column)); @@ -13661,6 +14008,26 @@ class $$MessageActionsTableOrderingComposer )); return composer; } + + $$ContactsTableOrderingComposer get contactId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } } class $$MessageActionsTableAnnotationComposer @@ -13672,9 +14039,6 @@ class $$MessageActionsTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn<int> get contactId => - $composableBuilder(column: $table.contactId, builder: (column) => column); - GeneratedColumnWithTypeConverter<MessageActionType, String> get type => $composableBuilder(column: $table.type, builder: (column) => column); @@ -13700,6 +14064,26 @@ class $$MessageActionsTableAnnotationComposer )); return composer; } + + $$ContactsTableAnnotationComposer get contactId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } } class $$MessageActionsTableTableManager extends RootTableManager< @@ -13713,7 +14097,7 @@ class $$MessageActionsTableTableManager extends RootTableManager< $$MessageActionsTableUpdateCompanionBuilder, (MessageAction, $$MessageActionsTableReferences), MessageAction, - PrefetchHooks Function({bool messageId})> { + PrefetchHooks Function({bool messageId, bool contactId})> { $$MessageActionsTableTableManager(_$TwonlyDB db, $MessageActionsTable table) : super(TableManagerState( db: db, @@ -13758,7 +14142,7 @@ class $$MessageActionsTableTableManager extends RootTableManager< $$MessageActionsTableReferences(db, table, e) )) .toList(), - prefetchHooksCallback: ({messageId = false}) { + prefetchHooksCallback: ({messageId = false, contactId = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [], @@ -13786,6 +14170,17 @@ class $$MessageActionsTableTableManager extends RootTableManager< .messageId, ) as T; } + if (contactId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.contactId, + referencedTable: + $$MessageActionsTableReferences._contactIdTable(db), + referencedColumn: $$MessageActionsTableReferences + ._contactIdTable(db) + .userId, + ) as T; + } return state; }, @@ -13808,7 +14203,7 @@ typedef $$MessageActionsTableProcessedTableManager = ProcessedTableManager< $$MessageActionsTableUpdateCompanionBuilder, (MessageAction, $$MessageActionsTableReferences), MessageAction, - PrefetchHooks Function({bool messageId})>; + PrefetchHooks Function({bool messageId, bool contactId})>; typedef $$GroupHistoriesTableCreateCompanionBuilder = GroupHistoriesCompanion Function({ required String groupHistoryId, diff --git a/lib/src/database/twonly.db.steps.dart b/lib/src/database/twonly.db.steps.dart index 24ecbc9..fc68ba1 100644 --- a/lib/src/database/twonly.db.steps.dart +++ b/lib/src/database/twonly.db.steps.dart @@ -2804,12 +2804,439 @@ i1.GeneratedColumn<DateTime> _column_104(String aliasedName) => i1.GeneratedColumn<DateTime>( 'mark_for_retry_after_accepted', aliasedName, true, type: i1.DriftSqlType.dateTime); + +final class Schema7 extends i0.VersionedSchema { + Schema7({required super.database}) : super(version: 7); + @override + late final List<i1.DatabaseSchemaEntity> entities = [ + contacts, + groups, + mediaFiles, + messages, + messageHistories, + reactions, + groupMembers, + receipts, + receivedReceipts, + signalIdentityKeyStores, + signalPreKeyStores, + signalSenderKeyStores, + signalSessionStores, + signalContactPreKeys, + signalContactSignedPreKeys, + messageActions, + groupHistories, + ]; + late final Shape0 contacts = Shape0( + source: i0.VersionedTable( + entityName: 'contacts', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(user_id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_8, + _column_9, + _column_10, + _column_11, + _column_12, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape17 groups = Shape17( + source: i0.VersionedTable( + entityName: 'groups', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(group_id)', + ], + columns: [ + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_22, + _column_23, + _column_24, + _column_100, + _column_25, + _column_26, + _column_27, + _column_12, + _column_28, + _column_29, + _column_30, + _column_31, + _column_32, + _column_33, + _column_34, + _column_35, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape18 mediaFiles = Shape18( + source: i0.VersionedTable( + entityName: 'media_files', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(media_id)', + ], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_102, + _column_12, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape21 messages = Shape21( + source: i0.VersionedTable( + entityName: 'messages', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(message_id)', + ], + columns: [ + _column_50, + _column_51, + _column_52, + _column_37, + _column_53, + _column_54, + _column_105, + _column_55, + _column_56, + _column_46, + _column_57, + _column_58, + _column_59, + _column_60, + _column_12, + _column_61, + _column_62, + _column_63, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape4 messageHistories = Shape4( + source: i0.VersionedTable( + entityName: 'message_histories', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_64, + _column_65, + _column_66, + _column_53, + _column_12, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape5 reactions = Shape5( + source: i0.VersionedTable( + entityName: 'reactions', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(message_id, sender_id, emoji)', + ], + columns: [ + _column_65, + _column_67, + _column_68, + _column_12, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape6 groupMembers = Shape6( + source: i0.VersionedTable( + entityName: 'group_members', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(group_id, contact_id)', + ], + columns: [ + _column_50, + _column_69, + _column_70, + _column_71, + _column_72, + _column_12, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape20 receipts = Shape20( + source: i0.VersionedTable( + entityName: 'receipts', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(receipt_id)', + ], + columns: [ + _column_73, + _column_74, + _column_75, + _column_76, + _column_77, + _column_103, + _column_104, + _column_78, + _column_79, + _column_80, + _column_12, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape8 receivedReceipts = Shape8( + source: i0.VersionedTable( + entityName: 'received_receipts', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(receipt_id)', + ], + columns: [ + _column_73, + _column_12, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape9 signalIdentityKeyStores = Shape9( + source: i0.VersionedTable( + entityName: 'signal_identity_key_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(device_id, name)', + ], + columns: [ + _column_81, + _column_82, + _column_83, + _column_12, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape10 signalPreKeyStores = Shape10( + source: i0.VersionedTable( + entityName: 'signal_pre_key_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(pre_key_id)', + ], + columns: [ + _column_84, + _column_85, + _column_12, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape11 signalSenderKeyStores = Shape11( + source: i0.VersionedTable( + entityName: 'signal_sender_key_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(sender_key_name)', + ], + columns: [ + _column_86, + _column_87, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape12 signalSessionStores = Shape12( + source: i0.VersionedTable( + entityName: 'signal_session_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(device_id, name)', + ], + columns: [ + _column_81, + _column_82, + _column_88, + _column_12, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape13 signalContactPreKeys = Shape13( + source: i0.VersionedTable( + entityName: 'signal_contact_pre_keys', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(contact_id, pre_key_id)', + ], + columns: [ + _column_74, + _column_84, + _column_85, + _column_12, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape14 signalContactSignedPreKeys = Shape14( + source: i0.VersionedTable( + entityName: 'signal_contact_signed_pre_keys', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(contact_id)', + ], + columns: [ + _column_74, + _column_89, + _column_90, + _column_91, + _column_12, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape15 messageActions = Shape15( + source: i0.VersionedTable( + entityName: 'message_actions', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(message_id, contact_id, type)', + ], + columns: [ + _column_65, + _column_92, + _column_37, + _column_93, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape16 groupHistories = Shape16( + source: i0.VersionedTable( + entityName: 'group_histories', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(group_history_id)', + ], + columns: [ + _column_94, + _column_50, + _column_95, + _column_101, + _column_97, + _column_98, + _column_99, + _column_37, + _column_93, + ], + attachedDatabase: database, + ), + alias: null); +} + +class Shape21 extends i0.VersionedTable { + Shape21({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn<String> get groupId => + columnsByName['group_id']! as i1.GeneratedColumn<String>; + i1.GeneratedColumn<String> get messageId => + columnsByName['message_id']! as i1.GeneratedColumn<String>; + i1.GeneratedColumn<int> get senderId => + columnsByName['sender_id']! as i1.GeneratedColumn<int>; + i1.GeneratedColumn<String> get type => + columnsByName['type']! as i1.GeneratedColumn<String>; + i1.GeneratedColumn<String> get content => + columnsByName['content']! as i1.GeneratedColumn<String>; + i1.GeneratedColumn<String> get mediaId => + columnsByName['media_id']! as i1.GeneratedColumn<String>; + i1.GeneratedColumn<i2.Uint8List> get additionalMessageData => + columnsByName['additional_message_data']! + as i1.GeneratedColumn<i2.Uint8List>; + i1.GeneratedColumn<bool> get mediaStored => + columnsByName['media_stored']! as i1.GeneratedColumn<bool>; + i1.GeneratedColumn<bool> get mediaReopened => + columnsByName['media_reopened']! as i1.GeneratedColumn<bool>; + i1.GeneratedColumn<i2.Uint8List> get downloadToken => + columnsByName['download_token']! as i1.GeneratedColumn<i2.Uint8List>; + i1.GeneratedColumn<String> get quotesMessageId => + columnsByName['quotes_message_id']! as i1.GeneratedColumn<String>; + i1.GeneratedColumn<bool> get isDeletedFromSender => + columnsByName['is_deleted_from_sender']! as i1.GeneratedColumn<bool>; + i1.GeneratedColumn<DateTime> get openedAt => + columnsByName['opened_at']! as i1.GeneratedColumn<DateTime>; + i1.GeneratedColumn<DateTime> get openedByAll => + columnsByName['opened_by_all']! as i1.GeneratedColumn<DateTime>; + i1.GeneratedColumn<DateTime> get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn<DateTime>; + i1.GeneratedColumn<DateTime> get modifiedAt => + columnsByName['modified_at']! as i1.GeneratedColumn<DateTime>; + i1.GeneratedColumn<DateTime> get ackByUser => + columnsByName['ack_by_user']! as i1.GeneratedColumn<DateTime>; + i1.GeneratedColumn<DateTime> get ackByServer => + columnsByName['ack_by_server']! as i1.GeneratedColumn<DateTime>; +} + +i1.GeneratedColumn<i2.Uint8List> _column_105(String aliasedName) => + i1.GeneratedColumn<i2.Uint8List>( + 'additional_message_data', aliasedName, true, + type: i1.DriftSqlType.blob); i0.MigrationStepWithVersion migrationSteps({ required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4, required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5, required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6, + required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -2838,6 +3265,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from5To6(migrator, schema); return 6; + case 6: + final schema = Schema7(database: database); + final migrator = i1.Migrator(database, schema); + await from6To7(migrator, schema); + return 7; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -2850,6 +3282,7 @@ i1.OnUpgrade stepByStep({ required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4, required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5, required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6, + required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( @@ -2858,4 +3291,5 @@ i1.OnUpgrade stepByStep({ from3To4: from3To4, from4To5: from4To5, from5To6: from5To6, + from6To7: from6To7, )); diff --git a/lib/src/model/protobuf/client/data.proto b/lib/src/model/protobuf/client/data.proto new file mode 100644 index 0000000..20a3b48 --- /dev/null +++ b/lib/src/model/protobuf/client/data.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + + +message AdditionalMessageData { + enum Type { + LINK = 0; + } + Type type = 1; + + optional string link = 2; +} \ No newline at end of file diff --git a/lib/src/model/protobuf/client/generated/data.pb.dart b/lib/src/model/protobuf/client/generated/data.pb.dart new file mode 100644 index 0000000..9b547b3 --- /dev/null +++ b/lib/src/model/protobuf/client/generated/data.pb.dart @@ -0,0 +1,99 @@ +// This is a generated file - do not edit. +// +// Generated from data.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'data.pbenum.dart'; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +export 'data.pbenum.dart'; + +class AdditionalMessageData extends $pb.GeneratedMessage { + factory AdditionalMessageData({ + AdditionalMessageData_Type? type, + $core.String? link, + }) { + final result = create(); + if (type != null) result.type = type; + if (link != null) result.link = link; + return result; + } + + AdditionalMessageData._(); + + factory AdditionalMessageData.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory AdditionalMessageData.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'AdditionalMessageData', + createEmptyInstance: create) + ..e<AdditionalMessageData_Type>( + 1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, + defaultOrMaker: AdditionalMessageData_Type.LINK, + valueOf: AdditionalMessageData_Type.valueOf, + enumValues: AdditionalMessageData_Type.values) + ..aOS(2, _omitFieldNames ? '' : 'link') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + AdditionalMessageData clone() => + AdditionalMessageData()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + AdditionalMessageData copyWith( + void Function(AdditionalMessageData) updates) => + super.copyWith((message) => updates(message as AdditionalMessageData)) + as AdditionalMessageData; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AdditionalMessageData create() => AdditionalMessageData._(); + @$core.override + AdditionalMessageData createEmptyInstance() => create(); + static $pb.PbList<AdditionalMessageData> createRepeated() => + $pb.PbList<AdditionalMessageData>(); + @$core.pragma('dart2js:noInline') + static AdditionalMessageData getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<AdditionalMessageData>(create); + static AdditionalMessageData? _defaultInstance; + + @$pb.TagNumber(1) + AdditionalMessageData_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(AdditionalMessageData_Type value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get link => $_getSZ(1); + @$pb.TagNumber(2) + set link($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasLink() => $_has(1); + @$pb.TagNumber(2) + void clearLink() => $_clearField(2); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/lib/src/model/protobuf/client/generated/data.pbenum.dart b/lib/src/model/protobuf/client/generated/data.pbenum.dart new file mode 100644 index 0000000..4f40622 --- /dev/null +++ b/lib/src/model/protobuf/client/generated/data.pbenum.dart @@ -0,0 +1,35 @@ +// This is a generated file - do not edit. +// +// Generated from data.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class AdditionalMessageData_Type extends $pb.ProtobufEnum { + static const AdditionalMessageData_Type LINK = + AdditionalMessageData_Type._(0, _omitEnumNames ? '' : 'LINK'); + + static const $core.List<AdditionalMessageData_Type> values = + <AdditionalMessageData_Type>[ + LINK, + ]; + + static final $core.List<AdditionalMessageData_Type?> _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 0); + static AdditionalMessageData_Type? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const AdditionalMessageData_Type._(super.value, super.name); +} + +const $core.bool _omitEnumNames = + $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/lib/src/model/protobuf/client/generated/data.pbjson.dart b/lib/src/model/protobuf/client/generated/data.pbjson.dart new file mode 100644 index 0000000..fb0a248 --- /dev/null +++ b/lib/src/model/protobuf/client/generated/data.pbjson.dart @@ -0,0 +1,49 @@ +// This is a generated file - do not edit. +// +// Generated from data.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use additionalMessageDataDescriptor instead') +const AdditionalMessageData$json = { + '1': 'AdditionalMessageData', + '2': [ + { + '1': 'type', + '3': 1, + '4': 1, + '5': 14, + '6': '.AdditionalMessageData.Type', + '10': 'type' + }, + {'1': 'link', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'link', '17': true}, + ], + '4': [AdditionalMessageData_Type$json], + '8': [ + {'1': '_link'}, + ], +}; + +@$core.Deprecated('Use additionalMessageDataDescriptor instead') +const AdditionalMessageData_Type$json = { + '1': 'Type', + '2': [ + {'1': 'LINK', '2': 0}, + ], +}; + +/// Descriptor for `AdditionalMessageData`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List additionalMessageDataDescriptor = $convert.base64Decode( + 'ChVBZGRpdGlvbmFsTWVzc2FnZURhdGESLwoEdHlwZRgBIAEoDjIbLkFkZGl0aW9uYWxNZXNzYW' + 'dlRGF0YS5UeXBlUgR0eXBlEhcKBGxpbmsYAiABKAlIAFIEbGlua4gBASIQCgRUeXBlEggKBExJ' + 'TksQAEIHCgVfbGluaw=='); diff --git a/lib/src/model/protobuf/client/generated/messages.pb.dart b/lib/src/model/protobuf/client/generated/messages.pb.dart index a02260f..b137dde 100644 --- a/lib/src/model/protobuf/client/generated/messages.pb.dart +++ b/lib/src/model/protobuf/client/generated/messages.pb.dart @@ -969,6 +969,7 @@ class EncryptedContent_Media extends $pb.GeneratedMessage { $core.List<$core.int>? encryptionKey, $core.List<$core.int>? encryptionMac, $core.List<$core.int>? encryptionNonce, + $core.List<$core.int>? additionalMessageData, }) { final result = create(); if (senderMessageId != null) result.senderMessageId = senderMessageId; @@ -983,6 +984,8 @@ class EncryptedContent_Media extends $pb.GeneratedMessage { if (encryptionKey != null) result.encryptionKey = encryptionKey; if (encryptionMac != null) result.encryptionMac = encryptionMac; if (encryptionNonce != null) result.encryptionNonce = encryptionNonce; + if (additionalMessageData != null) + result.additionalMessageData = additionalMessageData; return result; } @@ -1024,6 +1027,8 @@ class EncryptedContent_Media extends $pb.GeneratedMessage { ..a<$core.List<$core.int>>( 10, _omitFieldNames ? '' : 'encryptionNonce', $pb.PbFieldType.OY, protoName: 'encryptionNonce') + ..a<$core.List<$core.int>>( + 11, _omitFieldNames ? '' : 'additionalMessageData', $pb.PbFieldType.OY) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -1138,6 +1143,16 @@ class EncryptedContent_Media extends $pb.GeneratedMessage { $core.bool hasEncryptionNonce() => $_has(9); @$pb.TagNumber(10) void clearEncryptionNonce() => $_clearField(10); + + @$pb.TagNumber(11) + $core.List<$core.int> get additionalMessageData => $_getN(10); + @$pb.TagNumber(11) + set additionalMessageData($core.List<$core.int> value) => + $_setBytes(10, value); + @$pb.TagNumber(11) + $core.bool hasAdditionalMessageData() => $_has(10); + @$pb.TagNumber(11) + void clearAdditionalMessageData() => $_clearField(11); } class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index 0331672..fd49512 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -603,6 +603,15 @@ const EncryptedContent_Media$json = { '10': 'encryptionNonce', '17': true }, + { + '1': 'additional_message_data', + '3': 11, + '4': 1, + '5': 12, + '9': 6, + '10': 'additionalMessageData', + '17': true + }, ], '4': [EncryptedContent_Media_Type$json], '8': [ @@ -612,6 +621,7 @@ const EncryptedContent_Media$json = { {'1': '_encryptionKey'}, {'1': '_encryptionMac'}, {'1': '_encryptionNonce'}, + {'1': '_additional_message_data'}, ], }; @@ -840,7 +850,7 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( 'EjoKGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxgDIAMoCVIYbXVsdGlwbGVUYXJnZXRNZXNzYW' 'dlSWRzEhcKBHRleHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVz' 'dGFtcCItCgRUeXBlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEgoKBk9QRU5FRBACQhIKEF' - '9zZW5kZXJNZXNzYWdlSWRCBwoFX3RleHQalwUKBU1lZGlhEigKD3NlbmRlck1lc3NhZ2VJZBgB' + '9zZW5kZXJNZXNzYWdlSWRCBwoFX3RleHQa8AUKBU1lZGlhEigKD3NlbmRlck1lc3NhZ2VJZBgB' 'IAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0ZWRDb250ZW50Lk' '1lZGlhLlR5cGVSBHR5cGUSQwoaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHMYAyABKANIAFIa' 'ZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNgoWcmVxdWlyZXNBdXRoZW50aWNhdGlvbh' @@ -849,29 +859,31 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( 'dubG9hZFRva2VuGAcgASgMSAJSDWRvd25sb2FkVG9rZW6IAQESKQoNZW5jcnlwdGlvbktleRgI' 'IAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEikKDWVuY3J5cHRpb25NYWMYCSABKAxIBFINZW5jcn' 'lwdGlvbk1hY4gBARItCg9lbmNyeXB0aW9uTm9uY2UYCiABKAxIBVIPZW5jcnlwdGlvbk5vbmNl' - 'iAEBIj4KBFR5cGUSDAoIUkVVUExPQUQQABIJCgVJTUFHRRABEgkKBVZJREVPEAISBwoDR0lGEA' - 'MSCQoFQVVESU8QBEIdChtfZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVz' - 'c2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQhAKDl9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW' - '9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2UapwEKC01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4y' - 'Ii5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cGVSBHR5cGUSKAoPdGFyZ2V0TWVzc2' - 'FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNU' - 'T1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGA' - 'EgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5' - 'cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0VQVBACGp4CCg1Db250YWN0VXBkYX' - 'RlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIE' - 'dHlwZRI1ChNhdmF0YXJTdmdDb21wcmVzc2VkGAIgASgMSABSE2F2YXRhclN2Z0NvbXByZXNzZW' - 'SIAQESHwoIdXNlcm5hbWUYAyABKAlIAVIIdXNlcm5hbWWIAQESJQoLZGlzcGxheU5hbWUYBCAB' - 'KAlIAlILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCFg' - 'oUX2F2YXRhclN2Z0NvbXByZXNzZWRCCwoJX3VzZXJuYW1lQg4KDF9kaXNwbGF5TmFtZRrVAQoI' - 'UHVzaEtleXMSMwoEdHlwZRgBIAEoDjIfLkVuY3J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZV' - 'IEdHlwZRIZCgVrZXlJZBgCIAEoA0gAUgVrZXlJZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEB' - 'EiEKCWNyZWF0ZWRBdBgEIAEoA0gCUgljcmVhdGVkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEA' - 'ASCgoGVVBEQVRFEAFCCAoGX2tleUlkQgYKBF9rZXlCDAoKX2NyZWF0ZWRBdBqpAQoJRmxhbWVT' - 'eW5jEiIKDGZsYW1lQ291bnRlchgBIAEoA1IMZmxhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW' - '50ZXJDaGFuZ2UYAiABKANSFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgD' - 'IAEoCFIKYmVzdEZyaWVuZBIgCgtmb3JjZVVwZGF0ZRgEIAEoCFILZm9yY2VVcGRhdGVCCgoIX2' - 'dyb3VwSWRCDwoNX2lzRGlyZWN0Q2hhdEIXChVfc2VuZGVyUHJvZmlsZUNvdW50ZXJCEAoOX21l' - 'c3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZWRpYVVwZGF0ZUIQCg5fY29udGFjdFVwZGF0ZU' - 'IRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1lU3luY0ILCglfcHVzaEtleXNCCwoJX3JlYWN0' - 'aW9uQg4KDF90ZXh0TWVzc2FnZUIOCgxfZ3JvdXBDcmVhdGVCDAoKX2dyb3VwSm9pbkIOCgxfZ3' - 'JvdXBVcGRhdGVCFwoVX3Jlc2VuZEdyb3VwUHVibGljS2V5QhEKD19lcnJvcl9tZXNzYWdlcw=='); + 'iAEBEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAsgASgMSAZSFWFkZGl0aW9uYWxNZXNzYW' + 'dlRGF0YYgBASI+CgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJCgVWSURFTxACEgcK' + 'A0dJRhADEgkKBUFVRElPEARCHQobX2Rpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRzQhEKD19xdW' + '90ZU1lc3NhZ2VJZEIQCg5fZG93bmxvYWRUb2tlbkIQCg5fZW5jcnlwdGlvbktleUIQCg5fZW5j' + 'cnlwdGlvbk1hY0ISChBfZW5jcnlwdGlvbk5vbmNlQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZG' + 'F0YRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVk' + 'aWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE1lc3' + 'NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9F' + 'UlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW' + '50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVK' + 'RUNUEAESCgoGQUNDRVBUEAIangIKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3' + 'J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0NvbXBy' + 'ZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgDIAEoCU' + 'gBUgh1c2VybmFtZYgBARIlCgtkaXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZYgBASIf' + 'CgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJlc3NlZE' + 'ILCglfdXNlcm5hbWVCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgO' + 'Mh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIgASgDSA' + 'BSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgASgDSAJS' + 'CWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZfa2V5SW' + 'RCBgoEX2tleUIMCgpfY3JlYXRlZEF0GqkBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudGVyGAEg' + 'ASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IWbGFzdE' + 'ZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiAKC2Zv' + 'cmNlVXBkYXRlGAQgASgIUgtmb3JjZVVwZGF0ZUIKCghfZ3JvdXBJZEIPCg1faXNEaXJlY3RDaG' + 'F0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbWVkaWFC' + 'DgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVzdEIMCg' + 'pfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYWdlQg4K' + 'DF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVzZW5kR3' + 'JvdXBQdWJsaWNLZXlCEQoPX2Vycm9yX21lc3NhZ2Vz'); diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index 1e71cbc..38e24ad 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -132,6 +132,8 @@ message EncryptedContent { optional bytes encryptionKey = 8; optional bytes encryptionMac = 9; optional bytes encryptionNonce = 10; + + optional bytes additional_message_data = 11; } message MediaUpdate { diff --git a/lib/src/services/api/client2client/media.c2c.dart b/lib/src/services/api/client2client/media.c2c.dart index 807706a..d0c8e3a 100644 --- a/lib/src/services/api/client2client/media.c2c.dart +++ b/lib/src/services/api/client2client/media.c2c.dart @@ -105,6 +105,11 @@ Future<void> handleMedia( groupId: Value(groupId), mediaId: Value(mediaFile.mediaId), type: const Value(MessageType.media), + additionalMessageData: Value.absentIfNull( + media.hasAdditionalMessageData() + ? Uint8List.fromList(media.additionalMessageData) + : null, + ), quotesMessageId: Value( media.hasQuoteMessageId() ? media.quoteMessageId : null, ), diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index 26f3d7e..6826f20 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -16,6 +16,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/mediafiles/media_background.service.dart'; import 'package:twonly/src/services/api/messages.dart'; @@ -88,8 +89,9 @@ Future<MediaFileService?> initializeMediaUpload( Future<void> insertMediaFileInMessagesTable( MediaFileService mediaService, - List<String> groupIds, -) async { + List<String> groupIds, { + AdditionalMessageData? additionalData, +}) async { await twonlyDB.mediaFilesDao.updateAllMediaFiles( const MediaFilesCompanion( isDraftMedia: Value(false), @@ -101,6 +103,8 @@ Future<void> insertMediaFileInMessagesTable( groupId: Value(groupId), mediaId: Value(mediaService.mediaFile.mediaId), type: const Value(MessageType.media), + additionalMessageData: + Value.absentIfNull(additionalData?.writeToBuffer()), ), ); await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now()); @@ -245,6 +249,7 @@ Future<void> _createUploadRequest(MediaFileService media) async { encryptionKey: media.mediaFile.encryptionKey, encryptionNonce: media.mediaFile.encryptionNonce, encryptionMac: media.mediaFile.encryptionMac, + additionalMessageData: message.additionalMessageData, ), ); diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index 49ee512..6db4af4 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -61,6 +61,7 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ String? receiptId, Receipt? receipt, bool onlyReturnEncryptedData = false, + bool blocking = true, }) async { try { if (receiptId == null && receipt == null) return null; @@ -238,12 +239,11 @@ Future<void> sendCipherTextToGroup( encryptedContent.groupId = groupId; for (final groupMember in groupMembers) { - unawaited( - sendCipherText( - groupMember.contactId, - encryptedContent, - messageId: messageId, - ), + await sendCipherText( + groupMember.contactId, + encryptedContent, + messageId: messageId, + blocking: false, ); } } @@ -252,6 +252,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText( int contactId, pb.EncryptedContent encryptedContent, { bool onlyReturnEncryptedData = false, + bool blocking = true, String? messageId, }) async { encryptedContent.senderProfileCounter = Int64(gUser.avatarCounter); @@ -270,10 +271,15 @@ Future<(Uint8List, Uint8List?)?> sendCipherText( ); if (receipt != null) { - return tryToSendCompleteMessage( + final tmp = tryToSendCompleteMessage( receipt: receipt, onlyReturnEncryptedData: onlyReturnEncryptedData, + blocking: blocking, ); + if (!blocking) { + return null; + } + return tmp; } return null; } @@ -302,6 +308,7 @@ Future<void> notifyContactAboutOpeningMessage( timestamp: Int64(actionAt.millisecondsSinceEpoch), ), ), + blocking: false, ); for (final messageId in messageOtherIds) { await twonlyDB.messagesDao.updateMessageId( diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index c7dbfa1..870a06b 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:camera/camera.dart'; import 'package:clock/clock.dart'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_android_volume_keydown/flutter_android_volume_keydown.dart'; @@ -352,7 +353,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { sendToGroup: widget.sendToGroup, mediaFileService: mediaFileService, mainCameraController: mc, - previewLink: mc.sharedLinkForPreview, + // previewLink: mc.sharedLinkForPreview, + previewLink: kDebugMode + ? Uri.parse( + 'https://mastodon.social/@islieb/115883317936171927', + ) + : mc.sharedLinkForPreview, ), transitionsBuilder: (context, animation, secondaryAnimation, child) { return child; diff --git a/lib/src/views/camera/share_image_contact_selection.view.dart b/lib/src/views/camera/share_image_contact_selection.view.dart index 0ecbbab..7cdf082 100644 --- a/lib/src/views/camera/share_image_contact_selection.view.dart +++ b/lib/src/views/camera/share_image_contact_selection.view.dart @@ -9,6 +9,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; @@ -24,12 +25,14 @@ class ShareImageView extends StatefulWidget { required this.updateSelectedGroupIds, required this.mediaStoreFuture, required this.mediaFileService, + required this.additionalData, super.key, }); final HashSet<String> selectedGroupIds; final void Function(String, bool) updateSelectedGroupIds; final Future<Uint8List?>? mediaStoreFuture; final MediaFileService mediaFileService; + final AdditionalMessageData? additionalData; @override State<ShareImageView> createState() => _ShareImageView(); @@ -286,6 +289,7 @@ class _ShareImageView extends State<ShareImageView> { await insertMediaFileInMessagesTable( widget.mediaFileService, widget.selectedGroupIds.toList(), + additionalData: widget.additionalData, ); if (context.mounted) { diff --git a/lib/src/views/camera/share_image_editor.view.dart b/lib/src/views/camera/share_image_editor.view.dart index 87dcecb..a27f826 100644 --- a/lib/src/views/camera/share_image_editor.view.dart +++ b/lib/src/views/camera/share_image_editor.view.dart @@ -11,6 +11,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; @@ -420,6 +421,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> { updateSelectedGroupIds: updateSelectedGroupIds, mediaStoreFuture: mediaStoreFuture, mediaFileService: mediaService, + additionalData: getAdditionalData(), ), ), ) as bool?; @@ -545,6 +547,18 @@ class _ShareImageEditorView extends State<ShareImageEditorView> { }); } + AdditionalMessageData? getAdditionalData() { + AdditionalMessageData? additionalData; + + if (widget.previewLink != null) { + additionalData = AdditionalMessageData( + type: AdditionalMessageData_Type.LINK, + link: widget.previewLink.toString(), + ); + } + return additionalData; + } + Future<void> sendImageToSinglePerson() async { if (sendingOrLoadingImage) return; setState(() { @@ -560,6 +574,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> { await insertMediaFileInMessagesTable( mediaService, [widget.sendToGroup!.groupId], + additionalData: getAdditionalData(), ); if (mounted) { diff --git a/lib/src/views/camera/share_image_editor/layer_data.dart b/lib/src/views/camera/share_image_editor/layer_data.dart index 504df26..ffee88a 100755 --- a/lib/src/views/camera/share_image_editor/layer_data.dart +++ b/lib/src/views/camera/share_image_editor/layer_data.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hand_signature/signature.dart'; import 'package:twonly/src/views/camera/share_image_editor/image_item.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; /// Layer class with some common properties class Layer { @@ -41,6 +42,8 @@ class LinkPreviewLayerData extends Layer { required this.link, }); Uri link; + Metadata? metadata; + bool error = false; } class FilterLayerData extends Layer { diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview.layer.dart b/lib/src/views/camera/share_image_editor/layers/link_preview.layer.dart index 029a536..c34065f 100644 --- a/lib/src/views/camera/share_image_editor/layers/link_preview.layer.dart +++ b/lib/src/views/camera/share_image_editor/layers/link_preview.layer.dart @@ -1,5 +1,12 @@ import 'package:flutter/material.dart'; import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/cards/custom.card.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/cards/twitter.card.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/cards/youtube.card.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; +import 'package:twonly/src/views/components/loader.dart'; class LinkPreviewLayer extends StatefulWidget { const LinkPreviewLayer({ @@ -15,11 +22,48 @@ class LinkPreviewLayer extends StatefulWidget { } class _LinkPreviewLayerState extends State<LinkPreviewLayer> { + Metadata? metadata; + + @override + void initState() { + initAsync(); + super.initState(); + } + + Future<void> initAsync() async { + if (widget.layerData.metadata == null) { + widget.layerData.metadata = + await getMetadata(widget.layerData.link.toString()); + if (widget.layerData.metadata == null) { + widget.layerData.error = true; + } + if (mounted) setState(() {}); + } + } + @override Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.zero, - child: Text(widget.layerData.link.toString()), + if (widget.layerData.error) { + return Container(); + } + final meta = widget.layerData.metadata; + late Widget child; + if (meta == null) { + child = const ThreeRotatingDots(size: 30); + } else if (meta.title == null) { + return Container(); + } else if (meta.vendor == Vendor.mastodonSocialMediaPosting) { + child = MastodonPostCard(info: meta); + } else if (meta.vendor == Vendor.twitterPosting) { + child = TwitterPostCard(info: meta); + } else if (meta.vendor == Vendor.youtubeVideo) { + child = YouTubePostCard(info: meta); + } else { + child = CustomLinkCard(info: meta); + } + + return Center( + child: child, ); } } diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/cards/custom.card.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/cards/custom.card.dart new file mode 100644 index 0000000..f7d7e23 --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/cards/custom.card.dart @@ -0,0 +1,87 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; + +class CustomLinkCard extends StatelessWidget { + const CustomLinkCard({required this.info, super.key}); + final Metadata info; + + @override + Widget build(BuildContext context) { + return FractionallySizedBox( + widthFactor: 0.8, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFF1E1E1E), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.white10), + ), + clipBehavior: Clip.antiAlias, + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + Uri.parse(info.url).host.toUpperCase(), + style: TextStyle( + color: context.color.primary, + fontSize: 10, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + ), + ), + const SizedBox(height: 4), + Text( + substringBy(info.title ?? 'Link Preview', 35), + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + if (info.desc != null && info.desc != 'null') ...[ + const SizedBox(height: 6), + Text( + substringBy(info.desc!, 500), + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Color(0xFFB0B0B0), + fontSize: 13, + height: 1.4, + ), + ), + ], + if (info.image != null && info.image != 'null') + Padding( + padding: const EdgeInsets.only(top: 12), + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: CachedNetworkImage( + imageUrl: info.image!, + fit: BoxFit.cover, + width: double.infinity, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart new file mode 100644 index 0000000..f69d78e --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart @@ -0,0 +1,110 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; +import 'package:twonly/src/views/components/loader.dart'; + +class MastodonPostCard extends StatelessWidget { + const MastodonPostCard({required this.info, super.key}); + final Metadata info; + + @override + Widget build(BuildContext context) { + const backgroundColor = Color(0xFF282C37); + const secondaryTextColor = Color(0xFF9BA3AF); + const accentColor = Color(0xFF6364FF); + + return FractionallySizedBox( + widthFactor: 0.8, + child: Container( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + const FaIcon( + FontAwesomeIcons.mastodon, + color: accentColor, + size: 20, + ), + const SizedBox(width: 10), + Text( + substringBy(info.title ?? 'Mastodon User', 37), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 15, + ), + ), + ], + ), + const SizedBox(height: 4), + if (info.desc != null && info.desc != 'null') + Text( + substringBy(info.desc!, 1000), + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + if (info.image != null && info.image != 'null') + Padding( + padding: const EdgeInsets.only(top: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 250), + child: CachedNetworkImage( + imageUrl: info.image!, + fit: BoxFit.contain, + width: double.infinity, + placeholder: (context, url) => Container( + height: 150, + color: Colors.black12, + child: const Center( + child: ThreeRotatingDots(size: 20), + ), + ), + ), + ), + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildAction( + Icons.repeat, + '${info.shareAction ?? 0}', + secondaryTextColor, + ), + const SizedBox(width: 20), + _buildAction( + Icons.star_border, + '${info.likeAction ?? 0}', + secondaryTextColor, + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildAction(IconData icon, String count, Color color) { + return Row( + children: [ + Icon(icon, size: 18, color: color), + if (count.isNotEmpty && count != '0') ...[ + const SizedBox(width: 5), + Text(count, style: TextStyle(color: color, fontSize: 13)), + ], + ], + ); + } +} diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/cards/twitter.card.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/cards/twitter.card.dart new file mode 100644 index 0000000..7216b3f --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/cards/twitter.card.dart @@ -0,0 +1,101 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +// Assuming the same Metadata import structure +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; + +class TwitterPostCard extends StatelessWidget { + const TwitterPostCard({required this.info, super.key}); + final Metadata info; + + @override + Widget build(BuildContext context) { + // Classic Twitter Brand Colors + const twitterBlue = Color(0xFF1DA1F2); + const backgroundWhite = Colors.white; + const primaryText = Color(0xFF14171A); + const borderColor = Color(0xFFE1E8ED); + + return FractionallySizedBox( + widthFactor: 0.9, // Twitter cards often feel a bit wider + child: Container( + decoration: BoxDecoration( + color: backgroundWhite, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: borderColor), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + const FaIcon( + FontAwesomeIcons.twitter, + color: twitterBlue, + size: 22, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + substringBy(info.title ?? 'Twitter User', 37), + style: const TextStyle( + color: primaryText, + fontWeight: FontWeight.w800, + fontSize: 16, + letterSpacing: -0.5, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 8), + if (info.desc != null && info.desc != 'null') + Text( + substringBy(info.desc!, 1000), + style: const TextStyle( + color: primaryText, + fontSize: 15, + height: 1.3, + ), + ), + if (info.image != null && info.image != 'null') + Padding( + padding: const EdgeInsets.only(top: 12), + child: ClipRRect( + borderRadius: BorderRadius.circular(14), + child: Container( + decoration: BoxDecoration( + border: Border.all(color: borderColor), + borderRadius: BorderRadius.circular(14), + ), + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: CachedNetworkImage( + imageUrl: info.image!, + fit: BoxFit.cover, + width: double.infinity, + placeholder: (context, url) => Container( + height: 150, + color: const Color(0xFFF5F8FA), + child: const Center( + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(twitterBlue), + ), + ), + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/cards/youtube.card.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/cards/youtube.card.dart new file mode 100644 index 0000000..c9bbb85 --- /dev/null +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/cards/youtube.card.dart @@ -0,0 +1,90 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; + +class YouTubePostCard extends StatelessWidget { + const YouTubePostCard({required this.info, super.key}); + final Metadata info; + + @override + Widget build(BuildContext context) { + const ytBlack = Color(0xFF0F0F0F); + const ytWhite = Colors.white; + const ytRed = Color.fromARGB(255, 255, 1, 51); + + return FractionallySizedBox( + widthFactor: 0.8, + child: Container( + decoration: BoxDecoration( + color: ytBlack, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + alignment: Alignment.bottomRight, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: AspectRatio( + aspectRatio: 16 / 9, + child: CachedNetworkImage( + imageUrl: info.image ?? '', + fit: BoxFit.cover, + placeholder: (context, url) => + Container(color: Colors.white10), + errorWidget: (context, url, error) => const ColoredBox( + color: Colors.white10, + child: Icon( + Icons.play_circle_outline, + color: ytWhite, + size: 50, + ), + ), + ), + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const FaIcon( + FontAwesomeIcons.youtube, + color: ytRed, + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + substringBy(info.title ?? 'Video Title', 600), + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: ytWhite, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.2, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart b/lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart index 24900de..ef5ea52 100644 --- a/lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart +++ b/lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart @@ -18,7 +18,7 @@ class FriendlyMessageTime extends StatelessWidget { padding: const EdgeInsets.only(left: 6), child: Row( children: [ - if (message.modifiedAt != null) + if (message.modifiedAt != null && !message.isDeletedFromSender) Padding( padding: const EdgeInsets.only(right: 5), child: SizedBox( diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index 7760c61..aa7f821 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -21,6 +21,7 @@ import 'package:twonly/src/services/notifications/background.notifications.dart' import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/camera_send_to.view.dart'; +import 'package:twonly/src/views/chats/media_viewer_components/additional_message_content.dart'; import 'package:twonly/src/views/chats/media_viewer_components/reaction_buttons.component.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/loader.dart'; @@ -493,6 +494,19 @@ class _MediaViewerViewState extends State<MediaViewerView> { ); } + Widget _loader() { + return Center( + child: SizedBox( + height: 60, + width: 60, + child: ThreeRotatingDots( + size: 40, + color: context.color.primary, + ), + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -500,17 +514,7 @@ class _MediaViewerViewState extends State<MediaViewerView> { child: Stack( fit: StackFit.expand, children: [ - if (_showDownloadingLoader) - Center( - child: SizedBox( - height: 60, - width: 60, - child: ThreeRotatingDots( - size: 40, - color: context.color.primary, - ), - ), - ), + if (_showDownloadingLoader) _loader(), if ((currentMedia != null || videoController != null) && (canBeSeenUntil == null || progress >= 0)) GestureDetector( @@ -549,9 +553,7 @@ class _MediaViewerViewState extends State<MediaViewerView> { if (displayTwonlyPresent) Positioned.fill( child: GestureDetector( - onTap: () { - loadCurrentMediaFile(showTwonly: true); - }, + onTap: () => loadCurrentMediaFile(showTwonly: true), child: Column( children: [ Expanded( @@ -575,26 +577,14 @@ class _MediaViewerViewState extends State<MediaViewerView> { IconButton( icon: const Icon(Icons.close, size: 30), color: Colors.white, - onPressed: () async { - Navigator.pop(context); - }, + onPressed: () => Navigator.pop(context), ), ], ), ), if (currentMedia != null && currentMedia?.mediaFile.downloadState != DownloadState.ready) - const Positioned.fill( - child: Center( - child: SizedBox( - height: 60, - width: 60, - child: CircularProgressIndicator( - strokeWidth: 6, - ), - ), - ), - ), + Positioned.fill(child: _loader()), if (canBeSeenUntil != null || progress >= 0) Positioned( right: 20, @@ -718,6 +708,8 @@ class _MediaViewerViewState extends State<MediaViewerView> { Positioned.fill( child: EmojiFloatWidget(key: emojiKey), ), + if (currentMessage != null) + AdditionalMessageContent(currentMessage!), ], ), ), diff --git a/lib/src/views/chats/media_viewer_components/additional_message_content.dart b/lib/src/views/chats/media_viewer_components/additional_message_content.dart new file mode 100644 index 0000000..7f810c2 --- /dev/null +++ b/lib/src/views/chats/media_viewer_components/additional_message_content.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class AdditionalMessageContent extends StatelessWidget { + const AdditionalMessageContent(this.message, {super.key}); + + final Message message; + + @override + Widget build(BuildContext context) { + if (message.additionalMessageData == null) return Container(); + try { + final data = + AdditionalMessageData.fromBuffer(message.additionalMessageData!); + + switch (data.type) { + case AdditionalMessageData_Type.LINK: + if (!data.link.startsWith('http://') && + !data.link.startsWith('https://')) { + return Container(); + } + return Positioned( + bottom: 150, + right: 0, + left: 0, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + FilledButton.icon( + icon: const FaIcon(FontAwesomeIcons.shareFromSquare), + onPressed: () => launchUrlString(data.link), + label: Text( + substringBy( + data.link + .replaceAll('http://', '') + .replaceAll('https://', ''), + 30, + ), + ), + ), + ], + ), + ); + default: + } + // ignore: empty_catches + } catch (e) {} + return Container(); + } +} diff --git a/scripts/generate_proto.sh b/scripts/generate_proto.sh index 20fd3b6..40880f9 100755 --- a/scripts/generate_proto.sh +++ b/scripts/generate_proto.sh @@ -15,6 +15,7 @@ protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "backup.proto" protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "messages.proto" protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "groups.proto" protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "qr.proto" +protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "data.proto" protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "push_notification.proto" protoc --proto_path="$CLIENT_DIR" --swift_out="./ios/NotificationService/" "push_notification.proto" diff --git a/test/drift/twonly_db/generated/schema.dart b/test/drift/twonly_db/generated/schema.dart index d59002b..87de919 100644 --- a/test/drift/twonly_db/generated/schema.dart +++ b/test/drift/twonly_db/generated/schema.dart @@ -9,6 +9,7 @@ import 'schema_v3.dart' as v3; import 'schema_v4.dart' as v4; import 'schema_v5.dart' as v5; import 'schema_v6.dart' as v6; +import 'schema_v7.dart' as v7; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -26,10 +27,12 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v5.DatabaseAtV5(db); case 6: return v6.DatabaseAtV6(db); + case 7: + return v7.DatabaseAtV7(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2, 3, 4, 5, 6]; + static const versions = const [1, 2, 3, 4, 5, 6, 7]; } diff --git a/test/drift/twonly_db/generated/schema_v7.dart b/test/drift/twonly_db/generated/schema_v7.dart new file mode 100644 index 0000000..ef2ae77 --- /dev/null +++ b/test/drift/twonly_db/generated/schema_v7.dart @@ -0,0 +1,6582 @@ +// dart format width=80 +import 'dart:typed_data' as i2; +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class Contacts extends Table with TableInfo<Contacts, ContactsData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Contacts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn<int> userId = GeneratedColumn<int>( + 'user_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn<String> username = GeneratedColumn<String>( + 'username', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn<String> displayName = GeneratedColumn<String>( + 'display_name', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn<String> nickName = GeneratedColumn<String>( + 'nick_name', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn<i2.Uint8List> avatarSvgCompressed = + GeneratedColumn<i2.Uint8List>('avatar_svg_compressed', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + late final GeneratedColumn<int> senderProfileCounter = GeneratedColumn<int>( + 'sender_profile_counter', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<bool> accepted = GeneratedColumn<bool>( + 'accepted', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("accepted" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<bool> deletedByUser = GeneratedColumn<bool>( + 'deleted_by_user', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("deleted_by_user" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<bool> requested = GeneratedColumn<bool>( + 'requested', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("requested" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<bool> blocked = GeneratedColumn<bool>( + 'blocked', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("blocked" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<bool> verified = GeneratedColumn<bool>( + 'verified', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("verified" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<bool> accountDeleted = GeneratedColumn<bool>( + 'account_deleted', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("account_deleted" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); + @override + List<GeneratedColumn> get $columns => [ + userId, + username, + displayName, + nickName, + avatarSvgCompressed, + senderProfileCounter, + accepted, + deletedByUser, + requested, + blocked, + verified, + accountDeleted, + createdAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'contacts'; + @override + Set<GeneratedColumn> get $primaryKey => {userId}; + @override + ContactsData map(Map<String, dynamic> data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ContactsData( + userId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}user_id'])!, + username: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}username'])!, + displayName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}display_name']), + nickName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}nick_name']), + avatarSvgCompressed: attachedDatabase.typeMapping.read( + DriftSqlType.blob, data['${effectivePrefix}avatar_svg_compressed']), + senderProfileCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}sender_profile_counter'])!, + accepted: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}accepted'])!, + deletedByUser: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}deleted_by_user'])!, + requested: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}requested'])!, + blocked: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}blocked'])!, + verified: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}verified'])!, + accountDeleted: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}account_deleted'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + Contacts createAlias(String alias) { + return Contacts(attachedDatabase, alias); + } +} + +class ContactsData extends DataClass implements Insertable<ContactsData> { + final int userId; + final String username; + final String? displayName; + final String? nickName; + final i2.Uint8List? avatarSvgCompressed; + final int senderProfileCounter; + final bool accepted; + final bool deletedByUser; + final bool requested; + final bool blocked; + final bool verified; + final bool accountDeleted; + final DateTime createdAt; + const ContactsData( + {required this.userId, + required this.username, + this.displayName, + this.nickName, + this.avatarSvgCompressed, + required this.senderProfileCounter, + required this.accepted, + required this.deletedByUser, + required this.requested, + required this.blocked, + required this.verified, + required this.accountDeleted, + required this.createdAt}); + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + map['user_id'] = Variable<int>(userId); + map['username'] = Variable<String>(username); + if (!nullToAbsent || displayName != null) { + map['display_name'] = Variable<String>(displayName); + } + if (!nullToAbsent || nickName != null) { + map['nick_name'] = Variable<String>(nickName); + } + if (!nullToAbsent || avatarSvgCompressed != null) { + map['avatar_svg_compressed'] = + Variable<i2.Uint8List>(avatarSvgCompressed); + } + map['sender_profile_counter'] = Variable<int>(senderProfileCounter); + map['accepted'] = Variable<bool>(accepted); + map['deleted_by_user'] = Variable<bool>(deletedByUser); + map['requested'] = Variable<bool>(requested); + map['blocked'] = Variable<bool>(blocked); + map['verified'] = Variable<bool>(verified); + map['account_deleted'] = Variable<bool>(accountDeleted); + map['created_at'] = Variable<DateTime>(createdAt); + return map; + } + + ContactsCompanion toCompanion(bool nullToAbsent) { + return ContactsCompanion( + userId: Value(userId), + username: Value(username), + displayName: displayName == null && nullToAbsent + ? const Value.absent() + : Value(displayName), + nickName: nickName == null && nullToAbsent + ? const Value.absent() + : Value(nickName), + avatarSvgCompressed: avatarSvgCompressed == null && nullToAbsent + ? const Value.absent() + : Value(avatarSvgCompressed), + senderProfileCounter: Value(senderProfileCounter), + accepted: Value(accepted), + deletedByUser: Value(deletedByUser), + requested: Value(requested), + blocked: Value(blocked), + verified: Value(verified), + accountDeleted: Value(accountDeleted), + createdAt: Value(createdAt), + ); + } + + factory ContactsData.fromJson(Map<String, dynamic> json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ContactsData( + userId: serializer.fromJson<int>(json['userId']), + username: serializer.fromJson<String>(json['username']), + displayName: serializer.fromJson<String?>(json['displayName']), + nickName: serializer.fromJson<String?>(json['nickName']), + avatarSvgCompressed: + serializer.fromJson<i2.Uint8List?>(json['avatarSvgCompressed']), + senderProfileCounter: + serializer.fromJson<int>(json['senderProfileCounter']), + accepted: serializer.fromJson<bool>(json['accepted']), + deletedByUser: serializer.fromJson<bool>(json['deletedByUser']), + requested: serializer.fromJson<bool>(json['requested']), + blocked: serializer.fromJson<bool>(json['blocked']), + verified: serializer.fromJson<bool>(json['verified']), + accountDeleted: serializer.fromJson<bool>(json['accountDeleted']), + createdAt: serializer.fromJson<DateTime>(json['createdAt']), + ); + } + @override + Map<String, dynamic> toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return <String, dynamic>{ + 'userId': serializer.toJson<int>(userId), + 'username': serializer.toJson<String>(username), + 'displayName': serializer.toJson<String?>(displayName), + 'nickName': serializer.toJson<String?>(nickName), + 'avatarSvgCompressed': + serializer.toJson<i2.Uint8List?>(avatarSvgCompressed), + 'senderProfileCounter': serializer.toJson<int>(senderProfileCounter), + 'accepted': serializer.toJson<bool>(accepted), + 'deletedByUser': serializer.toJson<bool>(deletedByUser), + 'requested': serializer.toJson<bool>(requested), + 'blocked': serializer.toJson<bool>(blocked), + 'verified': serializer.toJson<bool>(verified), + 'accountDeleted': serializer.toJson<bool>(accountDeleted), + 'createdAt': serializer.toJson<DateTime>(createdAt), + }; + } + + ContactsData copyWith( + {int? userId, + String? username, + Value<String?> displayName = const Value.absent(), + Value<String?> nickName = const Value.absent(), + Value<i2.Uint8List?> avatarSvgCompressed = const Value.absent(), + int? senderProfileCounter, + bool? accepted, + bool? deletedByUser, + bool? requested, + bool? blocked, + bool? verified, + bool? accountDeleted, + DateTime? createdAt}) => + ContactsData( + userId: userId ?? this.userId, + username: username ?? this.username, + displayName: displayName.present ? displayName.value : this.displayName, + nickName: nickName.present ? nickName.value : this.nickName, + avatarSvgCompressed: avatarSvgCompressed.present + ? avatarSvgCompressed.value + : this.avatarSvgCompressed, + senderProfileCounter: senderProfileCounter ?? this.senderProfileCounter, + accepted: accepted ?? this.accepted, + deletedByUser: deletedByUser ?? this.deletedByUser, + requested: requested ?? this.requested, + blocked: blocked ?? this.blocked, + verified: verified ?? this.verified, + accountDeleted: accountDeleted ?? this.accountDeleted, + createdAt: createdAt ?? this.createdAt, + ); + ContactsData copyWithCompanion(ContactsCompanion data) { + return ContactsData( + userId: data.userId.present ? data.userId.value : this.userId, + username: data.username.present ? data.username.value : this.username, + displayName: + data.displayName.present ? data.displayName.value : this.displayName, + nickName: data.nickName.present ? data.nickName.value : this.nickName, + avatarSvgCompressed: data.avatarSvgCompressed.present + ? data.avatarSvgCompressed.value + : this.avatarSvgCompressed, + senderProfileCounter: data.senderProfileCounter.present + ? data.senderProfileCounter.value + : this.senderProfileCounter, + accepted: data.accepted.present ? data.accepted.value : this.accepted, + deletedByUser: data.deletedByUser.present + ? data.deletedByUser.value + : this.deletedByUser, + requested: data.requested.present ? data.requested.value : this.requested, + blocked: data.blocked.present ? data.blocked.value : this.blocked, + verified: data.verified.present ? data.verified.value : this.verified, + accountDeleted: data.accountDeleted.present + ? data.accountDeleted.value + : this.accountDeleted, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('ContactsData(') + ..write('userId: $userId, ') + ..write('username: $username, ') + ..write('displayName: $displayName, ') + ..write('nickName: $nickName, ') + ..write('avatarSvgCompressed: $avatarSvgCompressed, ') + ..write('senderProfileCounter: $senderProfileCounter, ') + ..write('accepted: $accepted, ') + ..write('deletedByUser: $deletedByUser, ') + ..write('requested: $requested, ') + ..write('blocked: $blocked, ') + ..write('verified: $verified, ') + ..write('accountDeleted: $accountDeleted, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + userId, + username, + displayName, + nickName, + $driftBlobEquality.hash(avatarSvgCompressed), + senderProfileCounter, + accepted, + deletedByUser, + requested, + blocked, + verified, + accountDeleted, + createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ContactsData && + other.userId == this.userId && + other.username == this.username && + other.displayName == this.displayName && + other.nickName == this.nickName && + $driftBlobEquality.equals( + other.avatarSvgCompressed, this.avatarSvgCompressed) && + other.senderProfileCounter == this.senderProfileCounter && + other.accepted == this.accepted && + other.deletedByUser == this.deletedByUser && + other.requested == this.requested && + other.blocked == this.blocked && + other.verified == this.verified && + other.accountDeleted == this.accountDeleted && + other.createdAt == this.createdAt); +} + +class ContactsCompanion extends UpdateCompanion<ContactsData> { + final Value<int> userId; + final Value<String> username; + final Value<String?> displayName; + final Value<String?> nickName; + final Value<i2.Uint8List?> avatarSvgCompressed; + final Value<int> senderProfileCounter; + final Value<bool> accepted; + final Value<bool> deletedByUser; + final Value<bool> requested; + final Value<bool> blocked; + final Value<bool> verified; + final Value<bool> accountDeleted; + final Value<DateTime> createdAt; + const ContactsCompanion({ + this.userId = const Value.absent(), + this.username = const Value.absent(), + this.displayName = const Value.absent(), + this.nickName = const Value.absent(), + this.avatarSvgCompressed = const Value.absent(), + this.senderProfileCounter = const Value.absent(), + this.accepted = const Value.absent(), + this.deletedByUser = const Value.absent(), + this.requested = const Value.absent(), + this.blocked = const Value.absent(), + this.verified = const Value.absent(), + this.accountDeleted = const Value.absent(), + this.createdAt = const Value.absent(), + }); + ContactsCompanion.insert({ + this.userId = const Value.absent(), + required String username, + this.displayName = const Value.absent(), + this.nickName = const Value.absent(), + this.avatarSvgCompressed = const Value.absent(), + this.senderProfileCounter = const Value.absent(), + this.accepted = const Value.absent(), + this.deletedByUser = const Value.absent(), + this.requested = const Value.absent(), + this.blocked = const Value.absent(), + this.verified = const Value.absent(), + this.accountDeleted = const Value.absent(), + this.createdAt = const Value.absent(), + }) : username = Value(username); + static Insertable<ContactsData> custom({ + Expression<int>? userId, + Expression<String>? username, + Expression<String>? displayName, + Expression<String>? nickName, + Expression<i2.Uint8List>? avatarSvgCompressed, + Expression<int>? senderProfileCounter, + Expression<bool>? accepted, + Expression<bool>? deletedByUser, + Expression<bool>? requested, + Expression<bool>? blocked, + Expression<bool>? verified, + Expression<bool>? accountDeleted, + Expression<DateTime>? createdAt, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (username != null) 'username': username, + if (displayName != null) 'display_name': displayName, + if (nickName != null) 'nick_name': nickName, + if (avatarSvgCompressed != null) + 'avatar_svg_compressed': avatarSvgCompressed, + if (senderProfileCounter != null) + 'sender_profile_counter': senderProfileCounter, + if (accepted != null) 'accepted': accepted, + if (deletedByUser != null) 'deleted_by_user': deletedByUser, + if (requested != null) 'requested': requested, + if (blocked != null) 'blocked': blocked, + if (verified != null) 'verified': verified, + if (accountDeleted != null) 'account_deleted': accountDeleted, + if (createdAt != null) 'created_at': createdAt, + }); + } + + ContactsCompanion copyWith( + {Value<int>? userId, + Value<String>? username, + Value<String?>? displayName, + Value<String?>? nickName, + Value<i2.Uint8List?>? avatarSvgCompressed, + Value<int>? senderProfileCounter, + Value<bool>? accepted, + Value<bool>? deletedByUser, + Value<bool>? requested, + Value<bool>? blocked, + Value<bool>? verified, + Value<bool>? accountDeleted, + Value<DateTime>? createdAt}) { + return ContactsCompanion( + userId: userId ?? this.userId, + username: username ?? this.username, + displayName: displayName ?? this.displayName, + nickName: nickName ?? this.nickName, + avatarSvgCompressed: avatarSvgCompressed ?? this.avatarSvgCompressed, + senderProfileCounter: senderProfileCounter ?? this.senderProfileCounter, + accepted: accepted ?? this.accepted, + deletedByUser: deletedByUser ?? this.deletedByUser, + requested: requested ?? this.requested, + blocked: blocked ?? this.blocked, + verified: verified ?? this.verified, + accountDeleted: accountDeleted ?? this.accountDeleted, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + if (userId.present) { + map['user_id'] = Variable<int>(userId.value); + } + if (username.present) { + map['username'] = Variable<String>(username.value); + } + if (displayName.present) { + map['display_name'] = Variable<String>(displayName.value); + } + if (nickName.present) { + map['nick_name'] = Variable<String>(nickName.value); + } + if (avatarSvgCompressed.present) { + map['avatar_svg_compressed'] = + Variable<i2.Uint8List>(avatarSvgCompressed.value); + } + if (senderProfileCounter.present) { + map['sender_profile_counter'] = Variable<int>(senderProfileCounter.value); + } + if (accepted.present) { + map['accepted'] = Variable<bool>(accepted.value); + } + if (deletedByUser.present) { + map['deleted_by_user'] = Variable<bool>(deletedByUser.value); + } + if (requested.present) { + map['requested'] = Variable<bool>(requested.value); + } + if (blocked.present) { + map['blocked'] = Variable<bool>(blocked.value); + } + if (verified.present) { + map['verified'] = Variable<bool>(verified.value); + } + if (accountDeleted.present) { + map['account_deleted'] = Variable<bool>(accountDeleted.value); + } + if (createdAt.present) { + map['created_at'] = Variable<DateTime>(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ContactsCompanion(') + ..write('userId: $userId, ') + ..write('username: $username, ') + ..write('displayName: $displayName, ') + ..write('nickName: $nickName, ') + ..write('avatarSvgCompressed: $avatarSvgCompressed, ') + ..write('senderProfileCounter: $senderProfileCounter, ') + ..write('accepted: $accepted, ') + ..write('deletedByUser: $deletedByUser, ') + ..write('requested: $requested, ') + ..write('blocked: $blocked, ') + ..write('verified: $verified, ') + ..write('accountDeleted: $accountDeleted, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class Groups extends Table with TableInfo<Groups, GroupsData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Groups(this.attachedDatabase, [this._alias]); + late final GeneratedColumn<String> groupId = GeneratedColumn<String>( + 'group_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn<bool> isGroupAdmin = GeneratedColumn<bool>( + 'is_group_admin', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_group_admin" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<bool> isDirectChat = GeneratedColumn<bool>( + 'is_direct_chat', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_direct_chat" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<bool> pinned = GeneratedColumn<bool>( + 'pinned', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("pinned" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<bool> archived = GeneratedColumn<bool>( + 'archived', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("archived" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<bool> joinedGroup = GeneratedColumn<bool>( + 'joined_group', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("joined_group" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<bool> leftGroup = GeneratedColumn<bool>( + 'left_group', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("left_group" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<bool> deletedContent = GeneratedColumn<bool>( + 'deleted_content', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("deleted_content" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<int> stateVersionId = GeneratedColumn<int>( + 'state_version_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<i2.Uint8List> stateEncryptionKey = + GeneratedColumn<i2.Uint8List>('state_encryption_key', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + late final GeneratedColumn<i2.Uint8List> myGroupPrivateKey = + GeneratedColumn<i2.Uint8List>('my_group_private_key', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + late final GeneratedColumn<String> groupName = GeneratedColumn<String>( + 'group_name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn<String> draftMessage = GeneratedColumn<String>( + 'draft_message', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn<int> totalMediaCounter = GeneratedColumn<int>( + 'total_media_counter', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<bool> alsoBestFriend = GeneratedColumn<bool>( + 'also_best_friend', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("also_best_friend" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<int> deleteMessagesAfterMilliseconds = + GeneratedColumn<int>( + 'delete_messages_after_milliseconds', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('86400000')); + late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); + late final GeneratedColumn<DateTime> lastMessageSend = + GeneratedColumn<DateTime>('last_message_send', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn<DateTime> lastMessageReceived = + GeneratedColumn<DateTime>('last_message_received', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn<DateTime> lastFlameCounterChange = + GeneratedColumn<DateTime>('last_flame_counter_change', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn<DateTime> lastFlameSync = + GeneratedColumn<DateTime>('last_flame_sync', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn<int> flameCounter = GeneratedColumn<int>( + 'flame_counter', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<int> maxFlameCounter = GeneratedColumn<int>( + 'max_flame_counter', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<DateTime> maxFlameCounterFrom = + GeneratedColumn<DateTime>('max_flame_counter_from', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn<DateTime> lastMessageExchange = + GeneratedColumn<DateTime>('last_message_exchange', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); + @override + List<GeneratedColumn> get $columns => [ + groupId, + isGroupAdmin, + isDirectChat, + pinned, + archived, + joinedGroup, + leftGroup, + deletedContent, + stateVersionId, + stateEncryptionKey, + myGroupPrivateKey, + groupName, + draftMessage, + totalMediaCounter, + alsoBestFriend, + deleteMessagesAfterMilliseconds, + createdAt, + lastMessageSend, + lastMessageReceived, + lastFlameCounterChange, + lastFlameSync, + flameCounter, + maxFlameCounter, + maxFlameCounterFrom, + lastMessageExchange + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'groups'; + @override + Set<GeneratedColumn> get $primaryKey => {groupId}; + @override + GroupsData map(Map<String, dynamic> data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return GroupsData( + groupId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_id'])!, + isGroupAdmin: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_group_admin'])!, + isDirectChat: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_direct_chat'])!, + pinned: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!, + archived: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}archived'])!, + joinedGroup: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}joined_group'])!, + leftGroup: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}left_group'])!, + deletedContent: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}deleted_content'])!, + stateVersionId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}state_version_id'])!, + stateEncryptionKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, data['${effectivePrefix}state_encryption_key']), + myGroupPrivateKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, data['${effectivePrefix}my_group_private_key']), + groupName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_name'])!, + draftMessage: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}draft_message']), + totalMediaCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}total_media_counter'])!, + alsoBestFriend: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}also_best_friend'])!, + deleteMessagesAfterMilliseconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}delete_messages_after_milliseconds'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + lastMessageSend: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}last_message_send']), + lastMessageReceived: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_message_received']), + lastFlameCounterChange: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_flame_counter_change']), + lastFlameSync: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}last_flame_sync']), + flameCounter: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}flame_counter'])!, + maxFlameCounter: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}max_flame_counter'])!, + maxFlameCounterFrom: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}max_flame_counter_from']), + lastMessageExchange: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_message_exchange'])!, + ); + } + + @override + Groups createAlias(String alias) { + return Groups(attachedDatabase, alias); + } +} + +class GroupsData extends DataClass implements Insertable<GroupsData> { + final String groupId; + final bool isGroupAdmin; + final bool isDirectChat; + final bool pinned; + final bool archived; + final bool joinedGroup; + final bool leftGroup; + final bool deletedContent; + final int stateVersionId; + final i2.Uint8List? stateEncryptionKey; + final i2.Uint8List? myGroupPrivateKey; + final String groupName; + final String? draftMessage; + final int totalMediaCounter; + final bool alsoBestFriend; + final int deleteMessagesAfterMilliseconds; + final DateTime createdAt; + final DateTime? lastMessageSend; + final DateTime? lastMessageReceived; + final DateTime? lastFlameCounterChange; + final DateTime? lastFlameSync; + final int flameCounter; + final int maxFlameCounter; + final DateTime? maxFlameCounterFrom; + final DateTime lastMessageExchange; + const GroupsData( + {required this.groupId, + required this.isGroupAdmin, + required this.isDirectChat, + required this.pinned, + required this.archived, + required this.joinedGroup, + required this.leftGroup, + required this.deletedContent, + required this.stateVersionId, + this.stateEncryptionKey, + this.myGroupPrivateKey, + required this.groupName, + this.draftMessage, + required this.totalMediaCounter, + required this.alsoBestFriend, + required this.deleteMessagesAfterMilliseconds, + required this.createdAt, + this.lastMessageSend, + this.lastMessageReceived, + this.lastFlameCounterChange, + this.lastFlameSync, + required this.flameCounter, + required this.maxFlameCounter, + this.maxFlameCounterFrom, + required this.lastMessageExchange}); + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + map['group_id'] = Variable<String>(groupId); + map['is_group_admin'] = Variable<bool>(isGroupAdmin); + map['is_direct_chat'] = Variable<bool>(isDirectChat); + map['pinned'] = Variable<bool>(pinned); + map['archived'] = Variable<bool>(archived); + map['joined_group'] = Variable<bool>(joinedGroup); + map['left_group'] = Variable<bool>(leftGroup); + map['deleted_content'] = Variable<bool>(deletedContent); + map['state_version_id'] = Variable<int>(stateVersionId); + if (!nullToAbsent || stateEncryptionKey != null) { + map['state_encryption_key'] = Variable<i2.Uint8List>(stateEncryptionKey); + } + if (!nullToAbsent || myGroupPrivateKey != null) { + map['my_group_private_key'] = Variable<i2.Uint8List>(myGroupPrivateKey); + } + map['group_name'] = Variable<String>(groupName); + if (!nullToAbsent || draftMessage != null) { + map['draft_message'] = Variable<String>(draftMessage); + } + map['total_media_counter'] = Variable<int>(totalMediaCounter); + map['also_best_friend'] = Variable<bool>(alsoBestFriend); + map['delete_messages_after_milliseconds'] = + Variable<int>(deleteMessagesAfterMilliseconds); + map['created_at'] = Variable<DateTime>(createdAt); + if (!nullToAbsent || lastMessageSend != null) { + map['last_message_send'] = Variable<DateTime>(lastMessageSend); + } + if (!nullToAbsent || lastMessageReceived != null) { + map['last_message_received'] = Variable<DateTime>(lastMessageReceived); + } + if (!nullToAbsent || lastFlameCounterChange != null) { + map['last_flame_counter_change'] = + Variable<DateTime>(lastFlameCounterChange); + } + if (!nullToAbsent || lastFlameSync != null) { + map['last_flame_sync'] = Variable<DateTime>(lastFlameSync); + } + map['flame_counter'] = Variable<int>(flameCounter); + map['max_flame_counter'] = Variable<int>(maxFlameCounter); + if (!nullToAbsent || maxFlameCounterFrom != null) { + map['max_flame_counter_from'] = Variable<DateTime>(maxFlameCounterFrom); + } + map['last_message_exchange'] = Variable<DateTime>(lastMessageExchange); + return map; + } + + GroupsCompanion toCompanion(bool nullToAbsent) { + return GroupsCompanion( + groupId: Value(groupId), + isGroupAdmin: Value(isGroupAdmin), + isDirectChat: Value(isDirectChat), + pinned: Value(pinned), + archived: Value(archived), + joinedGroup: Value(joinedGroup), + leftGroup: Value(leftGroup), + deletedContent: Value(deletedContent), + stateVersionId: Value(stateVersionId), + stateEncryptionKey: stateEncryptionKey == null && nullToAbsent + ? const Value.absent() + : Value(stateEncryptionKey), + myGroupPrivateKey: myGroupPrivateKey == null && nullToAbsent + ? const Value.absent() + : Value(myGroupPrivateKey), + groupName: Value(groupName), + draftMessage: draftMessage == null && nullToAbsent + ? const Value.absent() + : Value(draftMessage), + totalMediaCounter: Value(totalMediaCounter), + alsoBestFriend: Value(alsoBestFriend), + deleteMessagesAfterMilliseconds: Value(deleteMessagesAfterMilliseconds), + createdAt: Value(createdAt), + lastMessageSend: lastMessageSend == null && nullToAbsent + ? const Value.absent() + : Value(lastMessageSend), + lastMessageReceived: lastMessageReceived == null && nullToAbsent + ? const Value.absent() + : Value(lastMessageReceived), + lastFlameCounterChange: lastFlameCounterChange == null && nullToAbsent + ? const Value.absent() + : Value(lastFlameCounterChange), + lastFlameSync: lastFlameSync == null && nullToAbsent + ? const Value.absent() + : Value(lastFlameSync), + flameCounter: Value(flameCounter), + maxFlameCounter: Value(maxFlameCounter), + maxFlameCounterFrom: maxFlameCounterFrom == null && nullToAbsent + ? const Value.absent() + : Value(maxFlameCounterFrom), + lastMessageExchange: Value(lastMessageExchange), + ); + } + + factory GroupsData.fromJson(Map<String, dynamic> json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GroupsData( + groupId: serializer.fromJson<String>(json['groupId']), + isGroupAdmin: serializer.fromJson<bool>(json['isGroupAdmin']), + isDirectChat: serializer.fromJson<bool>(json['isDirectChat']), + pinned: serializer.fromJson<bool>(json['pinned']), + archived: serializer.fromJson<bool>(json['archived']), + joinedGroup: serializer.fromJson<bool>(json['joinedGroup']), + leftGroup: serializer.fromJson<bool>(json['leftGroup']), + deletedContent: serializer.fromJson<bool>(json['deletedContent']), + stateVersionId: serializer.fromJson<int>(json['stateVersionId']), + stateEncryptionKey: + serializer.fromJson<i2.Uint8List?>(json['stateEncryptionKey']), + myGroupPrivateKey: + serializer.fromJson<i2.Uint8List?>(json['myGroupPrivateKey']), + groupName: serializer.fromJson<String>(json['groupName']), + draftMessage: serializer.fromJson<String?>(json['draftMessage']), + totalMediaCounter: serializer.fromJson<int>(json['totalMediaCounter']), + alsoBestFriend: serializer.fromJson<bool>(json['alsoBestFriend']), + deleteMessagesAfterMilliseconds: + serializer.fromJson<int>(json['deleteMessagesAfterMilliseconds']), + createdAt: serializer.fromJson<DateTime>(json['createdAt']), + lastMessageSend: serializer.fromJson<DateTime?>(json['lastMessageSend']), + lastMessageReceived: + serializer.fromJson<DateTime?>(json['lastMessageReceived']), + lastFlameCounterChange: + serializer.fromJson<DateTime?>(json['lastFlameCounterChange']), + lastFlameSync: serializer.fromJson<DateTime?>(json['lastFlameSync']), + flameCounter: serializer.fromJson<int>(json['flameCounter']), + maxFlameCounter: serializer.fromJson<int>(json['maxFlameCounter']), + maxFlameCounterFrom: + serializer.fromJson<DateTime?>(json['maxFlameCounterFrom']), + lastMessageExchange: + serializer.fromJson<DateTime>(json['lastMessageExchange']), + ); + } + @override + Map<String, dynamic> toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return <String, dynamic>{ + 'groupId': serializer.toJson<String>(groupId), + 'isGroupAdmin': serializer.toJson<bool>(isGroupAdmin), + 'isDirectChat': serializer.toJson<bool>(isDirectChat), + 'pinned': serializer.toJson<bool>(pinned), + 'archived': serializer.toJson<bool>(archived), + 'joinedGroup': serializer.toJson<bool>(joinedGroup), + 'leftGroup': serializer.toJson<bool>(leftGroup), + 'deletedContent': serializer.toJson<bool>(deletedContent), + 'stateVersionId': serializer.toJson<int>(stateVersionId), + 'stateEncryptionKey': + serializer.toJson<i2.Uint8List?>(stateEncryptionKey), + 'myGroupPrivateKey': serializer.toJson<i2.Uint8List?>(myGroupPrivateKey), + 'groupName': serializer.toJson<String>(groupName), + 'draftMessage': serializer.toJson<String?>(draftMessage), + 'totalMediaCounter': serializer.toJson<int>(totalMediaCounter), + 'alsoBestFriend': serializer.toJson<bool>(alsoBestFriend), + 'deleteMessagesAfterMilliseconds': + serializer.toJson<int>(deleteMessagesAfterMilliseconds), + 'createdAt': serializer.toJson<DateTime>(createdAt), + 'lastMessageSend': serializer.toJson<DateTime?>(lastMessageSend), + 'lastMessageReceived': serializer.toJson<DateTime?>(lastMessageReceived), + 'lastFlameCounterChange': + serializer.toJson<DateTime?>(lastFlameCounterChange), + 'lastFlameSync': serializer.toJson<DateTime?>(lastFlameSync), + 'flameCounter': serializer.toJson<int>(flameCounter), + 'maxFlameCounter': serializer.toJson<int>(maxFlameCounter), + 'maxFlameCounterFrom': serializer.toJson<DateTime?>(maxFlameCounterFrom), + 'lastMessageExchange': serializer.toJson<DateTime>(lastMessageExchange), + }; + } + + GroupsData copyWith( + {String? groupId, + bool? isGroupAdmin, + bool? isDirectChat, + bool? pinned, + bool? archived, + bool? joinedGroup, + bool? leftGroup, + bool? deletedContent, + int? stateVersionId, + Value<i2.Uint8List?> stateEncryptionKey = const Value.absent(), + Value<i2.Uint8List?> myGroupPrivateKey = const Value.absent(), + String? groupName, + Value<String?> draftMessage = const Value.absent(), + int? totalMediaCounter, + bool? alsoBestFriend, + int? deleteMessagesAfterMilliseconds, + DateTime? createdAt, + Value<DateTime?> lastMessageSend = const Value.absent(), + Value<DateTime?> lastMessageReceived = const Value.absent(), + Value<DateTime?> lastFlameCounterChange = const Value.absent(), + Value<DateTime?> lastFlameSync = const Value.absent(), + int? flameCounter, + int? maxFlameCounter, + Value<DateTime?> maxFlameCounterFrom = const Value.absent(), + DateTime? lastMessageExchange}) => + GroupsData( + groupId: groupId ?? this.groupId, + isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, + isDirectChat: isDirectChat ?? this.isDirectChat, + pinned: pinned ?? this.pinned, + archived: archived ?? this.archived, + joinedGroup: joinedGroup ?? this.joinedGroup, + leftGroup: leftGroup ?? this.leftGroup, + deletedContent: deletedContent ?? this.deletedContent, + stateVersionId: stateVersionId ?? this.stateVersionId, + stateEncryptionKey: stateEncryptionKey.present + ? stateEncryptionKey.value + : this.stateEncryptionKey, + myGroupPrivateKey: myGroupPrivateKey.present + ? myGroupPrivateKey.value + : this.myGroupPrivateKey, + groupName: groupName ?? this.groupName, + draftMessage: + draftMessage.present ? draftMessage.value : this.draftMessage, + totalMediaCounter: totalMediaCounter ?? this.totalMediaCounter, + alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, + deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds ?? + this.deleteMessagesAfterMilliseconds, + createdAt: createdAt ?? this.createdAt, + lastMessageSend: lastMessageSend.present + ? lastMessageSend.value + : this.lastMessageSend, + lastMessageReceived: lastMessageReceived.present + ? lastMessageReceived.value + : this.lastMessageReceived, + lastFlameCounterChange: lastFlameCounterChange.present + ? lastFlameCounterChange.value + : this.lastFlameCounterChange, + lastFlameSync: + lastFlameSync.present ? lastFlameSync.value : this.lastFlameSync, + flameCounter: flameCounter ?? this.flameCounter, + maxFlameCounter: maxFlameCounter ?? this.maxFlameCounter, + maxFlameCounterFrom: maxFlameCounterFrom.present + ? maxFlameCounterFrom.value + : this.maxFlameCounterFrom, + lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, + ); + GroupsData copyWithCompanion(GroupsCompanion data) { + return GroupsData( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + isGroupAdmin: data.isGroupAdmin.present + ? data.isGroupAdmin.value + : this.isGroupAdmin, + isDirectChat: data.isDirectChat.present + ? data.isDirectChat.value + : this.isDirectChat, + pinned: data.pinned.present ? data.pinned.value : this.pinned, + archived: data.archived.present ? data.archived.value : this.archived, + joinedGroup: + data.joinedGroup.present ? data.joinedGroup.value : this.joinedGroup, + leftGroup: data.leftGroup.present ? data.leftGroup.value : this.leftGroup, + deletedContent: data.deletedContent.present + ? data.deletedContent.value + : this.deletedContent, + stateVersionId: data.stateVersionId.present + ? data.stateVersionId.value + : this.stateVersionId, + stateEncryptionKey: data.stateEncryptionKey.present + ? data.stateEncryptionKey.value + : this.stateEncryptionKey, + myGroupPrivateKey: data.myGroupPrivateKey.present + ? data.myGroupPrivateKey.value + : this.myGroupPrivateKey, + groupName: data.groupName.present ? data.groupName.value : this.groupName, + draftMessage: data.draftMessage.present + ? data.draftMessage.value + : this.draftMessage, + totalMediaCounter: data.totalMediaCounter.present + ? data.totalMediaCounter.value + : this.totalMediaCounter, + alsoBestFriend: data.alsoBestFriend.present + ? data.alsoBestFriend.value + : this.alsoBestFriend, + deleteMessagesAfterMilliseconds: + data.deleteMessagesAfterMilliseconds.present + ? data.deleteMessagesAfterMilliseconds.value + : this.deleteMessagesAfterMilliseconds, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + lastMessageSend: data.lastMessageSend.present + ? data.lastMessageSend.value + : this.lastMessageSend, + lastMessageReceived: data.lastMessageReceived.present + ? data.lastMessageReceived.value + : this.lastMessageReceived, + lastFlameCounterChange: data.lastFlameCounterChange.present + ? data.lastFlameCounterChange.value + : this.lastFlameCounterChange, + lastFlameSync: data.lastFlameSync.present + ? data.lastFlameSync.value + : this.lastFlameSync, + flameCounter: data.flameCounter.present + ? data.flameCounter.value + : this.flameCounter, + maxFlameCounter: data.maxFlameCounter.present + ? data.maxFlameCounter.value + : this.maxFlameCounter, + maxFlameCounterFrom: data.maxFlameCounterFrom.present + ? data.maxFlameCounterFrom.value + : this.maxFlameCounterFrom, + lastMessageExchange: data.lastMessageExchange.present + ? data.lastMessageExchange.value + : this.lastMessageExchange, + ); + } + + @override + String toString() { + return (StringBuffer('GroupsData(') + ..write('groupId: $groupId, ') + ..write('isGroupAdmin: $isGroupAdmin, ') + ..write('isDirectChat: $isDirectChat, ') + ..write('pinned: $pinned, ') + ..write('archived: $archived, ') + ..write('joinedGroup: $joinedGroup, ') + ..write('leftGroup: $leftGroup, ') + ..write('deletedContent: $deletedContent, ') + ..write('stateVersionId: $stateVersionId, ') + ..write('stateEncryptionKey: $stateEncryptionKey, ') + ..write('myGroupPrivateKey: $myGroupPrivateKey, ') + ..write('groupName: $groupName, ') + ..write('draftMessage: $draftMessage, ') + ..write('totalMediaCounter: $totalMediaCounter, ') + ..write('alsoBestFriend: $alsoBestFriend, ') + ..write( + 'deleteMessagesAfterMilliseconds: $deleteMessagesAfterMilliseconds, ') + ..write('createdAt: $createdAt, ') + ..write('lastMessageSend: $lastMessageSend, ') + ..write('lastMessageReceived: $lastMessageReceived, ') + ..write('lastFlameCounterChange: $lastFlameCounterChange, ') + ..write('lastFlameSync: $lastFlameSync, ') + ..write('flameCounter: $flameCounter, ') + ..write('maxFlameCounter: $maxFlameCounter, ') + ..write('maxFlameCounterFrom: $maxFlameCounterFrom, ') + ..write('lastMessageExchange: $lastMessageExchange') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + groupId, + isGroupAdmin, + isDirectChat, + pinned, + archived, + joinedGroup, + leftGroup, + deletedContent, + stateVersionId, + $driftBlobEquality.hash(stateEncryptionKey), + $driftBlobEquality.hash(myGroupPrivateKey), + groupName, + draftMessage, + totalMediaCounter, + alsoBestFriend, + deleteMessagesAfterMilliseconds, + createdAt, + lastMessageSend, + lastMessageReceived, + lastFlameCounterChange, + lastFlameSync, + flameCounter, + maxFlameCounter, + maxFlameCounterFrom, + lastMessageExchange + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GroupsData && + other.groupId == this.groupId && + other.isGroupAdmin == this.isGroupAdmin && + other.isDirectChat == this.isDirectChat && + other.pinned == this.pinned && + other.archived == this.archived && + other.joinedGroup == this.joinedGroup && + other.leftGroup == this.leftGroup && + other.deletedContent == this.deletedContent && + other.stateVersionId == this.stateVersionId && + $driftBlobEquality.equals( + other.stateEncryptionKey, this.stateEncryptionKey) && + $driftBlobEquality.equals( + other.myGroupPrivateKey, this.myGroupPrivateKey) && + other.groupName == this.groupName && + other.draftMessage == this.draftMessage && + other.totalMediaCounter == this.totalMediaCounter && + other.alsoBestFriend == this.alsoBestFriend && + other.deleteMessagesAfterMilliseconds == + this.deleteMessagesAfterMilliseconds && + other.createdAt == this.createdAt && + other.lastMessageSend == this.lastMessageSend && + other.lastMessageReceived == this.lastMessageReceived && + other.lastFlameCounterChange == this.lastFlameCounterChange && + other.lastFlameSync == this.lastFlameSync && + other.flameCounter == this.flameCounter && + other.maxFlameCounter == this.maxFlameCounter && + other.maxFlameCounterFrom == this.maxFlameCounterFrom && + other.lastMessageExchange == this.lastMessageExchange); +} + +class GroupsCompanion extends UpdateCompanion<GroupsData> { + final Value<String> groupId; + final Value<bool> isGroupAdmin; + final Value<bool> isDirectChat; + final Value<bool> pinned; + final Value<bool> archived; + final Value<bool> joinedGroup; + final Value<bool> leftGroup; + final Value<bool> deletedContent; + final Value<int> stateVersionId; + final Value<i2.Uint8List?> stateEncryptionKey; + final Value<i2.Uint8List?> myGroupPrivateKey; + final Value<String> groupName; + final Value<String?> draftMessage; + final Value<int> totalMediaCounter; + final Value<bool> alsoBestFriend; + final Value<int> deleteMessagesAfterMilliseconds; + final Value<DateTime> createdAt; + final Value<DateTime?> lastMessageSend; + final Value<DateTime?> lastMessageReceived; + final Value<DateTime?> lastFlameCounterChange; + final Value<DateTime?> lastFlameSync; + final Value<int> flameCounter; + final Value<int> maxFlameCounter; + final Value<DateTime?> maxFlameCounterFrom; + final Value<DateTime> lastMessageExchange; + final Value<int> rowid; + const GroupsCompanion({ + this.groupId = const Value.absent(), + this.isGroupAdmin = const Value.absent(), + this.isDirectChat = const Value.absent(), + this.pinned = const Value.absent(), + this.archived = const Value.absent(), + this.joinedGroup = const Value.absent(), + this.leftGroup = const Value.absent(), + this.deletedContent = const Value.absent(), + this.stateVersionId = const Value.absent(), + this.stateEncryptionKey = const Value.absent(), + this.myGroupPrivateKey = const Value.absent(), + this.groupName = const Value.absent(), + this.draftMessage = const Value.absent(), + this.totalMediaCounter = const Value.absent(), + this.alsoBestFriend = const Value.absent(), + this.deleteMessagesAfterMilliseconds = const Value.absent(), + this.createdAt = const Value.absent(), + this.lastMessageSend = const Value.absent(), + this.lastMessageReceived = const Value.absent(), + this.lastFlameCounterChange = const Value.absent(), + this.lastFlameSync = const Value.absent(), + this.flameCounter = const Value.absent(), + this.maxFlameCounter = const Value.absent(), + this.maxFlameCounterFrom = const Value.absent(), + this.lastMessageExchange = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupsCompanion.insert({ + required String groupId, + this.isGroupAdmin = const Value.absent(), + this.isDirectChat = const Value.absent(), + this.pinned = const Value.absent(), + this.archived = const Value.absent(), + this.joinedGroup = const Value.absent(), + this.leftGroup = const Value.absent(), + this.deletedContent = const Value.absent(), + this.stateVersionId = const Value.absent(), + this.stateEncryptionKey = const Value.absent(), + this.myGroupPrivateKey = const Value.absent(), + required String groupName, + this.draftMessage = const Value.absent(), + this.totalMediaCounter = const Value.absent(), + this.alsoBestFriend = const Value.absent(), + this.deleteMessagesAfterMilliseconds = const Value.absent(), + this.createdAt = const Value.absent(), + this.lastMessageSend = const Value.absent(), + this.lastMessageReceived = const Value.absent(), + this.lastFlameCounterChange = const Value.absent(), + this.lastFlameSync = const Value.absent(), + this.flameCounter = const Value.absent(), + this.maxFlameCounter = const Value.absent(), + this.maxFlameCounterFrom = const Value.absent(), + this.lastMessageExchange = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupId = Value(groupId), + groupName = Value(groupName); + static Insertable<GroupsData> custom({ + Expression<String>? groupId, + Expression<bool>? isGroupAdmin, + Expression<bool>? isDirectChat, + Expression<bool>? pinned, + Expression<bool>? archived, + Expression<bool>? joinedGroup, + Expression<bool>? leftGroup, + Expression<bool>? deletedContent, + Expression<int>? stateVersionId, + Expression<i2.Uint8List>? stateEncryptionKey, + Expression<i2.Uint8List>? myGroupPrivateKey, + Expression<String>? groupName, + Expression<String>? draftMessage, + Expression<int>? totalMediaCounter, + Expression<bool>? alsoBestFriend, + Expression<int>? deleteMessagesAfterMilliseconds, + Expression<DateTime>? createdAt, + Expression<DateTime>? lastMessageSend, + Expression<DateTime>? lastMessageReceived, + Expression<DateTime>? lastFlameCounterChange, + Expression<DateTime>? lastFlameSync, + Expression<int>? flameCounter, + Expression<int>? maxFlameCounter, + Expression<DateTime>? maxFlameCounterFrom, + Expression<DateTime>? lastMessageExchange, + Expression<int>? rowid, + }) { + return RawValuesInsertable({ + if (groupId != null) 'group_id': groupId, + if (isGroupAdmin != null) 'is_group_admin': isGroupAdmin, + if (isDirectChat != null) 'is_direct_chat': isDirectChat, + if (pinned != null) 'pinned': pinned, + if (archived != null) 'archived': archived, + if (joinedGroup != null) 'joined_group': joinedGroup, + if (leftGroup != null) 'left_group': leftGroup, + if (deletedContent != null) 'deleted_content': deletedContent, + if (stateVersionId != null) 'state_version_id': stateVersionId, + if (stateEncryptionKey != null) + 'state_encryption_key': stateEncryptionKey, + if (myGroupPrivateKey != null) 'my_group_private_key': myGroupPrivateKey, + if (groupName != null) 'group_name': groupName, + if (draftMessage != null) 'draft_message': draftMessage, + if (totalMediaCounter != null) 'total_media_counter': totalMediaCounter, + if (alsoBestFriend != null) 'also_best_friend': alsoBestFriend, + if (deleteMessagesAfterMilliseconds != null) + 'delete_messages_after_milliseconds': deleteMessagesAfterMilliseconds, + if (createdAt != null) 'created_at': createdAt, + if (lastMessageSend != null) 'last_message_send': lastMessageSend, + if (lastMessageReceived != null) + 'last_message_received': lastMessageReceived, + if (lastFlameCounterChange != null) + 'last_flame_counter_change': lastFlameCounterChange, + if (lastFlameSync != null) 'last_flame_sync': lastFlameSync, + if (flameCounter != null) 'flame_counter': flameCounter, + if (maxFlameCounter != null) 'max_flame_counter': maxFlameCounter, + if (maxFlameCounterFrom != null) + 'max_flame_counter_from': maxFlameCounterFrom, + if (lastMessageExchange != null) + 'last_message_exchange': lastMessageExchange, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupsCompanion copyWith( + {Value<String>? groupId, + Value<bool>? isGroupAdmin, + Value<bool>? isDirectChat, + Value<bool>? pinned, + Value<bool>? archived, + Value<bool>? joinedGroup, + Value<bool>? leftGroup, + Value<bool>? deletedContent, + Value<int>? stateVersionId, + Value<i2.Uint8List?>? stateEncryptionKey, + Value<i2.Uint8List?>? myGroupPrivateKey, + Value<String>? groupName, + Value<String?>? draftMessage, + Value<int>? totalMediaCounter, + Value<bool>? alsoBestFriend, + Value<int>? deleteMessagesAfterMilliseconds, + Value<DateTime>? createdAt, + Value<DateTime?>? lastMessageSend, + Value<DateTime?>? lastMessageReceived, + Value<DateTime?>? lastFlameCounterChange, + Value<DateTime?>? lastFlameSync, + Value<int>? flameCounter, + Value<int>? maxFlameCounter, + Value<DateTime?>? maxFlameCounterFrom, + Value<DateTime>? lastMessageExchange, + Value<int>? rowid}) { + return GroupsCompanion( + groupId: groupId ?? this.groupId, + isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, + isDirectChat: isDirectChat ?? this.isDirectChat, + pinned: pinned ?? this.pinned, + archived: archived ?? this.archived, + joinedGroup: joinedGroup ?? this.joinedGroup, + leftGroup: leftGroup ?? this.leftGroup, + deletedContent: deletedContent ?? this.deletedContent, + stateVersionId: stateVersionId ?? this.stateVersionId, + stateEncryptionKey: stateEncryptionKey ?? this.stateEncryptionKey, + myGroupPrivateKey: myGroupPrivateKey ?? this.myGroupPrivateKey, + groupName: groupName ?? this.groupName, + draftMessage: draftMessage ?? this.draftMessage, + totalMediaCounter: totalMediaCounter ?? this.totalMediaCounter, + alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, + deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds ?? + this.deleteMessagesAfterMilliseconds, + createdAt: createdAt ?? this.createdAt, + lastMessageSend: lastMessageSend ?? this.lastMessageSend, + lastMessageReceived: lastMessageReceived ?? this.lastMessageReceived, + lastFlameCounterChange: + lastFlameCounterChange ?? this.lastFlameCounterChange, + lastFlameSync: lastFlameSync ?? this.lastFlameSync, + flameCounter: flameCounter ?? this.flameCounter, + maxFlameCounter: maxFlameCounter ?? this.maxFlameCounter, + maxFlameCounterFrom: maxFlameCounterFrom ?? this.maxFlameCounterFrom, + lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + if (groupId.present) { + map['group_id'] = Variable<String>(groupId.value); + } + if (isGroupAdmin.present) { + map['is_group_admin'] = Variable<bool>(isGroupAdmin.value); + } + if (isDirectChat.present) { + map['is_direct_chat'] = Variable<bool>(isDirectChat.value); + } + if (pinned.present) { + map['pinned'] = Variable<bool>(pinned.value); + } + if (archived.present) { + map['archived'] = Variable<bool>(archived.value); + } + if (joinedGroup.present) { + map['joined_group'] = Variable<bool>(joinedGroup.value); + } + if (leftGroup.present) { + map['left_group'] = Variable<bool>(leftGroup.value); + } + if (deletedContent.present) { + map['deleted_content'] = Variable<bool>(deletedContent.value); + } + if (stateVersionId.present) { + map['state_version_id'] = Variable<int>(stateVersionId.value); + } + if (stateEncryptionKey.present) { + map['state_encryption_key'] = + Variable<i2.Uint8List>(stateEncryptionKey.value); + } + if (myGroupPrivateKey.present) { + map['my_group_private_key'] = + Variable<i2.Uint8List>(myGroupPrivateKey.value); + } + if (groupName.present) { + map['group_name'] = Variable<String>(groupName.value); + } + if (draftMessage.present) { + map['draft_message'] = Variable<String>(draftMessage.value); + } + if (totalMediaCounter.present) { + map['total_media_counter'] = Variable<int>(totalMediaCounter.value); + } + if (alsoBestFriend.present) { + map['also_best_friend'] = Variable<bool>(alsoBestFriend.value); + } + if (deleteMessagesAfterMilliseconds.present) { + map['delete_messages_after_milliseconds'] = + Variable<int>(deleteMessagesAfterMilliseconds.value); + } + if (createdAt.present) { + map['created_at'] = Variable<DateTime>(createdAt.value); + } + if (lastMessageSend.present) { + map['last_message_send'] = Variable<DateTime>(lastMessageSend.value); + } + if (lastMessageReceived.present) { + map['last_message_received'] = + Variable<DateTime>(lastMessageReceived.value); + } + if (lastFlameCounterChange.present) { + map['last_flame_counter_change'] = + Variable<DateTime>(lastFlameCounterChange.value); + } + if (lastFlameSync.present) { + map['last_flame_sync'] = Variable<DateTime>(lastFlameSync.value); + } + if (flameCounter.present) { + map['flame_counter'] = Variable<int>(flameCounter.value); + } + if (maxFlameCounter.present) { + map['max_flame_counter'] = Variable<int>(maxFlameCounter.value); + } + if (maxFlameCounterFrom.present) { + map['max_flame_counter_from'] = + Variable<DateTime>(maxFlameCounterFrom.value); + } + if (lastMessageExchange.present) { + map['last_message_exchange'] = + Variable<DateTime>(lastMessageExchange.value); + } + if (rowid.present) { + map['rowid'] = Variable<int>(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupsCompanion(') + ..write('groupId: $groupId, ') + ..write('isGroupAdmin: $isGroupAdmin, ') + ..write('isDirectChat: $isDirectChat, ') + ..write('pinned: $pinned, ') + ..write('archived: $archived, ') + ..write('joinedGroup: $joinedGroup, ') + ..write('leftGroup: $leftGroup, ') + ..write('deletedContent: $deletedContent, ') + ..write('stateVersionId: $stateVersionId, ') + ..write('stateEncryptionKey: $stateEncryptionKey, ') + ..write('myGroupPrivateKey: $myGroupPrivateKey, ') + ..write('groupName: $groupName, ') + ..write('draftMessage: $draftMessage, ') + ..write('totalMediaCounter: $totalMediaCounter, ') + ..write('alsoBestFriend: $alsoBestFriend, ') + ..write( + 'deleteMessagesAfterMilliseconds: $deleteMessagesAfterMilliseconds, ') + ..write('createdAt: $createdAt, ') + ..write('lastMessageSend: $lastMessageSend, ') + ..write('lastMessageReceived: $lastMessageReceived, ') + ..write('lastFlameCounterChange: $lastFlameCounterChange, ') + ..write('lastFlameSync: $lastFlameSync, ') + ..write('flameCounter: $flameCounter, ') + ..write('maxFlameCounter: $maxFlameCounter, ') + ..write('maxFlameCounterFrom: $maxFlameCounterFrom, ') + ..write('lastMessageExchange: $lastMessageExchange, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class MediaFiles extends Table with TableInfo<MediaFiles, MediaFilesData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MediaFiles(this.attachedDatabase, [this._alias]); + late final GeneratedColumn<String> mediaId = GeneratedColumn<String>( + 'media_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn<String> type = GeneratedColumn<String>( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn<String> uploadState = GeneratedColumn<String>( + 'upload_state', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn<String> downloadState = GeneratedColumn<String>( + 'download_state', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn<bool> requiresAuthentication = + GeneratedColumn<bool>('requires_authentication', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("requires_authentication" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<bool> stored = GeneratedColumn<bool>( + 'stored', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("stored" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<bool> isDraftMedia = GeneratedColumn<bool>( + 'is_draft_media', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_draft_media" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<String> reuploadRequestedBy = + GeneratedColumn<String>('reupload_requested_by', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn<int> displayLimitInMilliseconds = + GeneratedColumn<int>('display_limit_in_milliseconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn<bool> removeAudio = GeneratedColumn<bool>( + 'remove_audio', aliasedName, true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("remove_audio" IN (0, 1))')); + late final GeneratedColumn<i2.Uint8List> downloadToken = + GeneratedColumn<i2.Uint8List>('download_token', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + late final GeneratedColumn<i2.Uint8List> encryptionKey = + GeneratedColumn<i2.Uint8List>('encryption_key', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + late final GeneratedColumn<i2.Uint8List> encryptionMac = + GeneratedColumn<i2.Uint8List>('encryption_mac', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + late final GeneratedColumn<i2.Uint8List> encryptionNonce = + GeneratedColumn<i2.Uint8List>('encryption_nonce', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + late final GeneratedColumn<i2.Uint8List> storedFileHash = + GeneratedColumn<i2.Uint8List>('stored_file_hash', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); + @override + List<GeneratedColumn> get $columns => [ + mediaId, + type, + uploadState, + downloadState, + requiresAuthentication, + stored, + isDraftMedia, + reuploadRequestedBy, + displayLimitInMilliseconds, + removeAudio, + downloadToken, + encryptionKey, + encryptionMac, + encryptionNonce, + storedFileHash, + createdAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'media_files'; + @override + Set<GeneratedColumn> get $primaryKey => {mediaId}; + @override + MediaFilesData map(Map<String, dynamic> data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MediaFilesData( + mediaId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}media_id'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + uploadState: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}upload_state']), + downloadState: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}download_state']), + requiresAuthentication: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}requires_authentication'])!, + stored: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}stored'])!, + isDraftMedia: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_draft_media'])!, + reuploadRequestedBy: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}reupload_requested_by']), + displayLimitInMilliseconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}display_limit_in_milliseconds']), + removeAudio: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}remove_audio']), + downloadToken: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}download_token']), + encryptionKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}encryption_key']), + encryptionMac: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}encryption_mac']), + encryptionNonce: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}encryption_nonce']), + storedFileHash: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}stored_file_hash']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + MediaFiles createAlias(String alias) { + return MediaFiles(attachedDatabase, alias); + } +} + +class MediaFilesData extends DataClass implements Insertable<MediaFilesData> { + final String mediaId; + final String type; + final String? uploadState; + final String? downloadState; + final bool requiresAuthentication; + final bool stored; + final bool isDraftMedia; + final String? reuploadRequestedBy; + final int? displayLimitInMilliseconds; + final bool? removeAudio; + final i2.Uint8List? downloadToken; + final i2.Uint8List? encryptionKey; + final i2.Uint8List? encryptionMac; + final i2.Uint8List? encryptionNonce; + final i2.Uint8List? storedFileHash; + final DateTime createdAt; + const MediaFilesData( + {required this.mediaId, + required this.type, + this.uploadState, + this.downloadState, + required this.requiresAuthentication, + required this.stored, + required this.isDraftMedia, + this.reuploadRequestedBy, + this.displayLimitInMilliseconds, + this.removeAudio, + this.downloadToken, + this.encryptionKey, + this.encryptionMac, + this.encryptionNonce, + this.storedFileHash, + required this.createdAt}); + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + map['media_id'] = Variable<String>(mediaId); + map['type'] = Variable<String>(type); + if (!nullToAbsent || uploadState != null) { + map['upload_state'] = Variable<String>(uploadState); + } + if (!nullToAbsent || downloadState != null) { + map['download_state'] = Variable<String>(downloadState); + } + map['requires_authentication'] = Variable<bool>(requiresAuthentication); + map['stored'] = Variable<bool>(stored); + map['is_draft_media'] = Variable<bool>(isDraftMedia); + if (!nullToAbsent || reuploadRequestedBy != null) { + map['reupload_requested_by'] = Variable<String>(reuploadRequestedBy); + } + if (!nullToAbsent || displayLimitInMilliseconds != null) { + map['display_limit_in_milliseconds'] = + Variable<int>(displayLimitInMilliseconds); + } + if (!nullToAbsent || removeAudio != null) { + map['remove_audio'] = Variable<bool>(removeAudio); + } + if (!nullToAbsent || downloadToken != null) { + map['download_token'] = Variable<i2.Uint8List>(downloadToken); + } + if (!nullToAbsent || encryptionKey != null) { + map['encryption_key'] = Variable<i2.Uint8List>(encryptionKey); + } + if (!nullToAbsent || encryptionMac != null) { + map['encryption_mac'] = Variable<i2.Uint8List>(encryptionMac); + } + if (!nullToAbsent || encryptionNonce != null) { + map['encryption_nonce'] = Variable<i2.Uint8List>(encryptionNonce); + } + if (!nullToAbsent || storedFileHash != null) { + map['stored_file_hash'] = Variable<i2.Uint8List>(storedFileHash); + } + map['created_at'] = Variable<DateTime>(createdAt); + return map; + } + + MediaFilesCompanion toCompanion(bool nullToAbsent) { + return MediaFilesCompanion( + mediaId: Value(mediaId), + type: Value(type), + uploadState: uploadState == null && nullToAbsent + ? const Value.absent() + : Value(uploadState), + downloadState: downloadState == null && nullToAbsent + ? const Value.absent() + : Value(downloadState), + requiresAuthentication: Value(requiresAuthentication), + stored: Value(stored), + isDraftMedia: Value(isDraftMedia), + reuploadRequestedBy: reuploadRequestedBy == null && nullToAbsent + ? const Value.absent() + : Value(reuploadRequestedBy), + displayLimitInMilliseconds: + displayLimitInMilliseconds == null && nullToAbsent + ? const Value.absent() + : Value(displayLimitInMilliseconds), + removeAudio: removeAudio == null && nullToAbsent + ? const Value.absent() + : Value(removeAudio), + downloadToken: downloadToken == null && nullToAbsent + ? const Value.absent() + : Value(downloadToken), + encryptionKey: encryptionKey == null && nullToAbsent + ? const Value.absent() + : Value(encryptionKey), + encryptionMac: encryptionMac == null && nullToAbsent + ? const Value.absent() + : Value(encryptionMac), + encryptionNonce: encryptionNonce == null && nullToAbsent + ? const Value.absent() + : Value(encryptionNonce), + storedFileHash: storedFileHash == null && nullToAbsent + ? const Value.absent() + : Value(storedFileHash), + createdAt: Value(createdAt), + ); + } + + factory MediaFilesData.fromJson(Map<String, dynamic> json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MediaFilesData( + mediaId: serializer.fromJson<String>(json['mediaId']), + type: serializer.fromJson<String>(json['type']), + uploadState: serializer.fromJson<String?>(json['uploadState']), + downloadState: serializer.fromJson<String?>(json['downloadState']), + requiresAuthentication: + serializer.fromJson<bool>(json['requiresAuthentication']), + stored: serializer.fromJson<bool>(json['stored']), + isDraftMedia: serializer.fromJson<bool>(json['isDraftMedia']), + reuploadRequestedBy: + serializer.fromJson<String?>(json['reuploadRequestedBy']), + displayLimitInMilliseconds: + serializer.fromJson<int?>(json['displayLimitInMilliseconds']), + removeAudio: serializer.fromJson<bool?>(json['removeAudio']), + downloadToken: serializer.fromJson<i2.Uint8List?>(json['downloadToken']), + encryptionKey: serializer.fromJson<i2.Uint8List?>(json['encryptionKey']), + encryptionMac: serializer.fromJson<i2.Uint8List?>(json['encryptionMac']), + encryptionNonce: + serializer.fromJson<i2.Uint8List?>(json['encryptionNonce']), + storedFileHash: + serializer.fromJson<i2.Uint8List?>(json['storedFileHash']), + createdAt: serializer.fromJson<DateTime>(json['createdAt']), + ); + } + @override + Map<String, dynamic> toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return <String, dynamic>{ + 'mediaId': serializer.toJson<String>(mediaId), + 'type': serializer.toJson<String>(type), + 'uploadState': serializer.toJson<String?>(uploadState), + 'downloadState': serializer.toJson<String?>(downloadState), + 'requiresAuthentication': serializer.toJson<bool>(requiresAuthentication), + 'stored': serializer.toJson<bool>(stored), + 'isDraftMedia': serializer.toJson<bool>(isDraftMedia), + 'reuploadRequestedBy': serializer.toJson<String?>(reuploadRequestedBy), + 'displayLimitInMilliseconds': + serializer.toJson<int?>(displayLimitInMilliseconds), + 'removeAudio': serializer.toJson<bool?>(removeAudio), + 'downloadToken': serializer.toJson<i2.Uint8List?>(downloadToken), + 'encryptionKey': serializer.toJson<i2.Uint8List?>(encryptionKey), + 'encryptionMac': serializer.toJson<i2.Uint8List?>(encryptionMac), + 'encryptionNonce': serializer.toJson<i2.Uint8List?>(encryptionNonce), + 'storedFileHash': serializer.toJson<i2.Uint8List?>(storedFileHash), + 'createdAt': serializer.toJson<DateTime>(createdAt), + }; + } + + MediaFilesData copyWith( + {String? mediaId, + String? type, + Value<String?> uploadState = const Value.absent(), + Value<String?> downloadState = const Value.absent(), + bool? requiresAuthentication, + bool? stored, + bool? isDraftMedia, + Value<String?> reuploadRequestedBy = const Value.absent(), + Value<int?> displayLimitInMilliseconds = const Value.absent(), + Value<bool?> removeAudio = const Value.absent(), + Value<i2.Uint8List?> downloadToken = const Value.absent(), + Value<i2.Uint8List?> encryptionKey = const Value.absent(), + Value<i2.Uint8List?> encryptionMac = const Value.absent(), + Value<i2.Uint8List?> encryptionNonce = const Value.absent(), + Value<i2.Uint8List?> storedFileHash = const Value.absent(), + DateTime? createdAt}) => + MediaFilesData( + mediaId: mediaId ?? this.mediaId, + type: type ?? this.type, + uploadState: uploadState.present ? uploadState.value : this.uploadState, + downloadState: + downloadState.present ? downloadState.value : this.downloadState, + requiresAuthentication: + requiresAuthentication ?? this.requiresAuthentication, + stored: stored ?? this.stored, + isDraftMedia: isDraftMedia ?? this.isDraftMedia, + reuploadRequestedBy: reuploadRequestedBy.present + ? reuploadRequestedBy.value + : this.reuploadRequestedBy, + displayLimitInMilliseconds: displayLimitInMilliseconds.present + ? displayLimitInMilliseconds.value + : this.displayLimitInMilliseconds, + removeAudio: removeAudio.present ? removeAudio.value : this.removeAudio, + downloadToken: + downloadToken.present ? downloadToken.value : this.downloadToken, + encryptionKey: + encryptionKey.present ? encryptionKey.value : this.encryptionKey, + encryptionMac: + encryptionMac.present ? encryptionMac.value : this.encryptionMac, + encryptionNonce: encryptionNonce.present + ? encryptionNonce.value + : this.encryptionNonce, + storedFileHash: + storedFileHash.present ? storedFileHash.value : this.storedFileHash, + createdAt: createdAt ?? this.createdAt, + ); + MediaFilesData copyWithCompanion(MediaFilesCompanion data) { + return MediaFilesData( + mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, + type: data.type.present ? data.type.value : this.type, + uploadState: + data.uploadState.present ? data.uploadState.value : this.uploadState, + downloadState: data.downloadState.present + ? data.downloadState.value + : this.downloadState, + requiresAuthentication: data.requiresAuthentication.present + ? data.requiresAuthentication.value + : this.requiresAuthentication, + stored: data.stored.present ? data.stored.value : this.stored, + isDraftMedia: data.isDraftMedia.present + ? data.isDraftMedia.value + : this.isDraftMedia, + reuploadRequestedBy: data.reuploadRequestedBy.present + ? data.reuploadRequestedBy.value + : this.reuploadRequestedBy, + displayLimitInMilliseconds: data.displayLimitInMilliseconds.present + ? data.displayLimitInMilliseconds.value + : this.displayLimitInMilliseconds, + removeAudio: + data.removeAudio.present ? data.removeAudio.value : this.removeAudio, + downloadToken: data.downloadToken.present + ? data.downloadToken.value + : this.downloadToken, + encryptionKey: data.encryptionKey.present + ? data.encryptionKey.value + : this.encryptionKey, + encryptionMac: data.encryptionMac.present + ? data.encryptionMac.value + : this.encryptionMac, + encryptionNonce: data.encryptionNonce.present + ? data.encryptionNonce.value + : this.encryptionNonce, + storedFileHash: data.storedFileHash.present + ? data.storedFileHash.value + : this.storedFileHash, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('MediaFilesData(') + ..write('mediaId: $mediaId, ') + ..write('type: $type, ') + ..write('uploadState: $uploadState, ') + ..write('downloadState: $downloadState, ') + ..write('requiresAuthentication: $requiresAuthentication, ') + ..write('stored: $stored, ') + ..write('isDraftMedia: $isDraftMedia, ') + ..write('reuploadRequestedBy: $reuploadRequestedBy, ') + ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') + ..write('removeAudio: $removeAudio, ') + ..write('downloadToken: $downloadToken, ') + ..write('encryptionKey: $encryptionKey, ') + ..write('encryptionMac: $encryptionMac, ') + ..write('encryptionNonce: $encryptionNonce, ') + ..write('storedFileHash: $storedFileHash, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + mediaId, + type, + uploadState, + downloadState, + requiresAuthentication, + stored, + isDraftMedia, + reuploadRequestedBy, + displayLimitInMilliseconds, + removeAudio, + $driftBlobEquality.hash(downloadToken), + $driftBlobEquality.hash(encryptionKey), + $driftBlobEquality.hash(encryptionMac), + $driftBlobEquality.hash(encryptionNonce), + $driftBlobEquality.hash(storedFileHash), + createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MediaFilesData && + other.mediaId == this.mediaId && + other.type == this.type && + other.uploadState == this.uploadState && + other.downloadState == this.downloadState && + other.requiresAuthentication == this.requiresAuthentication && + other.stored == this.stored && + other.isDraftMedia == this.isDraftMedia && + other.reuploadRequestedBy == this.reuploadRequestedBy && + other.displayLimitInMilliseconds == this.displayLimitInMilliseconds && + other.removeAudio == this.removeAudio && + $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && + $driftBlobEquality.equals(other.encryptionKey, this.encryptionKey) && + $driftBlobEquality.equals(other.encryptionMac, this.encryptionMac) && + $driftBlobEquality.equals( + other.encryptionNonce, this.encryptionNonce) && + $driftBlobEquality.equals( + other.storedFileHash, this.storedFileHash) && + other.createdAt == this.createdAt); +} + +class MediaFilesCompanion extends UpdateCompanion<MediaFilesData> { + final Value<String> mediaId; + final Value<String> type; + final Value<String?> uploadState; + final Value<String?> downloadState; + final Value<bool> requiresAuthentication; + final Value<bool> stored; + final Value<bool> isDraftMedia; + final Value<String?> reuploadRequestedBy; + final Value<int?> displayLimitInMilliseconds; + final Value<bool?> removeAudio; + final Value<i2.Uint8List?> downloadToken; + final Value<i2.Uint8List?> encryptionKey; + final Value<i2.Uint8List?> encryptionMac; + final Value<i2.Uint8List?> encryptionNonce; + final Value<i2.Uint8List?> storedFileHash; + final Value<DateTime> createdAt; + final Value<int> rowid; + const MediaFilesCompanion({ + this.mediaId = const Value.absent(), + this.type = const Value.absent(), + this.uploadState = const Value.absent(), + this.downloadState = const Value.absent(), + this.requiresAuthentication = const Value.absent(), + this.stored = const Value.absent(), + this.isDraftMedia = const Value.absent(), + this.reuploadRequestedBy = const Value.absent(), + this.displayLimitInMilliseconds = const Value.absent(), + this.removeAudio = const Value.absent(), + this.downloadToken = const Value.absent(), + this.encryptionKey = const Value.absent(), + this.encryptionMac = const Value.absent(), + this.encryptionNonce = const Value.absent(), + this.storedFileHash = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + MediaFilesCompanion.insert({ + required String mediaId, + required String type, + this.uploadState = const Value.absent(), + this.downloadState = const Value.absent(), + this.requiresAuthentication = const Value.absent(), + this.stored = const Value.absent(), + this.isDraftMedia = const Value.absent(), + this.reuploadRequestedBy = const Value.absent(), + this.displayLimitInMilliseconds = const Value.absent(), + this.removeAudio = const Value.absent(), + this.downloadToken = const Value.absent(), + this.encryptionKey = const Value.absent(), + this.encryptionMac = const Value.absent(), + this.encryptionNonce = const Value.absent(), + this.storedFileHash = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : mediaId = Value(mediaId), + type = Value(type); + static Insertable<MediaFilesData> custom({ + Expression<String>? mediaId, + Expression<String>? type, + Expression<String>? uploadState, + Expression<String>? downloadState, + Expression<bool>? requiresAuthentication, + Expression<bool>? stored, + Expression<bool>? isDraftMedia, + Expression<String>? reuploadRequestedBy, + Expression<int>? displayLimitInMilliseconds, + Expression<bool>? removeAudio, + Expression<i2.Uint8List>? downloadToken, + Expression<i2.Uint8List>? encryptionKey, + Expression<i2.Uint8List>? encryptionMac, + Expression<i2.Uint8List>? encryptionNonce, + Expression<i2.Uint8List>? storedFileHash, + Expression<DateTime>? createdAt, + Expression<int>? rowid, + }) { + return RawValuesInsertable({ + if (mediaId != null) 'media_id': mediaId, + if (type != null) 'type': type, + if (uploadState != null) 'upload_state': uploadState, + if (downloadState != null) 'download_state': downloadState, + if (requiresAuthentication != null) + 'requires_authentication': requiresAuthentication, + if (stored != null) 'stored': stored, + if (isDraftMedia != null) 'is_draft_media': isDraftMedia, + if (reuploadRequestedBy != null) + 'reupload_requested_by': reuploadRequestedBy, + if (displayLimitInMilliseconds != null) + 'display_limit_in_milliseconds': displayLimitInMilliseconds, + if (removeAudio != null) 'remove_audio': removeAudio, + if (downloadToken != null) 'download_token': downloadToken, + if (encryptionKey != null) 'encryption_key': encryptionKey, + if (encryptionMac != null) 'encryption_mac': encryptionMac, + if (encryptionNonce != null) 'encryption_nonce': encryptionNonce, + if (storedFileHash != null) 'stored_file_hash': storedFileHash, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + MediaFilesCompanion copyWith( + {Value<String>? mediaId, + Value<String>? type, + Value<String?>? uploadState, + Value<String?>? downloadState, + Value<bool>? requiresAuthentication, + Value<bool>? stored, + Value<bool>? isDraftMedia, + Value<String?>? reuploadRequestedBy, + Value<int?>? displayLimitInMilliseconds, + Value<bool?>? removeAudio, + Value<i2.Uint8List?>? downloadToken, + Value<i2.Uint8List?>? encryptionKey, + Value<i2.Uint8List?>? encryptionMac, + Value<i2.Uint8List?>? encryptionNonce, + Value<i2.Uint8List?>? storedFileHash, + Value<DateTime>? createdAt, + Value<int>? rowid}) { + return MediaFilesCompanion( + mediaId: mediaId ?? this.mediaId, + type: type ?? this.type, + uploadState: uploadState ?? this.uploadState, + downloadState: downloadState ?? this.downloadState, + requiresAuthentication: + requiresAuthentication ?? this.requiresAuthentication, + stored: stored ?? this.stored, + isDraftMedia: isDraftMedia ?? this.isDraftMedia, + reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy, + displayLimitInMilliseconds: + displayLimitInMilliseconds ?? this.displayLimitInMilliseconds, + removeAudio: removeAudio ?? this.removeAudio, + downloadToken: downloadToken ?? this.downloadToken, + encryptionKey: encryptionKey ?? this.encryptionKey, + encryptionMac: encryptionMac ?? this.encryptionMac, + encryptionNonce: encryptionNonce ?? this.encryptionNonce, + storedFileHash: storedFileHash ?? this.storedFileHash, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + if (mediaId.present) { + map['media_id'] = Variable<String>(mediaId.value); + } + if (type.present) { + map['type'] = Variable<String>(type.value); + } + if (uploadState.present) { + map['upload_state'] = Variable<String>(uploadState.value); + } + if (downloadState.present) { + map['download_state'] = Variable<String>(downloadState.value); + } + if (requiresAuthentication.present) { + map['requires_authentication'] = + Variable<bool>(requiresAuthentication.value); + } + if (stored.present) { + map['stored'] = Variable<bool>(stored.value); + } + if (isDraftMedia.present) { + map['is_draft_media'] = Variable<bool>(isDraftMedia.value); + } + if (reuploadRequestedBy.present) { + map['reupload_requested_by'] = + Variable<String>(reuploadRequestedBy.value); + } + if (displayLimitInMilliseconds.present) { + map['display_limit_in_milliseconds'] = + Variable<int>(displayLimitInMilliseconds.value); + } + if (removeAudio.present) { + map['remove_audio'] = Variable<bool>(removeAudio.value); + } + if (downloadToken.present) { + map['download_token'] = Variable<i2.Uint8List>(downloadToken.value); + } + if (encryptionKey.present) { + map['encryption_key'] = Variable<i2.Uint8List>(encryptionKey.value); + } + if (encryptionMac.present) { + map['encryption_mac'] = Variable<i2.Uint8List>(encryptionMac.value); + } + if (encryptionNonce.present) { + map['encryption_nonce'] = Variable<i2.Uint8List>(encryptionNonce.value); + } + if (storedFileHash.present) { + map['stored_file_hash'] = Variable<i2.Uint8List>(storedFileHash.value); + } + if (createdAt.present) { + map['created_at'] = Variable<DateTime>(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable<int>(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MediaFilesCompanion(') + ..write('mediaId: $mediaId, ') + ..write('type: $type, ') + ..write('uploadState: $uploadState, ') + ..write('downloadState: $downloadState, ') + ..write('requiresAuthentication: $requiresAuthentication, ') + ..write('stored: $stored, ') + ..write('isDraftMedia: $isDraftMedia, ') + ..write('reuploadRequestedBy: $reuploadRequestedBy, ') + ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') + ..write('removeAudio: $removeAudio, ') + ..write('downloadToken: $downloadToken, ') + ..write('encryptionKey: $encryptionKey, ') + ..write('encryptionMac: $encryptionMac, ') + ..write('encryptionNonce: $encryptionNonce, ') + ..write('storedFileHash: $storedFileHash, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Messages extends Table with TableInfo<Messages, MessagesData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Messages(this.attachedDatabase, [this._alias]); + late final GeneratedColumn<String> groupId = GeneratedColumn<String>( + 'group_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES "groups" (group_id) ON DELETE CASCADE')); + late final GeneratedColumn<String> messageId = GeneratedColumn<String>( + 'message_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn<int> senderId = GeneratedColumn<int>( + 'sender_id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)')); + late final GeneratedColumn<String> type = GeneratedColumn<String>( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn<String> content = GeneratedColumn<String>( + 'content', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn<String> mediaId = GeneratedColumn<String>( + 'media_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES media_files (media_id) ON DELETE SET NULL')); + late final GeneratedColumn<i2.Uint8List> additionalMessageData = + GeneratedColumn<i2.Uint8List>( + 'additional_message_data', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + late final GeneratedColumn<bool> mediaStored = GeneratedColumn<bool>( + 'media_stored', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("media_stored" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<bool> mediaReopened = GeneratedColumn<bool>( + 'media_reopened', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("media_reopened" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<i2.Uint8List> downloadToken = + GeneratedColumn<i2.Uint8List>('download_token', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + late final GeneratedColumn<String> quotesMessageId = GeneratedColumn<String>( + 'quotes_message_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn<bool> isDeletedFromSender = GeneratedColumn<bool>( + 'is_deleted_from_sender', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_deleted_from_sender" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<DateTime> openedAt = GeneratedColumn<DateTime>( + 'opened_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn<DateTime> openedByAll = GeneratedColumn<DateTime>( + 'opened_by_all', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); + late final GeneratedColumn<DateTime> modifiedAt = GeneratedColumn<DateTime>( + 'modified_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn<DateTime> ackByUser = GeneratedColumn<DateTime>( + 'ack_by_user', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn<DateTime> ackByServer = GeneratedColumn<DateTime>( + 'ack_by_server', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List<GeneratedColumn> get $columns => [ + groupId, + messageId, + senderId, + type, + content, + mediaId, + additionalMessageData, + mediaStored, + mediaReopened, + downloadToken, + quotesMessageId, + isDeletedFromSender, + openedAt, + openedByAll, + createdAt, + modifiedAt, + ackByUser, + ackByServer + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'messages'; + @override + Set<GeneratedColumn> get $primaryKey => {messageId}; + @override + MessagesData map(Map<String, dynamic> data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MessagesData( + groupId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_id'])!, + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, + senderId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}sender_id']), + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + content: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}content']), + mediaId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}media_id']), + additionalMessageData: attachedDatabase.typeMapping.read( + DriftSqlType.blob, data['${effectivePrefix}additional_message_data']), + mediaStored: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}media_stored'])!, + mediaReopened: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}media_reopened'])!, + downloadToken: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}download_token']), + quotesMessageId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}quotes_message_id']), + isDeletedFromSender: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}is_deleted_from_sender'])!, + openedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}opened_at']), + openedByAll: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}opened_by_all']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + modifiedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}modified_at']), + ackByUser: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}ack_by_user']), + ackByServer: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}ack_by_server']), + ); + } + + @override + Messages createAlias(String alias) { + return Messages(attachedDatabase, alias); + } +} + +class MessagesData extends DataClass implements Insertable<MessagesData> { + final String groupId; + final String messageId; + final int? senderId; + final String type; + final String? content; + final String? mediaId; + final i2.Uint8List? additionalMessageData; + final bool mediaStored; + final bool mediaReopened; + final i2.Uint8List? downloadToken; + final String? quotesMessageId; + final bool isDeletedFromSender; + final DateTime? openedAt; + final DateTime? openedByAll; + final DateTime createdAt; + final DateTime? modifiedAt; + final DateTime? ackByUser; + final DateTime? ackByServer; + const MessagesData( + {required this.groupId, + required this.messageId, + this.senderId, + required this.type, + this.content, + this.mediaId, + this.additionalMessageData, + required this.mediaStored, + required this.mediaReopened, + this.downloadToken, + this.quotesMessageId, + required this.isDeletedFromSender, + this.openedAt, + this.openedByAll, + required this.createdAt, + this.modifiedAt, + this.ackByUser, + this.ackByServer}); + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + map['group_id'] = Variable<String>(groupId); + map['message_id'] = Variable<String>(messageId); + if (!nullToAbsent || senderId != null) { + map['sender_id'] = Variable<int>(senderId); + } + map['type'] = Variable<String>(type); + if (!nullToAbsent || content != null) { + map['content'] = Variable<String>(content); + } + if (!nullToAbsent || mediaId != null) { + map['media_id'] = Variable<String>(mediaId); + } + if (!nullToAbsent || additionalMessageData != null) { + map['additional_message_data'] = + Variable<i2.Uint8List>(additionalMessageData); + } + map['media_stored'] = Variable<bool>(mediaStored); + map['media_reopened'] = Variable<bool>(mediaReopened); + if (!nullToAbsent || downloadToken != null) { + map['download_token'] = Variable<i2.Uint8List>(downloadToken); + } + if (!nullToAbsent || quotesMessageId != null) { + map['quotes_message_id'] = Variable<String>(quotesMessageId); + } + map['is_deleted_from_sender'] = Variable<bool>(isDeletedFromSender); + if (!nullToAbsent || openedAt != null) { + map['opened_at'] = Variable<DateTime>(openedAt); + } + if (!nullToAbsent || openedByAll != null) { + map['opened_by_all'] = Variable<DateTime>(openedByAll); + } + map['created_at'] = Variable<DateTime>(createdAt); + if (!nullToAbsent || modifiedAt != null) { + map['modified_at'] = Variable<DateTime>(modifiedAt); + } + if (!nullToAbsent || ackByUser != null) { + map['ack_by_user'] = Variable<DateTime>(ackByUser); + } + if (!nullToAbsent || ackByServer != null) { + map['ack_by_server'] = Variable<DateTime>(ackByServer); + } + return map; + } + + MessagesCompanion toCompanion(bool nullToAbsent) { + return MessagesCompanion( + groupId: Value(groupId), + messageId: Value(messageId), + senderId: senderId == null && nullToAbsent + ? const Value.absent() + : Value(senderId), + type: Value(type), + content: content == null && nullToAbsent + ? const Value.absent() + : Value(content), + mediaId: mediaId == null && nullToAbsent + ? const Value.absent() + : Value(mediaId), + additionalMessageData: additionalMessageData == null && nullToAbsent + ? const Value.absent() + : Value(additionalMessageData), + mediaStored: Value(mediaStored), + mediaReopened: Value(mediaReopened), + downloadToken: downloadToken == null && nullToAbsent + ? const Value.absent() + : Value(downloadToken), + quotesMessageId: quotesMessageId == null && nullToAbsent + ? const Value.absent() + : Value(quotesMessageId), + isDeletedFromSender: Value(isDeletedFromSender), + openedAt: openedAt == null && nullToAbsent + ? const Value.absent() + : Value(openedAt), + openedByAll: openedByAll == null && nullToAbsent + ? const Value.absent() + : Value(openedByAll), + createdAt: Value(createdAt), + modifiedAt: modifiedAt == null && nullToAbsent + ? const Value.absent() + : Value(modifiedAt), + ackByUser: ackByUser == null && nullToAbsent + ? const Value.absent() + : Value(ackByUser), + ackByServer: ackByServer == null && nullToAbsent + ? const Value.absent() + : Value(ackByServer), + ); + } + + factory MessagesData.fromJson(Map<String, dynamic> json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MessagesData( + groupId: serializer.fromJson<String>(json['groupId']), + messageId: serializer.fromJson<String>(json['messageId']), + senderId: serializer.fromJson<int?>(json['senderId']), + type: serializer.fromJson<String>(json['type']), + content: serializer.fromJson<String?>(json['content']), + mediaId: serializer.fromJson<String?>(json['mediaId']), + additionalMessageData: + serializer.fromJson<i2.Uint8List?>(json['additionalMessageData']), + mediaStored: serializer.fromJson<bool>(json['mediaStored']), + mediaReopened: serializer.fromJson<bool>(json['mediaReopened']), + downloadToken: serializer.fromJson<i2.Uint8List?>(json['downloadToken']), + quotesMessageId: serializer.fromJson<String?>(json['quotesMessageId']), + isDeletedFromSender: + serializer.fromJson<bool>(json['isDeletedFromSender']), + openedAt: serializer.fromJson<DateTime?>(json['openedAt']), + openedByAll: serializer.fromJson<DateTime?>(json['openedByAll']), + createdAt: serializer.fromJson<DateTime>(json['createdAt']), + modifiedAt: serializer.fromJson<DateTime?>(json['modifiedAt']), + ackByUser: serializer.fromJson<DateTime?>(json['ackByUser']), + ackByServer: serializer.fromJson<DateTime?>(json['ackByServer']), + ); + } + @override + Map<String, dynamic> toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return <String, dynamic>{ + 'groupId': serializer.toJson<String>(groupId), + 'messageId': serializer.toJson<String>(messageId), + 'senderId': serializer.toJson<int?>(senderId), + 'type': serializer.toJson<String>(type), + 'content': serializer.toJson<String?>(content), + 'mediaId': serializer.toJson<String?>(mediaId), + 'additionalMessageData': + serializer.toJson<i2.Uint8List?>(additionalMessageData), + 'mediaStored': serializer.toJson<bool>(mediaStored), + 'mediaReopened': serializer.toJson<bool>(mediaReopened), + 'downloadToken': serializer.toJson<i2.Uint8List?>(downloadToken), + 'quotesMessageId': serializer.toJson<String?>(quotesMessageId), + 'isDeletedFromSender': serializer.toJson<bool>(isDeletedFromSender), + 'openedAt': serializer.toJson<DateTime?>(openedAt), + 'openedByAll': serializer.toJson<DateTime?>(openedByAll), + 'createdAt': serializer.toJson<DateTime>(createdAt), + 'modifiedAt': serializer.toJson<DateTime?>(modifiedAt), + 'ackByUser': serializer.toJson<DateTime?>(ackByUser), + 'ackByServer': serializer.toJson<DateTime?>(ackByServer), + }; + } + + MessagesData copyWith( + {String? groupId, + String? messageId, + Value<int?> senderId = const Value.absent(), + String? type, + Value<String?> content = const Value.absent(), + Value<String?> mediaId = const Value.absent(), + Value<i2.Uint8List?> additionalMessageData = const Value.absent(), + bool? mediaStored, + bool? mediaReopened, + Value<i2.Uint8List?> downloadToken = const Value.absent(), + Value<String?> quotesMessageId = const Value.absent(), + bool? isDeletedFromSender, + Value<DateTime?> openedAt = const Value.absent(), + Value<DateTime?> openedByAll = const Value.absent(), + DateTime? createdAt, + Value<DateTime?> modifiedAt = const Value.absent(), + Value<DateTime?> ackByUser = const Value.absent(), + Value<DateTime?> ackByServer = const Value.absent()}) => + MessagesData( + groupId: groupId ?? this.groupId, + messageId: messageId ?? this.messageId, + senderId: senderId.present ? senderId.value : this.senderId, + type: type ?? this.type, + content: content.present ? content.value : this.content, + mediaId: mediaId.present ? mediaId.value : this.mediaId, + additionalMessageData: additionalMessageData.present + ? additionalMessageData.value + : this.additionalMessageData, + mediaStored: mediaStored ?? this.mediaStored, + mediaReopened: mediaReopened ?? this.mediaReopened, + downloadToken: + downloadToken.present ? downloadToken.value : this.downloadToken, + quotesMessageId: quotesMessageId.present + ? quotesMessageId.value + : this.quotesMessageId, + isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, + openedAt: openedAt.present ? openedAt.value : this.openedAt, + openedByAll: openedByAll.present ? openedByAll.value : this.openedByAll, + createdAt: createdAt ?? this.createdAt, + modifiedAt: modifiedAt.present ? modifiedAt.value : this.modifiedAt, + ackByUser: ackByUser.present ? ackByUser.value : this.ackByUser, + ackByServer: ackByServer.present ? ackByServer.value : this.ackByServer, + ); + MessagesData copyWithCompanion(MessagesCompanion data) { + return MessagesData( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + senderId: data.senderId.present ? data.senderId.value : this.senderId, + type: data.type.present ? data.type.value : this.type, + content: data.content.present ? data.content.value : this.content, + mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, + additionalMessageData: data.additionalMessageData.present + ? data.additionalMessageData.value + : this.additionalMessageData, + mediaStored: + data.mediaStored.present ? data.mediaStored.value : this.mediaStored, + mediaReopened: data.mediaReopened.present + ? data.mediaReopened.value + : this.mediaReopened, + downloadToken: data.downloadToken.present + ? data.downloadToken.value + : this.downloadToken, + quotesMessageId: data.quotesMessageId.present + ? data.quotesMessageId.value + : this.quotesMessageId, + isDeletedFromSender: data.isDeletedFromSender.present + ? data.isDeletedFromSender.value + : this.isDeletedFromSender, + openedAt: data.openedAt.present ? data.openedAt.value : this.openedAt, + openedByAll: + data.openedByAll.present ? data.openedByAll.value : this.openedByAll, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + modifiedAt: + data.modifiedAt.present ? data.modifiedAt.value : this.modifiedAt, + ackByUser: data.ackByUser.present ? data.ackByUser.value : this.ackByUser, + ackByServer: + data.ackByServer.present ? data.ackByServer.value : this.ackByServer, + ); + } + + @override + String toString() { + return (StringBuffer('MessagesData(') + ..write('groupId: $groupId, ') + ..write('messageId: $messageId, ') + ..write('senderId: $senderId, ') + ..write('type: $type, ') + ..write('content: $content, ') + ..write('mediaId: $mediaId, ') + ..write('additionalMessageData: $additionalMessageData, ') + ..write('mediaStored: $mediaStored, ') + ..write('mediaReopened: $mediaReopened, ') + ..write('downloadToken: $downloadToken, ') + ..write('quotesMessageId: $quotesMessageId, ') + ..write('isDeletedFromSender: $isDeletedFromSender, ') + ..write('openedAt: $openedAt, ') + ..write('openedByAll: $openedByAll, ') + ..write('createdAt: $createdAt, ') + ..write('modifiedAt: $modifiedAt, ') + ..write('ackByUser: $ackByUser, ') + ..write('ackByServer: $ackByServer') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + groupId, + messageId, + senderId, + type, + content, + mediaId, + $driftBlobEquality.hash(additionalMessageData), + mediaStored, + mediaReopened, + $driftBlobEquality.hash(downloadToken), + quotesMessageId, + isDeletedFromSender, + openedAt, + openedByAll, + createdAt, + modifiedAt, + ackByUser, + ackByServer); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MessagesData && + other.groupId == this.groupId && + other.messageId == this.messageId && + other.senderId == this.senderId && + other.type == this.type && + other.content == this.content && + other.mediaId == this.mediaId && + $driftBlobEquality.equals( + other.additionalMessageData, this.additionalMessageData) && + other.mediaStored == this.mediaStored && + other.mediaReopened == this.mediaReopened && + $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && + other.quotesMessageId == this.quotesMessageId && + other.isDeletedFromSender == this.isDeletedFromSender && + other.openedAt == this.openedAt && + other.openedByAll == this.openedByAll && + other.createdAt == this.createdAt && + other.modifiedAt == this.modifiedAt && + other.ackByUser == this.ackByUser && + other.ackByServer == this.ackByServer); +} + +class MessagesCompanion extends UpdateCompanion<MessagesData> { + final Value<String> groupId; + final Value<String> messageId; + final Value<int?> senderId; + final Value<String> type; + final Value<String?> content; + final Value<String?> mediaId; + final Value<i2.Uint8List?> additionalMessageData; + final Value<bool> mediaStored; + final Value<bool> mediaReopened; + final Value<i2.Uint8List?> downloadToken; + final Value<String?> quotesMessageId; + final Value<bool> isDeletedFromSender; + final Value<DateTime?> openedAt; + final Value<DateTime?> openedByAll; + final Value<DateTime> createdAt; + final Value<DateTime?> modifiedAt; + final Value<DateTime?> ackByUser; + final Value<DateTime?> ackByServer; + final Value<int> rowid; + const MessagesCompanion({ + this.groupId = const Value.absent(), + this.messageId = const Value.absent(), + this.senderId = const Value.absent(), + this.type = const Value.absent(), + this.content = const Value.absent(), + this.mediaId = const Value.absent(), + this.additionalMessageData = const Value.absent(), + this.mediaStored = const Value.absent(), + this.mediaReopened = const Value.absent(), + this.downloadToken = const Value.absent(), + this.quotesMessageId = const Value.absent(), + this.isDeletedFromSender = const Value.absent(), + this.openedAt = const Value.absent(), + this.openedByAll = const Value.absent(), + this.createdAt = const Value.absent(), + this.modifiedAt = const Value.absent(), + this.ackByUser = const Value.absent(), + this.ackByServer = const Value.absent(), + this.rowid = const Value.absent(), + }); + MessagesCompanion.insert({ + required String groupId, + required String messageId, + this.senderId = const Value.absent(), + required String type, + this.content = const Value.absent(), + this.mediaId = const Value.absent(), + this.additionalMessageData = const Value.absent(), + this.mediaStored = const Value.absent(), + this.mediaReopened = const Value.absent(), + this.downloadToken = const Value.absent(), + this.quotesMessageId = const Value.absent(), + this.isDeletedFromSender = const Value.absent(), + this.openedAt = const Value.absent(), + this.openedByAll = const Value.absent(), + this.createdAt = const Value.absent(), + this.modifiedAt = const Value.absent(), + this.ackByUser = const Value.absent(), + this.ackByServer = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupId = Value(groupId), + messageId = Value(messageId), + type = Value(type); + static Insertable<MessagesData> custom({ + Expression<String>? groupId, + Expression<String>? messageId, + Expression<int>? senderId, + Expression<String>? type, + Expression<String>? content, + Expression<String>? mediaId, + Expression<i2.Uint8List>? additionalMessageData, + Expression<bool>? mediaStored, + Expression<bool>? mediaReopened, + Expression<i2.Uint8List>? downloadToken, + Expression<String>? quotesMessageId, + Expression<bool>? isDeletedFromSender, + Expression<DateTime>? openedAt, + Expression<DateTime>? openedByAll, + Expression<DateTime>? createdAt, + Expression<DateTime>? modifiedAt, + Expression<DateTime>? ackByUser, + Expression<DateTime>? ackByServer, + Expression<int>? rowid, + }) { + return RawValuesInsertable({ + if (groupId != null) 'group_id': groupId, + if (messageId != null) 'message_id': messageId, + if (senderId != null) 'sender_id': senderId, + if (type != null) 'type': type, + if (content != null) 'content': content, + if (mediaId != null) 'media_id': mediaId, + if (additionalMessageData != null) + 'additional_message_data': additionalMessageData, + if (mediaStored != null) 'media_stored': mediaStored, + if (mediaReopened != null) 'media_reopened': mediaReopened, + if (downloadToken != null) 'download_token': downloadToken, + if (quotesMessageId != null) 'quotes_message_id': quotesMessageId, + if (isDeletedFromSender != null) + 'is_deleted_from_sender': isDeletedFromSender, + if (openedAt != null) 'opened_at': openedAt, + if (openedByAll != null) 'opened_by_all': openedByAll, + if (createdAt != null) 'created_at': createdAt, + if (modifiedAt != null) 'modified_at': modifiedAt, + if (ackByUser != null) 'ack_by_user': ackByUser, + if (ackByServer != null) 'ack_by_server': ackByServer, + if (rowid != null) 'rowid': rowid, + }); + } + + MessagesCompanion copyWith( + {Value<String>? groupId, + Value<String>? messageId, + Value<int?>? senderId, + Value<String>? type, + Value<String?>? content, + Value<String?>? mediaId, + Value<i2.Uint8List?>? additionalMessageData, + Value<bool>? mediaStored, + Value<bool>? mediaReopened, + Value<i2.Uint8List?>? downloadToken, + Value<String?>? quotesMessageId, + Value<bool>? isDeletedFromSender, + Value<DateTime?>? openedAt, + Value<DateTime?>? openedByAll, + Value<DateTime>? createdAt, + Value<DateTime?>? modifiedAt, + Value<DateTime?>? ackByUser, + Value<DateTime?>? ackByServer, + Value<int>? rowid}) { + return MessagesCompanion( + groupId: groupId ?? this.groupId, + messageId: messageId ?? this.messageId, + senderId: senderId ?? this.senderId, + type: type ?? this.type, + content: content ?? this.content, + mediaId: mediaId ?? this.mediaId, + additionalMessageData: + additionalMessageData ?? this.additionalMessageData, + mediaStored: mediaStored ?? this.mediaStored, + mediaReopened: mediaReopened ?? this.mediaReopened, + downloadToken: downloadToken ?? this.downloadToken, + quotesMessageId: quotesMessageId ?? this.quotesMessageId, + isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, + openedAt: openedAt ?? this.openedAt, + openedByAll: openedByAll ?? this.openedByAll, + createdAt: createdAt ?? this.createdAt, + modifiedAt: modifiedAt ?? this.modifiedAt, + ackByUser: ackByUser ?? this.ackByUser, + ackByServer: ackByServer ?? this.ackByServer, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + if (groupId.present) { + map['group_id'] = Variable<String>(groupId.value); + } + if (messageId.present) { + map['message_id'] = Variable<String>(messageId.value); + } + if (senderId.present) { + map['sender_id'] = Variable<int>(senderId.value); + } + if (type.present) { + map['type'] = Variable<String>(type.value); + } + if (content.present) { + map['content'] = Variable<String>(content.value); + } + if (mediaId.present) { + map['media_id'] = Variable<String>(mediaId.value); + } + if (additionalMessageData.present) { + map['additional_message_data'] = + Variable<i2.Uint8List>(additionalMessageData.value); + } + if (mediaStored.present) { + map['media_stored'] = Variable<bool>(mediaStored.value); + } + if (mediaReopened.present) { + map['media_reopened'] = Variable<bool>(mediaReopened.value); + } + if (downloadToken.present) { + map['download_token'] = Variable<i2.Uint8List>(downloadToken.value); + } + if (quotesMessageId.present) { + map['quotes_message_id'] = Variable<String>(quotesMessageId.value); + } + if (isDeletedFromSender.present) { + map['is_deleted_from_sender'] = Variable<bool>(isDeletedFromSender.value); + } + if (openedAt.present) { + map['opened_at'] = Variable<DateTime>(openedAt.value); + } + if (openedByAll.present) { + map['opened_by_all'] = Variable<DateTime>(openedByAll.value); + } + if (createdAt.present) { + map['created_at'] = Variable<DateTime>(createdAt.value); + } + if (modifiedAt.present) { + map['modified_at'] = Variable<DateTime>(modifiedAt.value); + } + if (ackByUser.present) { + map['ack_by_user'] = Variable<DateTime>(ackByUser.value); + } + if (ackByServer.present) { + map['ack_by_server'] = Variable<DateTime>(ackByServer.value); + } + if (rowid.present) { + map['rowid'] = Variable<int>(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessagesCompanion(') + ..write('groupId: $groupId, ') + ..write('messageId: $messageId, ') + ..write('senderId: $senderId, ') + ..write('type: $type, ') + ..write('content: $content, ') + ..write('mediaId: $mediaId, ') + ..write('additionalMessageData: $additionalMessageData, ') + ..write('mediaStored: $mediaStored, ') + ..write('mediaReopened: $mediaReopened, ') + ..write('downloadToken: $downloadToken, ') + ..write('quotesMessageId: $quotesMessageId, ') + ..write('isDeletedFromSender: $isDeletedFromSender, ') + ..write('openedAt: $openedAt, ') + ..write('openedByAll: $openedByAll, ') + ..write('createdAt: $createdAt, ') + ..write('modifiedAt: $modifiedAt, ') + ..write('ackByUser: $ackByUser, ') + ..write('ackByServer: $ackByServer, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class MessageHistories extends Table + with TableInfo<MessageHistories, MessageHistoriesData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MessageHistories(this.attachedDatabase, [this._alias]); + late final GeneratedColumn<int> id = GeneratedColumn<int>( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn<String> messageId = GeneratedColumn<String>( + 'message_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES messages (message_id) ON DELETE CASCADE')); + late final GeneratedColumn<int> contactId = GeneratedColumn<int>( + 'contact_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn<String> content = GeneratedColumn<String>( + 'content', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); + @override + List<GeneratedColumn> get $columns => + [id, messageId, contactId, content, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'message_histories'; + @override + Set<GeneratedColumn> get $primaryKey => {id}; + @override + MessageHistoriesData map(Map<String, dynamic> data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MessageHistoriesData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id']), + content: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}content']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + MessageHistories createAlias(String alias) { + return MessageHistories(attachedDatabase, alias); + } +} + +class MessageHistoriesData extends DataClass + implements Insertable<MessageHistoriesData> { + final int id; + final String messageId; + final int? contactId; + final String? content; + final DateTime createdAt; + const MessageHistoriesData( + {required this.id, + required this.messageId, + this.contactId, + this.content, + required this.createdAt}); + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + map['id'] = Variable<int>(id); + map['message_id'] = Variable<String>(messageId); + if (!nullToAbsent || contactId != null) { + map['contact_id'] = Variable<int>(contactId); + } + if (!nullToAbsent || content != null) { + map['content'] = Variable<String>(content); + } + map['created_at'] = Variable<DateTime>(createdAt); + return map; + } + + MessageHistoriesCompanion toCompanion(bool nullToAbsent) { + return MessageHistoriesCompanion( + id: Value(id), + messageId: Value(messageId), + contactId: contactId == null && nullToAbsent + ? const Value.absent() + : Value(contactId), + content: content == null && nullToAbsent + ? const Value.absent() + : Value(content), + createdAt: Value(createdAt), + ); + } + + factory MessageHistoriesData.fromJson(Map<String, dynamic> json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MessageHistoriesData( + id: serializer.fromJson<int>(json['id']), + messageId: serializer.fromJson<String>(json['messageId']), + contactId: serializer.fromJson<int?>(json['contactId']), + content: serializer.fromJson<String?>(json['content']), + createdAt: serializer.fromJson<DateTime>(json['createdAt']), + ); + } + @override + Map<String, dynamic> toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return <String, dynamic>{ + 'id': serializer.toJson<int>(id), + 'messageId': serializer.toJson<String>(messageId), + 'contactId': serializer.toJson<int?>(contactId), + 'content': serializer.toJson<String?>(content), + 'createdAt': serializer.toJson<DateTime>(createdAt), + }; + } + + MessageHistoriesData copyWith( + {int? id, + String? messageId, + Value<int?> contactId = const Value.absent(), + Value<String?> content = const Value.absent(), + DateTime? createdAt}) => + MessageHistoriesData( + id: id ?? this.id, + messageId: messageId ?? this.messageId, + contactId: contactId.present ? contactId.value : this.contactId, + content: content.present ? content.value : this.content, + createdAt: createdAt ?? this.createdAt, + ); + MessageHistoriesData copyWithCompanion(MessageHistoriesCompanion data) { + return MessageHistoriesData( + id: data.id.present ? data.id.value : this.id, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + content: data.content.present ? data.content.value : this.content, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('MessageHistoriesData(') + ..write('id: $id, ') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('content: $content, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, messageId, contactId, content, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MessageHistoriesData && + other.id == this.id && + other.messageId == this.messageId && + other.contactId == this.contactId && + other.content == this.content && + other.createdAt == this.createdAt); +} + +class MessageHistoriesCompanion extends UpdateCompanion<MessageHistoriesData> { + final Value<int> id; + final Value<String> messageId; + final Value<int?> contactId; + final Value<String?> content; + final Value<DateTime> createdAt; + const MessageHistoriesCompanion({ + this.id = const Value.absent(), + this.messageId = const Value.absent(), + this.contactId = const Value.absent(), + this.content = const Value.absent(), + this.createdAt = const Value.absent(), + }); + MessageHistoriesCompanion.insert({ + this.id = const Value.absent(), + required String messageId, + this.contactId = const Value.absent(), + this.content = const Value.absent(), + this.createdAt = const Value.absent(), + }) : messageId = Value(messageId); + static Insertable<MessageHistoriesData> custom({ + Expression<int>? id, + Expression<String>? messageId, + Expression<int>? contactId, + Expression<String>? content, + Expression<DateTime>? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (messageId != null) 'message_id': messageId, + if (contactId != null) 'contact_id': contactId, + if (content != null) 'content': content, + if (createdAt != null) 'created_at': createdAt, + }); + } + + MessageHistoriesCompanion copyWith( + {Value<int>? id, + Value<String>? messageId, + Value<int?>? contactId, + Value<String?>? content, + Value<DateTime>? createdAt}) { + return MessageHistoriesCompanion( + id: id ?? this.id, + messageId: messageId ?? this.messageId, + contactId: contactId ?? this.contactId, + content: content ?? this.content, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + if (id.present) { + map['id'] = Variable<int>(id.value); + } + if (messageId.present) { + map['message_id'] = Variable<String>(messageId.value); + } + if (contactId.present) { + map['contact_id'] = Variable<int>(contactId.value); + } + if (content.present) { + map['content'] = Variable<String>(content.value); + } + if (createdAt.present) { + map['created_at'] = Variable<DateTime>(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessageHistoriesCompanion(') + ..write('id: $id, ') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('content: $content, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class Reactions extends Table with TableInfo<Reactions, ReactionsData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Reactions(this.attachedDatabase, [this._alias]); + late final GeneratedColumn<String> messageId = GeneratedColumn<String>( + 'message_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES messages (message_id) ON DELETE CASCADE')); + late final GeneratedColumn<String> emoji = GeneratedColumn<String>( + 'emoji', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn<int> senderId = GeneratedColumn<int>( + 'sender_id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES contacts (user_id) ON DELETE CASCADE')); + late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); + @override + List<GeneratedColumn> get $columns => [messageId, emoji, senderId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'reactions'; + @override + Set<GeneratedColumn> get $primaryKey => {messageId, senderId, emoji}; + @override + ReactionsData map(Map<String, dynamic> data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ReactionsData( + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, + emoji: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}emoji'])!, + senderId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}sender_id']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + Reactions createAlias(String alias) { + return Reactions(attachedDatabase, alias); + } +} + +class ReactionsData extends DataClass implements Insertable<ReactionsData> { + final String messageId; + final String emoji; + final int? senderId; + final DateTime createdAt; + const ReactionsData( + {required this.messageId, + required this.emoji, + this.senderId, + required this.createdAt}); + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + map['message_id'] = Variable<String>(messageId); + map['emoji'] = Variable<String>(emoji); + if (!nullToAbsent || senderId != null) { + map['sender_id'] = Variable<int>(senderId); + } + map['created_at'] = Variable<DateTime>(createdAt); + return map; + } + + ReactionsCompanion toCompanion(bool nullToAbsent) { + return ReactionsCompanion( + messageId: Value(messageId), + emoji: Value(emoji), + senderId: senderId == null && nullToAbsent + ? const Value.absent() + : Value(senderId), + createdAt: Value(createdAt), + ); + } + + factory ReactionsData.fromJson(Map<String, dynamic> json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ReactionsData( + messageId: serializer.fromJson<String>(json['messageId']), + emoji: serializer.fromJson<String>(json['emoji']), + senderId: serializer.fromJson<int?>(json['senderId']), + createdAt: serializer.fromJson<DateTime>(json['createdAt']), + ); + } + @override + Map<String, dynamic> toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return <String, dynamic>{ + 'messageId': serializer.toJson<String>(messageId), + 'emoji': serializer.toJson<String>(emoji), + 'senderId': serializer.toJson<int?>(senderId), + 'createdAt': serializer.toJson<DateTime>(createdAt), + }; + } + + ReactionsData copyWith( + {String? messageId, + String? emoji, + Value<int?> senderId = const Value.absent(), + DateTime? createdAt}) => + ReactionsData( + messageId: messageId ?? this.messageId, + emoji: emoji ?? this.emoji, + senderId: senderId.present ? senderId.value : this.senderId, + createdAt: createdAt ?? this.createdAt, + ); + ReactionsData copyWithCompanion(ReactionsCompanion data) { + return ReactionsData( + messageId: data.messageId.present ? data.messageId.value : this.messageId, + emoji: data.emoji.present ? data.emoji.value : this.emoji, + senderId: data.senderId.present ? data.senderId.value : this.senderId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('ReactionsData(') + ..write('messageId: $messageId, ') + ..write('emoji: $emoji, ') + ..write('senderId: $senderId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(messageId, emoji, senderId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ReactionsData && + other.messageId == this.messageId && + other.emoji == this.emoji && + other.senderId == this.senderId && + other.createdAt == this.createdAt); +} + +class ReactionsCompanion extends UpdateCompanion<ReactionsData> { + final Value<String> messageId; + final Value<String> emoji; + final Value<int?> senderId; + final Value<DateTime> createdAt; + final Value<int> rowid; + const ReactionsCompanion({ + this.messageId = const Value.absent(), + this.emoji = const Value.absent(), + this.senderId = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ReactionsCompanion.insert({ + required String messageId, + required String emoji, + this.senderId = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : messageId = Value(messageId), + emoji = Value(emoji); + static Insertable<ReactionsData> custom({ + Expression<String>? messageId, + Expression<String>? emoji, + Expression<int>? senderId, + Expression<DateTime>? createdAt, + Expression<int>? rowid, + }) { + return RawValuesInsertable({ + if (messageId != null) 'message_id': messageId, + if (emoji != null) 'emoji': emoji, + if (senderId != null) 'sender_id': senderId, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ReactionsCompanion copyWith( + {Value<String>? messageId, + Value<String>? emoji, + Value<int?>? senderId, + Value<DateTime>? createdAt, + Value<int>? rowid}) { + return ReactionsCompanion( + messageId: messageId ?? this.messageId, + emoji: emoji ?? this.emoji, + senderId: senderId ?? this.senderId, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + if (messageId.present) { + map['message_id'] = Variable<String>(messageId.value); + } + if (emoji.present) { + map['emoji'] = Variable<String>(emoji.value); + } + if (senderId.present) { + map['sender_id'] = Variable<int>(senderId.value); + } + if (createdAt.present) { + map['created_at'] = Variable<DateTime>(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable<int>(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ReactionsCompanion(') + ..write('messageId: $messageId, ') + ..write('emoji: $emoji, ') + ..write('senderId: $senderId, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class GroupMembers extends Table + with TableInfo<GroupMembers, GroupMembersData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + GroupMembers(this.attachedDatabase, [this._alias]); + late final GeneratedColumn<String> groupId = GeneratedColumn<String>( + 'group_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES "groups" (group_id) ON DELETE CASCADE')); + late final GeneratedColumn<int> contactId = GeneratedColumn<int>( + 'contact_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)')); + late final GeneratedColumn<String> memberState = GeneratedColumn<String>( + 'member_state', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn<i2.Uint8List> groupPublicKey = + GeneratedColumn<i2.Uint8List>('group_public_key', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + late final GeneratedColumn<DateTime> lastMessage = GeneratedColumn<DateTime>( + 'last_message', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); + @override + List<GeneratedColumn> get $columns => + [groupId, contactId, memberState, groupPublicKey, lastMessage, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'group_members'; + @override + Set<GeneratedColumn> get $primaryKey => {groupId, contactId}; + @override + GroupMembersData map(Map<String, dynamic> data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return GroupMembersData( + groupId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_id'])!, + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, + memberState: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}member_state']), + groupPublicKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}group_public_key']), + lastMessage: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}last_message']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + GroupMembers createAlias(String alias) { + return GroupMembers(attachedDatabase, alias); + } +} + +class GroupMembersData extends DataClass + implements Insertable<GroupMembersData> { + final String groupId; + final int contactId; + final String? memberState; + final i2.Uint8List? groupPublicKey; + final DateTime? lastMessage; + final DateTime createdAt; + const GroupMembersData( + {required this.groupId, + required this.contactId, + this.memberState, + this.groupPublicKey, + this.lastMessage, + required this.createdAt}); + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + map['group_id'] = Variable<String>(groupId); + map['contact_id'] = Variable<int>(contactId); + if (!nullToAbsent || memberState != null) { + map['member_state'] = Variable<String>(memberState); + } + if (!nullToAbsent || groupPublicKey != null) { + map['group_public_key'] = Variable<i2.Uint8List>(groupPublicKey); + } + if (!nullToAbsent || lastMessage != null) { + map['last_message'] = Variable<DateTime>(lastMessage); + } + map['created_at'] = Variable<DateTime>(createdAt); + return map; + } + + GroupMembersCompanion toCompanion(bool nullToAbsent) { + return GroupMembersCompanion( + groupId: Value(groupId), + contactId: Value(contactId), + memberState: memberState == null && nullToAbsent + ? const Value.absent() + : Value(memberState), + groupPublicKey: groupPublicKey == null && nullToAbsent + ? const Value.absent() + : Value(groupPublicKey), + lastMessage: lastMessage == null && nullToAbsent + ? const Value.absent() + : Value(lastMessage), + createdAt: Value(createdAt), + ); + } + + factory GroupMembersData.fromJson(Map<String, dynamic> json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GroupMembersData( + groupId: serializer.fromJson<String>(json['groupId']), + contactId: serializer.fromJson<int>(json['contactId']), + memberState: serializer.fromJson<String?>(json['memberState']), + groupPublicKey: + serializer.fromJson<i2.Uint8List?>(json['groupPublicKey']), + lastMessage: serializer.fromJson<DateTime?>(json['lastMessage']), + createdAt: serializer.fromJson<DateTime>(json['createdAt']), + ); + } + @override + Map<String, dynamic> toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return <String, dynamic>{ + 'groupId': serializer.toJson<String>(groupId), + 'contactId': serializer.toJson<int>(contactId), + 'memberState': serializer.toJson<String?>(memberState), + 'groupPublicKey': serializer.toJson<i2.Uint8List?>(groupPublicKey), + 'lastMessage': serializer.toJson<DateTime?>(lastMessage), + 'createdAt': serializer.toJson<DateTime>(createdAt), + }; + } + + GroupMembersData copyWith( + {String? groupId, + int? contactId, + Value<String?> memberState = const Value.absent(), + Value<i2.Uint8List?> groupPublicKey = const Value.absent(), + Value<DateTime?> lastMessage = const Value.absent(), + DateTime? createdAt}) => + GroupMembersData( + groupId: groupId ?? this.groupId, + contactId: contactId ?? this.contactId, + memberState: memberState.present ? memberState.value : this.memberState, + groupPublicKey: + groupPublicKey.present ? groupPublicKey.value : this.groupPublicKey, + lastMessage: lastMessage.present ? lastMessage.value : this.lastMessage, + createdAt: createdAt ?? this.createdAt, + ); + GroupMembersData copyWithCompanion(GroupMembersCompanion data) { + return GroupMembersData( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + memberState: + data.memberState.present ? data.memberState.value : this.memberState, + groupPublicKey: data.groupPublicKey.present + ? data.groupPublicKey.value + : this.groupPublicKey, + lastMessage: + data.lastMessage.present ? data.lastMessage.value : this.lastMessage, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('GroupMembersData(') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('memberState: $memberState, ') + ..write('groupPublicKey: $groupPublicKey, ') + ..write('lastMessage: $lastMessage, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(groupId, contactId, memberState, + $driftBlobEquality.hash(groupPublicKey), lastMessage, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GroupMembersData && + other.groupId == this.groupId && + other.contactId == this.contactId && + other.memberState == this.memberState && + $driftBlobEquality.equals( + other.groupPublicKey, this.groupPublicKey) && + other.lastMessage == this.lastMessage && + other.createdAt == this.createdAt); +} + +class GroupMembersCompanion extends UpdateCompanion<GroupMembersData> { + final Value<String> groupId; + final Value<int> contactId; + final Value<String?> memberState; + final Value<i2.Uint8List?> groupPublicKey; + final Value<DateTime?> lastMessage; + final Value<DateTime> createdAt; + final Value<int> rowid; + const GroupMembersCompanion({ + this.groupId = const Value.absent(), + this.contactId = const Value.absent(), + this.memberState = const Value.absent(), + this.groupPublicKey = const Value.absent(), + this.lastMessage = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupMembersCompanion.insert({ + required String groupId, + required int contactId, + this.memberState = const Value.absent(), + this.groupPublicKey = const Value.absent(), + this.lastMessage = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupId = Value(groupId), + contactId = Value(contactId); + static Insertable<GroupMembersData> custom({ + Expression<String>? groupId, + Expression<int>? contactId, + Expression<String>? memberState, + Expression<i2.Uint8List>? groupPublicKey, + Expression<DateTime>? lastMessage, + Expression<DateTime>? createdAt, + Expression<int>? rowid, + }) { + return RawValuesInsertable({ + if (groupId != null) 'group_id': groupId, + if (contactId != null) 'contact_id': contactId, + if (memberState != null) 'member_state': memberState, + if (groupPublicKey != null) 'group_public_key': groupPublicKey, + if (lastMessage != null) 'last_message': lastMessage, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupMembersCompanion copyWith( + {Value<String>? groupId, + Value<int>? contactId, + Value<String?>? memberState, + Value<i2.Uint8List?>? groupPublicKey, + Value<DateTime?>? lastMessage, + Value<DateTime>? createdAt, + Value<int>? rowid}) { + return GroupMembersCompanion( + groupId: groupId ?? this.groupId, + contactId: contactId ?? this.contactId, + memberState: memberState ?? this.memberState, + groupPublicKey: groupPublicKey ?? this.groupPublicKey, + lastMessage: lastMessage ?? this.lastMessage, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + if (groupId.present) { + map['group_id'] = Variable<String>(groupId.value); + } + if (contactId.present) { + map['contact_id'] = Variable<int>(contactId.value); + } + if (memberState.present) { + map['member_state'] = Variable<String>(memberState.value); + } + if (groupPublicKey.present) { + map['group_public_key'] = Variable<i2.Uint8List>(groupPublicKey.value); + } + if (lastMessage.present) { + map['last_message'] = Variable<DateTime>(lastMessage.value); + } + if (createdAt.present) { + map['created_at'] = Variable<DateTime>(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable<int>(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupMembersCompanion(') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('memberState: $memberState, ') + ..write('groupPublicKey: $groupPublicKey, ') + ..write('lastMessage: $lastMessage, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Receipts extends Table with TableInfo<Receipts, ReceiptsData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Receipts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn<String> receiptId = GeneratedColumn<String>( + 'receipt_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn<int> contactId = GeneratedColumn<int>( + 'contact_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES contacts (user_id) ON DELETE CASCADE')); + late final GeneratedColumn<String> messageId = GeneratedColumn<String>( + 'message_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES messages (message_id) ON DELETE CASCADE')); + late final GeneratedColumn<i2.Uint8List> message = + GeneratedColumn<i2.Uint8List>('message', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + late final GeneratedColumn<bool> contactWillSendsReceipt = + GeneratedColumn<bool>('contact_will_sends_receipt', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("contact_will_sends_receipt" IN (0, 1))'), + defaultValue: const CustomExpression('1')); + late final GeneratedColumn<DateTime> markForRetry = GeneratedColumn<DateTime>( + 'mark_for_retry', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn<DateTime> markForRetryAfterAccepted = + GeneratedColumn<DateTime>( + 'mark_for_retry_after_accepted', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn<DateTime> ackByServerAt = + GeneratedColumn<DateTime>('ack_by_server_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn<int> retryCount = GeneratedColumn<int>( + 'retry_count', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0')); + late final GeneratedColumn<DateTime> lastRetry = GeneratedColumn<DateTime>( + 'last_retry', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); + @override + List<GeneratedColumn> get $columns => [ + receiptId, + contactId, + messageId, + message, + contactWillSendsReceipt, + markForRetry, + markForRetryAfterAccepted, + ackByServerAt, + retryCount, + lastRetry, + createdAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'receipts'; + @override + Set<GeneratedColumn> get $primaryKey => {receiptId}; + @override + ReceiptsData map(Map<String, dynamic> data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ReceiptsData( + receiptId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}receipt_id'])!, + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id']), + message: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}message'])!, + contactWillSendsReceipt: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}contact_will_sends_receipt'])!, + markForRetry: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}mark_for_retry']), + markForRetryAfterAccepted: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}mark_for_retry_after_accepted']), + ackByServerAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}ack_by_server_at']), + retryCount: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}retry_count'])!, + lastRetry: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}last_retry']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + Receipts createAlias(String alias) { + return Receipts(attachedDatabase, alias); + } +} + +class ReceiptsData extends DataClass implements Insertable<ReceiptsData> { + final String receiptId; + final int contactId; + final String? messageId; + final i2.Uint8List message; + final bool contactWillSendsReceipt; + final DateTime? markForRetry; + final DateTime? markForRetryAfterAccepted; + final DateTime? ackByServerAt; + final int retryCount; + final DateTime? lastRetry; + final DateTime createdAt; + const ReceiptsData( + {required this.receiptId, + required this.contactId, + this.messageId, + required this.message, + required this.contactWillSendsReceipt, + this.markForRetry, + this.markForRetryAfterAccepted, + this.ackByServerAt, + required this.retryCount, + this.lastRetry, + required this.createdAt}); + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + map['receipt_id'] = Variable<String>(receiptId); + map['contact_id'] = Variable<int>(contactId); + if (!nullToAbsent || messageId != null) { + map['message_id'] = Variable<String>(messageId); + } + map['message'] = Variable<i2.Uint8List>(message); + map['contact_will_sends_receipt'] = Variable<bool>(contactWillSendsReceipt); + if (!nullToAbsent || markForRetry != null) { + map['mark_for_retry'] = Variable<DateTime>(markForRetry); + } + if (!nullToAbsent || markForRetryAfterAccepted != null) { + map['mark_for_retry_after_accepted'] = + Variable<DateTime>(markForRetryAfterAccepted); + } + if (!nullToAbsent || ackByServerAt != null) { + map['ack_by_server_at'] = Variable<DateTime>(ackByServerAt); + } + map['retry_count'] = Variable<int>(retryCount); + if (!nullToAbsent || lastRetry != null) { + map['last_retry'] = Variable<DateTime>(lastRetry); + } + map['created_at'] = Variable<DateTime>(createdAt); + return map; + } + + ReceiptsCompanion toCompanion(bool nullToAbsent) { + return ReceiptsCompanion( + receiptId: Value(receiptId), + contactId: Value(contactId), + messageId: messageId == null && nullToAbsent + ? const Value.absent() + : Value(messageId), + message: Value(message), + contactWillSendsReceipt: Value(contactWillSendsReceipt), + markForRetry: markForRetry == null && nullToAbsent + ? const Value.absent() + : Value(markForRetry), + markForRetryAfterAccepted: + markForRetryAfterAccepted == null && nullToAbsent + ? const Value.absent() + : Value(markForRetryAfterAccepted), + ackByServerAt: ackByServerAt == null && nullToAbsent + ? const Value.absent() + : Value(ackByServerAt), + retryCount: Value(retryCount), + lastRetry: lastRetry == null && nullToAbsent + ? const Value.absent() + : Value(lastRetry), + createdAt: Value(createdAt), + ); + } + + factory ReceiptsData.fromJson(Map<String, dynamic> json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ReceiptsData( + receiptId: serializer.fromJson<String>(json['receiptId']), + contactId: serializer.fromJson<int>(json['contactId']), + messageId: serializer.fromJson<String?>(json['messageId']), + message: serializer.fromJson<i2.Uint8List>(json['message']), + contactWillSendsReceipt: + serializer.fromJson<bool>(json['contactWillSendsReceipt']), + markForRetry: serializer.fromJson<DateTime?>(json['markForRetry']), + markForRetryAfterAccepted: + serializer.fromJson<DateTime?>(json['markForRetryAfterAccepted']), + ackByServerAt: serializer.fromJson<DateTime?>(json['ackByServerAt']), + retryCount: serializer.fromJson<int>(json['retryCount']), + lastRetry: serializer.fromJson<DateTime?>(json['lastRetry']), + createdAt: serializer.fromJson<DateTime>(json['createdAt']), + ); + } + @override + Map<String, dynamic> toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return <String, dynamic>{ + 'receiptId': serializer.toJson<String>(receiptId), + 'contactId': serializer.toJson<int>(contactId), + 'messageId': serializer.toJson<String?>(messageId), + 'message': serializer.toJson<i2.Uint8List>(message), + 'contactWillSendsReceipt': + serializer.toJson<bool>(contactWillSendsReceipt), + 'markForRetry': serializer.toJson<DateTime?>(markForRetry), + 'markForRetryAfterAccepted': + serializer.toJson<DateTime?>(markForRetryAfterAccepted), + 'ackByServerAt': serializer.toJson<DateTime?>(ackByServerAt), + 'retryCount': serializer.toJson<int>(retryCount), + 'lastRetry': serializer.toJson<DateTime?>(lastRetry), + 'createdAt': serializer.toJson<DateTime>(createdAt), + }; + } + + ReceiptsData copyWith( + {String? receiptId, + int? contactId, + Value<String?> messageId = const Value.absent(), + i2.Uint8List? message, + bool? contactWillSendsReceipt, + Value<DateTime?> markForRetry = const Value.absent(), + Value<DateTime?> markForRetryAfterAccepted = const Value.absent(), + Value<DateTime?> ackByServerAt = const Value.absent(), + int? retryCount, + Value<DateTime?> lastRetry = const Value.absent(), + DateTime? createdAt}) => + ReceiptsData( + receiptId: receiptId ?? this.receiptId, + contactId: contactId ?? this.contactId, + messageId: messageId.present ? messageId.value : this.messageId, + message: message ?? this.message, + contactWillSendsReceipt: + contactWillSendsReceipt ?? this.contactWillSendsReceipt, + markForRetry: + markForRetry.present ? markForRetry.value : this.markForRetry, + markForRetryAfterAccepted: markForRetryAfterAccepted.present + ? markForRetryAfterAccepted.value + : this.markForRetryAfterAccepted, + ackByServerAt: + ackByServerAt.present ? ackByServerAt.value : this.ackByServerAt, + retryCount: retryCount ?? this.retryCount, + lastRetry: lastRetry.present ? lastRetry.value : this.lastRetry, + createdAt: createdAt ?? this.createdAt, + ); + ReceiptsData copyWithCompanion(ReceiptsCompanion data) { + return ReceiptsData( + receiptId: data.receiptId.present ? data.receiptId.value : this.receiptId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + message: data.message.present ? data.message.value : this.message, + contactWillSendsReceipt: data.contactWillSendsReceipt.present + ? data.contactWillSendsReceipt.value + : this.contactWillSendsReceipt, + markForRetry: data.markForRetry.present + ? data.markForRetry.value + : this.markForRetry, + markForRetryAfterAccepted: data.markForRetryAfterAccepted.present + ? data.markForRetryAfterAccepted.value + : this.markForRetryAfterAccepted, + ackByServerAt: data.ackByServerAt.present + ? data.ackByServerAt.value + : this.ackByServerAt, + retryCount: + data.retryCount.present ? data.retryCount.value : this.retryCount, + lastRetry: data.lastRetry.present ? data.lastRetry.value : this.lastRetry, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('ReceiptsData(') + ..write('receiptId: $receiptId, ') + ..write('contactId: $contactId, ') + ..write('messageId: $messageId, ') + ..write('message: $message, ') + ..write('contactWillSendsReceipt: $contactWillSendsReceipt, ') + ..write('markForRetry: $markForRetry, ') + ..write('markForRetryAfterAccepted: $markForRetryAfterAccepted, ') + ..write('ackByServerAt: $ackByServerAt, ') + ..write('retryCount: $retryCount, ') + ..write('lastRetry: $lastRetry, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + receiptId, + contactId, + messageId, + $driftBlobEquality.hash(message), + contactWillSendsReceipt, + markForRetry, + markForRetryAfterAccepted, + ackByServerAt, + retryCount, + lastRetry, + createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ReceiptsData && + other.receiptId == this.receiptId && + other.contactId == this.contactId && + other.messageId == this.messageId && + $driftBlobEquality.equals(other.message, this.message) && + other.contactWillSendsReceipt == this.contactWillSendsReceipt && + other.markForRetry == this.markForRetry && + other.markForRetryAfterAccepted == this.markForRetryAfterAccepted && + other.ackByServerAt == this.ackByServerAt && + other.retryCount == this.retryCount && + other.lastRetry == this.lastRetry && + other.createdAt == this.createdAt); +} + +class ReceiptsCompanion extends UpdateCompanion<ReceiptsData> { + final Value<String> receiptId; + final Value<int> contactId; + final Value<String?> messageId; + final Value<i2.Uint8List> message; + final Value<bool> contactWillSendsReceipt; + final Value<DateTime?> markForRetry; + final Value<DateTime?> markForRetryAfterAccepted; + final Value<DateTime?> ackByServerAt; + final Value<int> retryCount; + final Value<DateTime?> lastRetry; + final Value<DateTime> createdAt; + final Value<int> rowid; + const ReceiptsCompanion({ + this.receiptId = const Value.absent(), + this.contactId = const Value.absent(), + this.messageId = const Value.absent(), + this.message = const Value.absent(), + this.contactWillSendsReceipt = const Value.absent(), + this.markForRetry = const Value.absent(), + this.markForRetryAfterAccepted = const Value.absent(), + this.ackByServerAt = const Value.absent(), + this.retryCount = const Value.absent(), + this.lastRetry = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ReceiptsCompanion.insert({ + required String receiptId, + required int contactId, + this.messageId = const Value.absent(), + required i2.Uint8List message, + this.contactWillSendsReceipt = const Value.absent(), + this.markForRetry = const Value.absent(), + this.markForRetryAfterAccepted = const Value.absent(), + this.ackByServerAt = const Value.absent(), + this.retryCount = const Value.absent(), + this.lastRetry = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : receiptId = Value(receiptId), + contactId = Value(contactId), + message = Value(message); + static Insertable<ReceiptsData> custom({ + Expression<String>? receiptId, + Expression<int>? contactId, + Expression<String>? messageId, + Expression<i2.Uint8List>? message, + Expression<bool>? contactWillSendsReceipt, + Expression<DateTime>? markForRetry, + Expression<DateTime>? markForRetryAfterAccepted, + Expression<DateTime>? ackByServerAt, + Expression<int>? retryCount, + Expression<DateTime>? lastRetry, + Expression<DateTime>? createdAt, + Expression<int>? rowid, + }) { + return RawValuesInsertable({ + if (receiptId != null) 'receipt_id': receiptId, + if (contactId != null) 'contact_id': contactId, + if (messageId != null) 'message_id': messageId, + if (message != null) 'message': message, + if (contactWillSendsReceipt != null) + 'contact_will_sends_receipt': contactWillSendsReceipt, + if (markForRetry != null) 'mark_for_retry': markForRetry, + if (markForRetryAfterAccepted != null) + 'mark_for_retry_after_accepted': markForRetryAfterAccepted, + if (ackByServerAt != null) 'ack_by_server_at': ackByServerAt, + if (retryCount != null) 'retry_count': retryCount, + if (lastRetry != null) 'last_retry': lastRetry, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ReceiptsCompanion copyWith( + {Value<String>? receiptId, + Value<int>? contactId, + Value<String?>? messageId, + Value<i2.Uint8List>? message, + Value<bool>? contactWillSendsReceipt, + Value<DateTime?>? markForRetry, + Value<DateTime?>? markForRetryAfterAccepted, + Value<DateTime?>? ackByServerAt, + Value<int>? retryCount, + Value<DateTime?>? lastRetry, + Value<DateTime>? createdAt, + Value<int>? rowid}) { + return ReceiptsCompanion( + receiptId: receiptId ?? this.receiptId, + contactId: contactId ?? this.contactId, + messageId: messageId ?? this.messageId, + message: message ?? this.message, + contactWillSendsReceipt: + contactWillSendsReceipt ?? this.contactWillSendsReceipt, + markForRetry: markForRetry ?? this.markForRetry, + markForRetryAfterAccepted: + markForRetryAfterAccepted ?? this.markForRetryAfterAccepted, + ackByServerAt: ackByServerAt ?? this.ackByServerAt, + retryCount: retryCount ?? this.retryCount, + lastRetry: lastRetry ?? this.lastRetry, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + if (receiptId.present) { + map['receipt_id'] = Variable<String>(receiptId.value); + } + if (contactId.present) { + map['contact_id'] = Variable<int>(contactId.value); + } + if (messageId.present) { + map['message_id'] = Variable<String>(messageId.value); + } + if (message.present) { + map['message'] = Variable<i2.Uint8List>(message.value); + } + if (contactWillSendsReceipt.present) { + map['contact_will_sends_receipt'] = + Variable<bool>(contactWillSendsReceipt.value); + } + if (markForRetry.present) { + map['mark_for_retry'] = Variable<DateTime>(markForRetry.value); + } + if (markForRetryAfterAccepted.present) { + map['mark_for_retry_after_accepted'] = + Variable<DateTime>(markForRetryAfterAccepted.value); + } + if (ackByServerAt.present) { + map['ack_by_server_at'] = Variable<DateTime>(ackByServerAt.value); + } + if (retryCount.present) { + map['retry_count'] = Variable<int>(retryCount.value); + } + if (lastRetry.present) { + map['last_retry'] = Variable<DateTime>(lastRetry.value); + } + if (createdAt.present) { + map['created_at'] = Variable<DateTime>(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable<int>(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ReceiptsCompanion(') + ..write('receiptId: $receiptId, ') + ..write('contactId: $contactId, ') + ..write('messageId: $messageId, ') + ..write('message: $message, ') + ..write('contactWillSendsReceipt: $contactWillSendsReceipt, ') + ..write('markForRetry: $markForRetry, ') + ..write('markForRetryAfterAccepted: $markForRetryAfterAccepted, ') + ..write('ackByServerAt: $ackByServerAt, ') + ..write('retryCount: $retryCount, ') + ..write('lastRetry: $lastRetry, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class ReceivedReceipts extends Table + with TableInfo<ReceivedReceipts, ReceivedReceiptsData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ReceivedReceipts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn<String> receiptId = GeneratedColumn<String>( + 'receipt_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); + @override + List<GeneratedColumn> get $columns => [receiptId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'received_receipts'; + @override + Set<GeneratedColumn> get $primaryKey => {receiptId}; + @override + ReceivedReceiptsData map(Map<String, dynamic> data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ReceivedReceiptsData( + receiptId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}receipt_id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + ReceivedReceipts createAlias(String alias) { + return ReceivedReceipts(attachedDatabase, alias); + } +} + +class ReceivedReceiptsData extends DataClass + implements Insertable<ReceivedReceiptsData> { + final String receiptId; + final DateTime createdAt; + const ReceivedReceiptsData( + {required this.receiptId, required this.createdAt}); + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + map['receipt_id'] = Variable<String>(receiptId); + map['created_at'] = Variable<DateTime>(createdAt); + return map; + } + + ReceivedReceiptsCompanion toCompanion(bool nullToAbsent) { + return ReceivedReceiptsCompanion( + receiptId: Value(receiptId), + createdAt: Value(createdAt), + ); + } + + factory ReceivedReceiptsData.fromJson(Map<String, dynamic> json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ReceivedReceiptsData( + receiptId: serializer.fromJson<String>(json['receiptId']), + createdAt: serializer.fromJson<DateTime>(json['createdAt']), + ); + } + @override + Map<String, dynamic> toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return <String, dynamic>{ + 'receiptId': serializer.toJson<String>(receiptId), + 'createdAt': serializer.toJson<DateTime>(createdAt), + }; + } + + ReceivedReceiptsData copyWith({String? receiptId, DateTime? createdAt}) => + ReceivedReceiptsData( + receiptId: receiptId ?? this.receiptId, + createdAt: createdAt ?? this.createdAt, + ); + ReceivedReceiptsData copyWithCompanion(ReceivedReceiptsCompanion data) { + return ReceivedReceiptsData( + receiptId: data.receiptId.present ? data.receiptId.value : this.receiptId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('ReceivedReceiptsData(') + ..write('receiptId: $receiptId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(receiptId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ReceivedReceiptsData && + other.receiptId == this.receiptId && + other.createdAt == this.createdAt); +} + +class ReceivedReceiptsCompanion extends UpdateCompanion<ReceivedReceiptsData> { + final Value<String> receiptId; + final Value<DateTime> createdAt; + final Value<int> rowid; + const ReceivedReceiptsCompanion({ + this.receiptId = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ReceivedReceiptsCompanion.insert({ + required String receiptId, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : receiptId = Value(receiptId); + static Insertable<ReceivedReceiptsData> custom({ + Expression<String>? receiptId, + Expression<DateTime>? createdAt, + Expression<int>? rowid, + }) { + return RawValuesInsertable({ + if (receiptId != null) 'receipt_id': receiptId, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ReceivedReceiptsCompanion copyWith( + {Value<String>? receiptId, + Value<DateTime>? createdAt, + Value<int>? rowid}) { + return ReceivedReceiptsCompanion( + receiptId: receiptId ?? this.receiptId, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + if (receiptId.present) { + map['receipt_id'] = Variable<String>(receiptId.value); + } + if (createdAt.present) { + map['created_at'] = Variable<DateTime>(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable<int>(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ReceivedReceiptsCompanion(') + ..write('receiptId: $receiptId, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class SignalIdentityKeyStores extends Table + with TableInfo<SignalIdentityKeyStores, SignalIdentityKeyStoresData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SignalIdentityKeyStores(this.attachedDatabase, [this._alias]); + late final GeneratedColumn<int> deviceId = GeneratedColumn<int>( + 'device_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn<String> name = GeneratedColumn<String>( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn<i2.Uint8List> identityKey = + GeneratedColumn<i2.Uint8List>('identity_key', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); + @override + List<GeneratedColumn> get $columns => + [deviceId, name, identityKey, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_identity_key_stores'; + @override + Set<GeneratedColumn> get $primaryKey => {deviceId, name}; + @override + SignalIdentityKeyStoresData map(Map<String, dynamic> data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalIdentityKeyStoresData( + deviceId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}device_id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + identityKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}identity_key'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + SignalIdentityKeyStores createAlias(String alias) { + return SignalIdentityKeyStores(attachedDatabase, alias); + } +} + +class SignalIdentityKeyStoresData extends DataClass + implements Insertable<SignalIdentityKeyStoresData> { + final int deviceId; + final String name; + final i2.Uint8List identityKey; + final DateTime createdAt; + const SignalIdentityKeyStoresData( + {required this.deviceId, + required this.name, + required this.identityKey, + required this.createdAt}); + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + map['device_id'] = Variable<int>(deviceId); + map['name'] = Variable<String>(name); + map['identity_key'] = Variable<i2.Uint8List>(identityKey); + map['created_at'] = Variable<DateTime>(createdAt); + return map; + } + + SignalIdentityKeyStoresCompanion toCompanion(bool nullToAbsent) { + return SignalIdentityKeyStoresCompanion( + deviceId: Value(deviceId), + name: Value(name), + identityKey: Value(identityKey), + createdAt: Value(createdAt), + ); + } + + factory SignalIdentityKeyStoresData.fromJson(Map<String, dynamic> json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalIdentityKeyStoresData( + deviceId: serializer.fromJson<int>(json['deviceId']), + name: serializer.fromJson<String>(json['name']), + identityKey: serializer.fromJson<i2.Uint8List>(json['identityKey']), + createdAt: serializer.fromJson<DateTime>(json['createdAt']), + ); + } + @override + Map<String, dynamic> toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return <String, dynamic>{ + 'deviceId': serializer.toJson<int>(deviceId), + 'name': serializer.toJson<String>(name), + 'identityKey': serializer.toJson<i2.Uint8List>(identityKey), + 'createdAt': serializer.toJson<DateTime>(createdAt), + }; + } + + SignalIdentityKeyStoresData copyWith( + {int? deviceId, + String? name, + i2.Uint8List? identityKey, + DateTime? createdAt}) => + SignalIdentityKeyStoresData( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + identityKey: identityKey ?? this.identityKey, + createdAt: createdAt ?? this.createdAt, + ); + SignalIdentityKeyStoresData copyWithCompanion( + SignalIdentityKeyStoresCompanion data) { + return SignalIdentityKeyStoresData( + deviceId: data.deviceId.present ? data.deviceId.value : this.deviceId, + name: data.name.present ? data.name.value : this.name, + identityKey: + data.identityKey.present ? data.identityKey.value : this.identityKey, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalIdentityKeyStoresData(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('identityKey: $identityKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + deviceId, name, $driftBlobEquality.hash(identityKey), createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalIdentityKeyStoresData && + other.deviceId == this.deviceId && + other.name == this.name && + $driftBlobEquality.equals(other.identityKey, this.identityKey) && + other.createdAt == this.createdAt); +} + +class SignalIdentityKeyStoresCompanion + extends UpdateCompanion<SignalIdentityKeyStoresData> { + final Value<int> deviceId; + final Value<String> name; + final Value<i2.Uint8List> identityKey; + final Value<DateTime> createdAt; + final Value<int> rowid; + const SignalIdentityKeyStoresCompanion({ + this.deviceId = const Value.absent(), + this.name = const Value.absent(), + this.identityKey = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalIdentityKeyStoresCompanion.insert({ + required int deviceId, + required String name, + required i2.Uint8List identityKey, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : deviceId = Value(deviceId), + name = Value(name), + identityKey = Value(identityKey); + static Insertable<SignalIdentityKeyStoresData> custom({ + Expression<int>? deviceId, + Expression<String>? name, + Expression<i2.Uint8List>? identityKey, + Expression<DateTime>? createdAt, + Expression<int>? rowid, + }) { + return RawValuesInsertable({ + if (deviceId != null) 'device_id': deviceId, + if (name != null) 'name': name, + if (identityKey != null) 'identity_key': identityKey, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalIdentityKeyStoresCompanion copyWith( + {Value<int>? deviceId, + Value<String>? name, + Value<i2.Uint8List>? identityKey, + Value<DateTime>? createdAt, + Value<int>? rowid}) { + return SignalIdentityKeyStoresCompanion( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + identityKey: identityKey ?? this.identityKey, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + if (deviceId.present) { + map['device_id'] = Variable<int>(deviceId.value); + } + if (name.present) { + map['name'] = Variable<String>(name.value); + } + if (identityKey.present) { + map['identity_key'] = Variable<i2.Uint8List>(identityKey.value); + } + if (createdAt.present) { + map['created_at'] = Variable<DateTime>(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable<int>(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalIdentityKeyStoresCompanion(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('identityKey: $identityKey, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class SignalPreKeyStores extends Table + with TableInfo<SignalPreKeyStores, SignalPreKeyStoresData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SignalPreKeyStores(this.attachedDatabase, [this._alias]); + late final GeneratedColumn<int> preKeyId = GeneratedColumn<int>( + 'pre_key_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn<i2.Uint8List> preKey = + GeneratedColumn<i2.Uint8List>('pre_key', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); + @override + List<GeneratedColumn> get $columns => [preKeyId, preKey, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_pre_key_stores'; + @override + Set<GeneratedColumn> get $primaryKey => {preKeyId}; + @override + SignalPreKeyStoresData map(Map<String, dynamic> data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalPreKeyStoresData( + preKeyId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}pre_key_id'])!, + preKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}pre_key'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + SignalPreKeyStores createAlias(String alias) { + return SignalPreKeyStores(attachedDatabase, alias); + } +} + +class SignalPreKeyStoresData extends DataClass + implements Insertable<SignalPreKeyStoresData> { + final int preKeyId; + final i2.Uint8List preKey; + final DateTime createdAt; + const SignalPreKeyStoresData( + {required this.preKeyId, required this.preKey, required this.createdAt}); + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + map['pre_key_id'] = Variable<int>(preKeyId); + map['pre_key'] = Variable<i2.Uint8List>(preKey); + map['created_at'] = Variable<DateTime>(createdAt); + return map; + } + + SignalPreKeyStoresCompanion toCompanion(bool nullToAbsent) { + return SignalPreKeyStoresCompanion( + preKeyId: Value(preKeyId), + preKey: Value(preKey), + createdAt: Value(createdAt), + ); + } + + factory SignalPreKeyStoresData.fromJson(Map<String, dynamic> json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalPreKeyStoresData( + preKeyId: serializer.fromJson<int>(json['preKeyId']), + preKey: serializer.fromJson<i2.Uint8List>(json['preKey']), + createdAt: serializer.fromJson<DateTime>(json['createdAt']), + ); + } + @override + Map<String, dynamic> toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return <String, dynamic>{ + 'preKeyId': serializer.toJson<int>(preKeyId), + 'preKey': serializer.toJson<i2.Uint8List>(preKey), + 'createdAt': serializer.toJson<DateTime>(createdAt), + }; + } + + SignalPreKeyStoresData copyWith( + {int? preKeyId, i2.Uint8List? preKey, DateTime? createdAt}) => + SignalPreKeyStoresData( + preKeyId: preKeyId ?? this.preKeyId, + preKey: preKey ?? this.preKey, + createdAt: createdAt ?? this.createdAt, + ); + SignalPreKeyStoresData copyWithCompanion(SignalPreKeyStoresCompanion data) { + return SignalPreKeyStoresData( + preKeyId: data.preKeyId.present ? data.preKeyId.value : this.preKeyId, + preKey: data.preKey.present ? data.preKey.value : this.preKey, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalPreKeyStoresData(') + ..write('preKeyId: $preKeyId, ') + ..write('preKey: $preKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(preKeyId, $driftBlobEquality.hash(preKey), createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalPreKeyStoresData && + other.preKeyId == this.preKeyId && + $driftBlobEquality.equals(other.preKey, this.preKey) && + other.createdAt == this.createdAt); +} + +class SignalPreKeyStoresCompanion + extends UpdateCompanion<SignalPreKeyStoresData> { + final Value<int> preKeyId; + final Value<i2.Uint8List> preKey; + final Value<DateTime> createdAt; + const SignalPreKeyStoresCompanion({ + this.preKeyId = const Value.absent(), + this.preKey = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SignalPreKeyStoresCompanion.insert({ + this.preKeyId = const Value.absent(), + required i2.Uint8List preKey, + this.createdAt = const Value.absent(), + }) : preKey = Value(preKey); + static Insertable<SignalPreKeyStoresData> custom({ + Expression<int>? preKeyId, + Expression<i2.Uint8List>? preKey, + Expression<DateTime>? createdAt, + }) { + return RawValuesInsertable({ + if (preKeyId != null) 'pre_key_id': preKeyId, + if (preKey != null) 'pre_key': preKey, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SignalPreKeyStoresCompanion copyWith( + {Value<int>? preKeyId, + Value<i2.Uint8List>? preKey, + Value<DateTime>? createdAt}) { + return SignalPreKeyStoresCompanion( + preKeyId: preKeyId ?? this.preKeyId, + preKey: preKey ?? this.preKey, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + if (preKeyId.present) { + map['pre_key_id'] = Variable<int>(preKeyId.value); + } + if (preKey.present) { + map['pre_key'] = Variable<i2.Uint8List>(preKey.value); + } + if (createdAt.present) { + map['created_at'] = Variable<DateTime>(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalPreKeyStoresCompanion(') + ..write('preKeyId: $preKeyId, ') + ..write('preKey: $preKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class SignalSenderKeyStores extends Table + with TableInfo<SignalSenderKeyStores, SignalSenderKeyStoresData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SignalSenderKeyStores(this.attachedDatabase, [this._alias]); + late final GeneratedColumn<String> senderKeyName = GeneratedColumn<String>( + 'sender_key_name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn<i2.Uint8List> senderKey = + GeneratedColumn<i2.Uint8List>('sender_key', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + @override + List<GeneratedColumn> get $columns => [senderKeyName, senderKey]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_sender_key_stores'; + @override + Set<GeneratedColumn> get $primaryKey => {senderKeyName}; + @override + SignalSenderKeyStoresData map(Map<String, dynamic> data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalSenderKeyStoresData( + senderKeyName: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}sender_key_name'])!, + senderKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}sender_key'])!, + ); + } + + @override + SignalSenderKeyStores createAlias(String alias) { + return SignalSenderKeyStores(attachedDatabase, alias); + } +} + +class SignalSenderKeyStoresData extends DataClass + implements Insertable<SignalSenderKeyStoresData> { + final String senderKeyName; + final i2.Uint8List senderKey; + const SignalSenderKeyStoresData( + {required this.senderKeyName, required this.senderKey}); + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + map['sender_key_name'] = Variable<String>(senderKeyName); + map['sender_key'] = Variable<i2.Uint8List>(senderKey); + return map; + } + + SignalSenderKeyStoresCompanion toCompanion(bool nullToAbsent) { + return SignalSenderKeyStoresCompanion( + senderKeyName: Value(senderKeyName), + senderKey: Value(senderKey), + ); + } + + factory SignalSenderKeyStoresData.fromJson(Map<String, dynamic> json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalSenderKeyStoresData( + senderKeyName: serializer.fromJson<String>(json['senderKeyName']), + senderKey: serializer.fromJson<i2.Uint8List>(json['senderKey']), + ); + } + @override + Map<String, dynamic> toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return <String, dynamic>{ + 'senderKeyName': serializer.toJson<String>(senderKeyName), + 'senderKey': serializer.toJson<i2.Uint8List>(senderKey), + }; + } + + SignalSenderKeyStoresData copyWith( + {String? senderKeyName, i2.Uint8List? senderKey}) => + SignalSenderKeyStoresData( + senderKeyName: senderKeyName ?? this.senderKeyName, + senderKey: senderKey ?? this.senderKey, + ); + SignalSenderKeyStoresData copyWithCompanion( + SignalSenderKeyStoresCompanion data) { + return SignalSenderKeyStoresData( + senderKeyName: data.senderKeyName.present + ? data.senderKeyName.value + : this.senderKeyName, + senderKey: data.senderKey.present ? data.senderKey.value : this.senderKey, + ); + } + + @override + String toString() { + return (StringBuffer('SignalSenderKeyStoresData(') + ..write('senderKeyName: $senderKeyName, ') + ..write('senderKey: $senderKey') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(senderKeyName, $driftBlobEquality.hash(senderKey)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalSenderKeyStoresData && + other.senderKeyName == this.senderKeyName && + $driftBlobEquality.equals(other.senderKey, this.senderKey)); +} + +class SignalSenderKeyStoresCompanion + extends UpdateCompanion<SignalSenderKeyStoresData> { + final Value<String> senderKeyName; + final Value<i2.Uint8List> senderKey; + final Value<int> rowid; + const SignalSenderKeyStoresCompanion({ + this.senderKeyName = const Value.absent(), + this.senderKey = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalSenderKeyStoresCompanion.insert({ + required String senderKeyName, + required i2.Uint8List senderKey, + this.rowid = const Value.absent(), + }) : senderKeyName = Value(senderKeyName), + senderKey = Value(senderKey); + static Insertable<SignalSenderKeyStoresData> custom({ + Expression<String>? senderKeyName, + Expression<i2.Uint8List>? senderKey, + Expression<int>? rowid, + }) { + return RawValuesInsertable({ + if (senderKeyName != null) 'sender_key_name': senderKeyName, + if (senderKey != null) 'sender_key': senderKey, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalSenderKeyStoresCompanion copyWith( + {Value<String>? senderKeyName, + Value<i2.Uint8List>? senderKey, + Value<int>? rowid}) { + return SignalSenderKeyStoresCompanion( + senderKeyName: senderKeyName ?? this.senderKeyName, + senderKey: senderKey ?? this.senderKey, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + if (senderKeyName.present) { + map['sender_key_name'] = Variable<String>(senderKeyName.value); + } + if (senderKey.present) { + map['sender_key'] = Variable<i2.Uint8List>(senderKey.value); + } + if (rowid.present) { + map['rowid'] = Variable<int>(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalSenderKeyStoresCompanion(') + ..write('senderKeyName: $senderKeyName, ') + ..write('senderKey: $senderKey, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class SignalSessionStores extends Table + with TableInfo<SignalSessionStores, SignalSessionStoresData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SignalSessionStores(this.attachedDatabase, [this._alias]); + late final GeneratedColumn<int> deviceId = GeneratedColumn<int>( + 'device_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn<String> name = GeneratedColumn<String>( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn<i2.Uint8List> sessionRecord = + GeneratedColumn<i2.Uint8List>('session_record', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); + @override + List<GeneratedColumn> get $columns => + [deviceId, name, sessionRecord, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_session_stores'; + @override + Set<GeneratedColumn> get $primaryKey => {deviceId, name}; + @override + SignalSessionStoresData map(Map<String, dynamic> data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalSessionStoresData( + deviceId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}device_id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + sessionRecord: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}session_record'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + SignalSessionStores createAlias(String alias) { + return SignalSessionStores(attachedDatabase, alias); + } +} + +class SignalSessionStoresData extends DataClass + implements Insertable<SignalSessionStoresData> { + final int deviceId; + final String name; + final i2.Uint8List sessionRecord; + final DateTime createdAt; + const SignalSessionStoresData( + {required this.deviceId, + required this.name, + required this.sessionRecord, + required this.createdAt}); + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + map['device_id'] = Variable<int>(deviceId); + map['name'] = Variable<String>(name); + map['session_record'] = Variable<i2.Uint8List>(sessionRecord); + map['created_at'] = Variable<DateTime>(createdAt); + return map; + } + + SignalSessionStoresCompanion toCompanion(bool nullToAbsent) { + return SignalSessionStoresCompanion( + deviceId: Value(deviceId), + name: Value(name), + sessionRecord: Value(sessionRecord), + createdAt: Value(createdAt), + ); + } + + factory SignalSessionStoresData.fromJson(Map<String, dynamic> json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalSessionStoresData( + deviceId: serializer.fromJson<int>(json['deviceId']), + name: serializer.fromJson<String>(json['name']), + sessionRecord: serializer.fromJson<i2.Uint8List>(json['sessionRecord']), + createdAt: serializer.fromJson<DateTime>(json['createdAt']), + ); + } + @override + Map<String, dynamic> toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return <String, dynamic>{ + 'deviceId': serializer.toJson<int>(deviceId), + 'name': serializer.toJson<String>(name), + 'sessionRecord': serializer.toJson<i2.Uint8List>(sessionRecord), + 'createdAt': serializer.toJson<DateTime>(createdAt), + }; + } + + SignalSessionStoresData copyWith( + {int? deviceId, + String? name, + i2.Uint8List? sessionRecord, + DateTime? createdAt}) => + SignalSessionStoresData( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + sessionRecord: sessionRecord ?? this.sessionRecord, + createdAt: createdAt ?? this.createdAt, + ); + SignalSessionStoresData copyWithCompanion(SignalSessionStoresCompanion data) { + return SignalSessionStoresData( + deviceId: data.deviceId.present ? data.deviceId.value : this.deviceId, + name: data.name.present ? data.name.value : this.name, + sessionRecord: data.sessionRecord.present + ? data.sessionRecord.value + : this.sessionRecord, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalSessionStoresData(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('sessionRecord: $sessionRecord, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + deviceId, name, $driftBlobEquality.hash(sessionRecord), createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalSessionStoresData && + other.deviceId == this.deviceId && + other.name == this.name && + $driftBlobEquality.equals(other.sessionRecord, this.sessionRecord) && + other.createdAt == this.createdAt); +} + +class SignalSessionStoresCompanion + extends UpdateCompanion<SignalSessionStoresData> { + final Value<int> deviceId; + final Value<String> name; + final Value<i2.Uint8List> sessionRecord; + final Value<DateTime> createdAt; + final Value<int> rowid; + const SignalSessionStoresCompanion({ + this.deviceId = const Value.absent(), + this.name = const Value.absent(), + this.sessionRecord = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalSessionStoresCompanion.insert({ + required int deviceId, + required String name, + required i2.Uint8List sessionRecord, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : deviceId = Value(deviceId), + name = Value(name), + sessionRecord = Value(sessionRecord); + static Insertable<SignalSessionStoresData> custom({ + Expression<int>? deviceId, + Expression<String>? name, + Expression<i2.Uint8List>? sessionRecord, + Expression<DateTime>? createdAt, + Expression<int>? rowid, + }) { + return RawValuesInsertable({ + if (deviceId != null) 'device_id': deviceId, + if (name != null) 'name': name, + if (sessionRecord != null) 'session_record': sessionRecord, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalSessionStoresCompanion copyWith( + {Value<int>? deviceId, + Value<String>? name, + Value<i2.Uint8List>? sessionRecord, + Value<DateTime>? createdAt, + Value<int>? rowid}) { + return SignalSessionStoresCompanion( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + sessionRecord: sessionRecord ?? this.sessionRecord, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + if (deviceId.present) { + map['device_id'] = Variable<int>(deviceId.value); + } + if (name.present) { + map['name'] = Variable<String>(name.value); + } + if (sessionRecord.present) { + map['session_record'] = Variable<i2.Uint8List>(sessionRecord.value); + } + if (createdAt.present) { + map['created_at'] = Variable<DateTime>(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable<int>(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalSessionStoresCompanion(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('sessionRecord: $sessionRecord, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class SignalContactPreKeys extends Table + with TableInfo<SignalContactPreKeys, SignalContactPreKeysData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SignalContactPreKeys(this.attachedDatabase, [this._alias]); + late final GeneratedColumn<int> contactId = GeneratedColumn<int>( + 'contact_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES contacts (user_id) ON DELETE CASCADE')); + late final GeneratedColumn<int> preKeyId = GeneratedColumn<int>( + 'pre_key_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn<i2.Uint8List> preKey = + GeneratedColumn<i2.Uint8List>('pre_key', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); + @override + List<GeneratedColumn> get $columns => + [contactId, preKeyId, preKey, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_contact_pre_keys'; + @override + Set<GeneratedColumn> get $primaryKey => {contactId, preKeyId}; + @override + SignalContactPreKeysData map(Map<String, dynamic> data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalContactPreKeysData( + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, + preKeyId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}pre_key_id'])!, + preKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}pre_key'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + SignalContactPreKeys createAlias(String alias) { + return SignalContactPreKeys(attachedDatabase, alias); + } +} + +class SignalContactPreKeysData extends DataClass + implements Insertable<SignalContactPreKeysData> { + final int contactId; + final int preKeyId; + final i2.Uint8List preKey; + final DateTime createdAt; + const SignalContactPreKeysData( + {required this.contactId, + required this.preKeyId, + required this.preKey, + required this.createdAt}); + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + map['contact_id'] = Variable<int>(contactId); + map['pre_key_id'] = Variable<int>(preKeyId); + map['pre_key'] = Variable<i2.Uint8List>(preKey); + map['created_at'] = Variable<DateTime>(createdAt); + return map; + } + + SignalContactPreKeysCompanion toCompanion(bool nullToAbsent) { + return SignalContactPreKeysCompanion( + contactId: Value(contactId), + preKeyId: Value(preKeyId), + preKey: Value(preKey), + createdAt: Value(createdAt), + ); + } + + factory SignalContactPreKeysData.fromJson(Map<String, dynamic> json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalContactPreKeysData( + contactId: serializer.fromJson<int>(json['contactId']), + preKeyId: serializer.fromJson<int>(json['preKeyId']), + preKey: serializer.fromJson<i2.Uint8List>(json['preKey']), + createdAt: serializer.fromJson<DateTime>(json['createdAt']), + ); + } + @override + Map<String, dynamic> toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return <String, dynamic>{ + 'contactId': serializer.toJson<int>(contactId), + 'preKeyId': serializer.toJson<int>(preKeyId), + 'preKey': serializer.toJson<i2.Uint8List>(preKey), + 'createdAt': serializer.toJson<DateTime>(createdAt), + }; + } + + SignalContactPreKeysData copyWith( + {int? contactId, + int? preKeyId, + i2.Uint8List? preKey, + DateTime? createdAt}) => + SignalContactPreKeysData( + contactId: contactId ?? this.contactId, + preKeyId: preKeyId ?? this.preKeyId, + preKey: preKey ?? this.preKey, + createdAt: createdAt ?? this.createdAt, + ); + SignalContactPreKeysData copyWithCompanion( + SignalContactPreKeysCompanion data) { + return SignalContactPreKeysData( + contactId: data.contactId.present ? data.contactId.value : this.contactId, + preKeyId: data.preKeyId.present ? data.preKeyId.value : this.preKeyId, + preKey: data.preKey.present ? data.preKey.value : this.preKey, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalContactPreKeysData(') + ..write('contactId: $contactId, ') + ..write('preKeyId: $preKeyId, ') + ..write('preKey: $preKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + contactId, preKeyId, $driftBlobEquality.hash(preKey), createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalContactPreKeysData && + other.contactId == this.contactId && + other.preKeyId == this.preKeyId && + $driftBlobEquality.equals(other.preKey, this.preKey) && + other.createdAt == this.createdAt); +} + +class SignalContactPreKeysCompanion + extends UpdateCompanion<SignalContactPreKeysData> { + final Value<int> contactId; + final Value<int> preKeyId; + final Value<i2.Uint8List> preKey; + final Value<DateTime> createdAt; + final Value<int> rowid; + const SignalContactPreKeysCompanion({ + this.contactId = const Value.absent(), + this.preKeyId = const Value.absent(), + this.preKey = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalContactPreKeysCompanion.insert({ + required int contactId, + required int preKeyId, + required i2.Uint8List preKey, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : contactId = Value(contactId), + preKeyId = Value(preKeyId), + preKey = Value(preKey); + static Insertable<SignalContactPreKeysData> custom({ + Expression<int>? contactId, + Expression<int>? preKeyId, + Expression<i2.Uint8List>? preKey, + Expression<DateTime>? createdAt, + Expression<int>? rowid, + }) { + return RawValuesInsertable({ + if (contactId != null) 'contact_id': contactId, + if (preKeyId != null) 'pre_key_id': preKeyId, + if (preKey != null) 'pre_key': preKey, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalContactPreKeysCompanion copyWith( + {Value<int>? contactId, + Value<int>? preKeyId, + Value<i2.Uint8List>? preKey, + Value<DateTime>? createdAt, + Value<int>? rowid}) { + return SignalContactPreKeysCompanion( + contactId: contactId ?? this.contactId, + preKeyId: preKeyId ?? this.preKeyId, + preKey: preKey ?? this.preKey, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + if (contactId.present) { + map['contact_id'] = Variable<int>(contactId.value); + } + if (preKeyId.present) { + map['pre_key_id'] = Variable<int>(preKeyId.value); + } + if (preKey.present) { + map['pre_key'] = Variable<i2.Uint8List>(preKey.value); + } + if (createdAt.present) { + map['created_at'] = Variable<DateTime>(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable<int>(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalContactPreKeysCompanion(') + ..write('contactId: $contactId, ') + ..write('preKeyId: $preKeyId, ') + ..write('preKey: $preKey, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class SignalContactSignedPreKeys extends Table + with TableInfo<SignalContactSignedPreKeys, SignalContactSignedPreKeysData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SignalContactSignedPreKeys(this.attachedDatabase, [this._alias]); + late final GeneratedColumn<int> contactId = GeneratedColumn<int>( + 'contact_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES contacts (user_id) ON DELETE CASCADE')); + late final GeneratedColumn<int> signedPreKeyId = GeneratedColumn<int>( + 'signed_pre_key_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn<i2.Uint8List> signedPreKey = + GeneratedColumn<i2.Uint8List>('signed_pre_key', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + late final GeneratedColumn<i2.Uint8List> signedPreKeySignature = + GeneratedColumn<i2.Uint8List>( + 'signed_pre_key_signature', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); + @override + List<GeneratedColumn> get $columns => [ + contactId, + signedPreKeyId, + signedPreKey, + signedPreKeySignature, + createdAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_contact_signed_pre_keys'; + @override + Set<GeneratedColumn> get $primaryKey => {contactId}; + @override + SignalContactSignedPreKeysData map(Map<String, dynamic> data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalContactSignedPreKeysData( + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, + signedPreKeyId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}signed_pre_key_id'])!, + signedPreKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}signed_pre_key'])!, + signedPreKeySignature: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}signed_pre_key_signature'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + SignalContactSignedPreKeys createAlias(String alias) { + return SignalContactSignedPreKeys(attachedDatabase, alias); + } +} + +class SignalContactSignedPreKeysData extends DataClass + implements Insertable<SignalContactSignedPreKeysData> { + final int contactId; + final int signedPreKeyId; + final i2.Uint8List signedPreKey; + final i2.Uint8List signedPreKeySignature; + final DateTime createdAt; + const SignalContactSignedPreKeysData( + {required this.contactId, + required this.signedPreKeyId, + required this.signedPreKey, + required this.signedPreKeySignature, + required this.createdAt}); + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + map['contact_id'] = Variable<int>(contactId); + map['signed_pre_key_id'] = Variable<int>(signedPreKeyId); + map['signed_pre_key'] = Variable<i2.Uint8List>(signedPreKey); + map['signed_pre_key_signature'] = + Variable<i2.Uint8List>(signedPreKeySignature); + map['created_at'] = Variable<DateTime>(createdAt); + return map; + } + + SignalContactSignedPreKeysCompanion toCompanion(bool nullToAbsent) { + return SignalContactSignedPreKeysCompanion( + contactId: Value(contactId), + signedPreKeyId: Value(signedPreKeyId), + signedPreKey: Value(signedPreKey), + signedPreKeySignature: Value(signedPreKeySignature), + createdAt: Value(createdAt), + ); + } + + factory SignalContactSignedPreKeysData.fromJson(Map<String, dynamic> json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalContactSignedPreKeysData( + contactId: serializer.fromJson<int>(json['contactId']), + signedPreKeyId: serializer.fromJson<int>(json['signedPreKeyId']), + signedPreKey: serializer.fromJson<i2.Uint8List>(json['signedPreKey']), + signedPreKeySignature: + serializer.fromJson<i2.Uint8List>(json['signedPreKeySignature']), + createdAt: serializer.fromJson<DateTime>(json['createdAt']), + ); + } + @override + Map<String, dynamic> toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return <String, dynamic>{ + 'contactId': serializer.toJson<int>(contactId), + 'signedPreKeyId': serializer.toJson<int>(signedPreKeyId), + 'signedPreKey': serializer.toJson<i2.Uint8List>(signedPreKey), + 'signedPreKeySignature': + serializer.toJson<i2.Uint8List>(signedPreKeySignature), + 'createdAt': serializer.toJson<DateTime>(createdAt), + }; + } + + SignalContactSignedPreKeysData copyWith( + {int? contactId, + int? signedPreKeyId, + i2.Uint8List? signedPreKey, + i2.Uint8List? signedPreKeySignature, + DateTime? createdAt}) => + SignalContactSignedPreKeysData( + contactId: contactId ?? this.contactId, + signedPreKeyId: signedPreKeyId ?? this.signedPreKeyId, + signedPreKey: signedPreKey ?? this.signedPreKey, + signedPreKeySignature: + signedPreKeySignature ?? this.signedPreKeySignature, + createdAt: createdAt ?? this.createdAt, + ); + SignalContactSignedPreKeysData copyWithCompanion( + SignalContactSignedPreKeysCompanion data) { + return SignalContactSignedPreKeysData( + contactId: data.contactId.present ? data.contactId.value : this.contactId, + signedPreKeyId: data.signedPreKeyId.present + ? data.signedPreKeyId.value + : this.signedPreKeyId, + signedPreKey: data.signedPreKey.present + ? data.signedPreKey.value + : this.signedPreKey, + signedPreKeySignature: data.signedPreKeySignature.present + ? data.signedPreKeySignature.value + : this.signedPreKeySignature, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalContactSignedPreKeysData(') + ..write('contactId: $contactId, ') + ..write('signedPreKeyId: $signedPreKeyId, ') + ..write('signedPreKey: $signedPreKey, ') + ..write('signedPreKeySignature: $signedPreKeySignature, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + contactId, + signedPreKeyId, + $driftBlobEquality.hash(signedPreKey), + $driftBlobEquality.hash(signedPreKeySignature), + createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalContactSignedPreKeysData && + other.contactId == this.contactId && + other.signedPreKeyId == this.signedPreKeyId && + $driftBlobEquality.equals(other.signedPreKey, this.signedPreKey) && + $driftBlobEquality.equals( + other.signedPreKeySignature, this.signedPreKeySignature) && + other.createdAt == this.createdAt); +} + +class SignalContactSignedPreKeysCompanion + extends UpdateCompanion<SignalContactSignedPreKeysData> { + final Value<int> contactId; + final Value<int> signedPreKeyId; + final Value<i2.Uint8List> signedPreKey; + final Value<i2.Uint8List> signedPreKeySignature; + final Value<DateTime> createdAt; + const SignalContactSignedPreKeysCompanion({ + this.contactId = const Value.absent(), + this.signedPreKeyId = const Value.absent(), + this.signedPreKey = const Value.absent(), + this.signedPreKeySignature = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SignalContactSignedPreKeysCompanion.insert({ + this.contactId = const Value.absent(), + required int signedPreKeyId, + required i2.Uint8List signedPreKey, + required i2.Uint8List signedPreKeySignature, + this.createdAt = const Value.absent(), + }) : signedPreKeyId = Value(signedPreKeyId), + signedPreKey = Value(signedPreKey), + signedPreKeySignature = Value(signedPreKeySignature); + static Insertable<SignalContactSignedPreKeysData> custom({ + Expression<int>? contactId, + Expression<int>? signedPreKeyId, + Expression<i2.Uint8List>? signedPreKey, + Expression<i2.Uint8List>? signedPreKeySignature, + Expression<DateTime>? createdAt, + }) { + return RawValuesInsertable({ + if (contactId != null) 'contact_id': contactId, + if (signedPreKeyId != null) 'signed_pre_key_id': signedPreKeyId, + if (signedPreKey != null) 'signed_pre_key': signedPreKey, + if (signedPreKeySignature != null) + 'signed_pre_key_signature': signedPreKeySignature, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SignalContactSignedPreKeysCompanion copyWith( + {Value<int>? contactId, + Value<int>? signedPreKeyId, + Value<i2.Uint8List>? signedPreKey, + Value<i2.Uint8List>? signedPreKeySignature, + Value<DateTime>? createdAt}) { + return SignalContactSignedPreKeysCompanion( + contactId: contactId ?? this.contactId, + signedPreKeyId: signedPreKeyId ?? this.signedPreKeyId, + signedPreKey: signedPreKey ?? this.signedPreKey, + signedPreKeySignature: + signedPreKeySignature ?? this.signedPreKeySignature, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + if (contactId.present) { + map['contact_id'] = Variable<int>(contactId.value); + } + if (signedPreKeyId.present) { + map['signed_pre_key_id'] = Variable<int>(signedPreKeyId.value); + } + if (signedPreKey.present) { + map['signed_pre_key'] = Variable<i2.Uint8List>(signedPreKey.value); + } + if (signedPreKeySignature.present) { + map['signed_pre_key_signature'] = + Variable<i2.Uint8List>(signedPreKeySignature.value); + } + if (createdAt.present) { + map['created_at'] = Variable<DateTime>(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalContactSignedPreKeysCompanion(') + ..write('contactId: $contactId, ') + ..write('signedPreKeyId: $signedPreKeyId, ') + ..write('signedPreKey: $signedPreKey, ') + ..write('signedPreKeySignature: $signedPreKeySignature, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class MessageActions extends Table + with TableInfo<MessageActions, MessageActionsData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MessageActions(this.attachedDatabase, [this._alias]); + late final GeneratedColumn<String> messageId = GeneratedColumn<String>( + 'message_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES messages (message_id) ON DELETE CASCADE')); + late final GeneratedColumn<int> contactId = GeneratedColumn<int>( + 'contact_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn<String> type = GeneratedColumn<String>( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn<DateTime> actionAt = GeneratedColumn<DateTime>( + 'action_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); + @override + List<GeneratedColumn> get $columns => [messageId, contactId, type, actionAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'message_actions'; + @override + Set<GeneratedColumn> get $primaryKey => {messageId, contactId, type}; + @override + MessageActionsData map(Map<String, dynamic> data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MessageActionsData( + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + actionAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}action_at'])!, + ); + } + + @override + MessageActions createAlias(String alias) { + return MessageActions(attachedDatabase, alias); + } +} + +class MessageActionsData extends DataClass + implements Insertable<MessageActionsData> { + final String messageId; + final int contactId; + final String type; + final DateTime actionAt; + const MessageActionsData( + {required this.messageId, + required this.contactId, + required this.type, + required this.actionAt}); + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + map['message_id'] = Variable<String>(messageId); + map['contact_id'] = Variable<int>(contactId); + map['type'] = Variable<String>(type); + map['action_at'] = Variable<DateTime>(actionAt); + return map; + } + + MessageActionsCompanion toCompanion(bool nullToAbsent) { + return MessageActionsCompanion( + messageId: Value(messageId), + contactId: Value(contactId), + type: Value(type), + actionAt: Value(actionAt), + ); + } + + factory MessageActionsData.fromJson(Map<String, dynamic> json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MessageActionsData( + messageId: serializer.fromJson<String>(json['messageId']), + contactId: serializer.fromJson<int>(json['contactId']), + type: serializer.fromJson<String>(json['type']), + actionAt: serializer.fromJson<DateTime>(json['actionAt']), + ); + } + @override + Map<String, dynamic> toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return <String, dynamic>{ + 'messageId': serializer.toJson<String>(messageId), + 'contactId': serializer.toJson<int>(contactId), + 'type': serializer.toJson<String>(type), + 'actionAt': serializer.toJson<DateTime>(actionAt), + }; + } + + MessageActionsData copyWith( + {String? messageId, + int? contactId, + String? type, + DateTime? actionAt}) => + MessageActionsData( + messageId: messageId ?? this.messageId, + contactId: contactId ?? this.contactId, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + ); + MessageActionsData copyWithCompanion(MessageActionsCompanion data) { + return MessageActionsData( + messageId: data.messageId.present ? data.messageId.value : this.messageId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + type: data.type.present ? data.type.value : this.type, + actionAt: data.actionAt.present ? data.actionAt.value : this.actionAt, + ); + } + + @override + String toString() { + return (StringBuffer('MessageActionsData(') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('type: $type, ') + ..write('actionAt: $actionAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(messageId, contactId, type, actionAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MessageActionsData && + other.messageId == this.messageId && + other.contactId == this.contactId && + other.type == this.type && + other.actionAt == this.actionAt); +} + +class MessageActionsCompanion extends UpdateCompanion<MessageActionsData> { + final Value<String> messageId; + final Value<int> contactId; + final Value<String> type; + final Value<DateTime> actionAt; + final Value<int> rowid; + const MessageActionsCompanion({ + this.messageId = const Value.absent(), + this.contactId = const Value.absent(), + this.type = const Value.absent(), + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + MessageActionsCompanion.insert({ + required String messageId, + required int contactId, + required String type, + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : messageId = Value(messageId), + contactId = Value(contactId), + type = Value(type); + static Insertable<MessageActionsData> custom({ + Expression<String>? messageId, + Expression<int>? contactId, + Expression<String>? type, + Expression<DateTime>? actionAt, + Expression<int>? rowid, + }) { + return RawValuesInsertable({ + if (messageId != null) 'message_id': messageId, + if (contactId != null) 'contact_id': contactId, + if (type != null) 'type': type, + if (actionAt != null) 'action_at': actionAt, + if (rowid != null) 'rowid': rowid, + }); + } + + MessageActionsCompanion copyWith( + {Value<String>? messageId, + Value<int>? contactId, + Value<String>? type, + Value<DateTime>? actionAt, + Value<int>? rowid}) { + return MessageActionsCompanion( + messageId: messageId ?? this.messageId, + contactId: contactId ?? this.contactId, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + if (messageId.present) { + map['message_id'] = Variable<String>(messageId.value); + } + if (contactId.present) { + map['contact_id'] = Variable<int>(contactId.value); + } + if (type.present) { + map['type'] = Variable<String>(type.value); + } + if (actionAt.present) { + map['action_at'] = Variable<DateTime>(actionAt.value); + } + if (rowid.present) { + map['rowid'] = Variable<int>(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessageActionsCompanion(') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('type: $type, ') + ..write('actionAt: $actionAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class GroupHistories extends Table + with TableInfo<GroupHistories, GroupHistoriesData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + GroupHistories(this.attachedDatabase, [this._alias]); + late final GeneratedColumn<String> groupHistoryId = GeneratedColumn<String>( + 'group_history_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn<String> groupId = GeneratedColumn<String>( + 'group_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES "groups" (group_id) ON DELETE CASCADE')); + late final GeneratedColumn<int> contactId = GeneratedColumn<int>( + 'contact_id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)')); + late final GeneratedColumn<int> affectedContactId = GeneratedColumn<int>( + 'affected_contact_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn<String> oldGroupName = GeneratedColumn<String>( + 'old_group_name', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn<String> newGroupName = GeneratedColumn<String>( + 'new_group_name', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn<int> newDeleteMessagesAfterMilliseconds = + GeneratedColumn<int>( + 'new_delete_messages_after_milliseconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn<String> type = GeneratedColumn<String>( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn<DateTime> actionAt = GeneratedColumn<DateTime>( + 'action_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)')); + @override + List<GeneratedColumn> get $columns => [ + groupHistoryId, + groupId, + contactId, + affectedContactId, + oldGroupName, + newGroupName, + newDeleteMessagesAfterMilliseconds, + type, + actionAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'group_histories'; + @override + Set<GeneratedColumn> get $primaryKey => {groupHistoryId}; + @override + GroupHistoriesData map(Map<String, dynamic> data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return GroupHistoriesData( + groupHistoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}group_history_id'])!, + groupId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_id'])!, + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id']), + affectedContactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}affected_contact_id']), + oldGroupName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}old_group_name']), + newGroupName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}new_group_name']), + newDeleteMessagesAfterMilliseconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}new_delete_messages_after_milliseconds']), + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + actionAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}action_at'])!, + ); + } + + @override + GroupHistories createAlias(String alias) { + return GroupHistories(attachedDatabase, alias); + } +} + +class GroupHistoriesData extends DataClass + implements Insertable<GroupHistoriesData> { + final String groupHistoryId; + final String groupId; + final int? contactId; + final int? affectedContactId; + final String? oldGroupName; + final String? newGroupName; + final int? newDeleteMessagesAfterMilliseconds; + final String type; + final DateTime actionAt; + const GroupHistoriesData( + {required this.groupHistoryId, + required this.groupId, + this.contactId, + this.affectedContactId, + this.oldGroupName, + this.newGroupName, + this.newDeleteMessagesAfterMilliseconds, + required this.type, + required this.actionAt}); + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + map['group_history_id'] = Variable<String>(groupHistoryId); + map['group_id'] = Variable<String>(groupId); + if (!nullToAbsent || contactId != null) { + map['contact_id'] = Variable<int>(contactId); + } + if (!nullToAbsent || affectedContactId != null) { + map['affected_contact_id'] = Variable<int>(affectedContactId); + } + if (!nullToAbsent || oldGroupName != null) { + map['old_group_name'] = Variable<String>(oldGroupName); + } + if (!nullToAbsent || newGroupName != null) { + map['new_group_name'] = Variable<String>(newGroupName); + } + if (!nullToAbsent || newDeleteMessagesAfterMilliseconds != null) { + map['new_delete_messages_after_milliseconds'] = + Variable<int>(newDeleteMessagesAfterMilliseconds); + } + map['type'] = Variable<String>(type); + map['action_at'] = Variable<DateTime>(actionAt); + return map; + } + + GroupHistoriesCompanion toCompanion(bool nullToAbsent) { + return GroupHistoriesCompanion( + groupHistoryId: Value(groupHistoryId), + groupId: Value(groupId), + contactId: contactId == null && nullToAbsent + ? const Value.absent() + : Value(contactId), + affectedContactId: affectedContactId == null && nullToAbsent + ? const Value.absent() + : Value(affectedContactId), + oldGroupName: oldGroupName == null && nullToAbsent + ? const Value.absent() + : Value(oldGroupName), + newGroupName: newGroupName == null && nullToAbsent + ? const Value.absent() + : Value(newGroupName), + newDeleteMessagesAfterMilliseconds: + newDeleteMessagesAfterMilliseconds == null && nullToAbsent + ? const Value.absent() + : Value(newDeleteMessagesAfterMilliseconds), + type: Value(type), + actionAt: Value(actionAt), + ); + } + + factory GroupHistoriesData.fromJson(Map<String, dynamic> json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GroupHistoriesData( + groupHistoryId: serializer.fromJson<String>(json['groupHistoryId']), + groupId: serializer.fromJson<String>(json['groupId']), + contactId: serializer.fromJson<int?>(json['contactId']), + affectedContactId: serializer.fromJson<int?>(json['affectedContactId']), + oldGroupName: serializer.fromJson<String?>(json['oldGroupName']), + newGroupName: serializer.fromJson<String?>(json['newGroupName']), + newDeleteMessagesAfterMilliseconds: + serializer.fromJson<int?>(json['newDeleteMessagesAfterMilliseconds']), + type: serializer.fromJson<String>(json['type']), + actionAt: serializer.fromJson<DateTime>(json['actionAt']), + ); + } + @override + Map<String, dynamic> toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return <String, dynamic>{ + 'groupHistoryId': serializer.toJson<String>(groupHistoryId), + 'groupId': serializer.toJson<String>(groupId), + 'contactId': serializer.toJson<int?>(contactId), + 'affectedContactId': serializer.toJson<int?>(affectedContactId), + 'oldGroupName': serializer.toJson<String?>(oldGroupName), + 'newGroupName': serializer.toJson<String?>(newGroupName), + 'newDeleteMessagesAfterMilliseconds': + serializer.toJson<int?>(newDeleteMessagesAfterMilliseconds), + 'type': serializer.toJson<String>(type), + 'actionAt': serializer.toJson<DateTime>(actionAt), + }; + } + + GroupHistoriesData copyWith( + {String? groupHistoryId, + String? groupId, + Value<int?> contactId = const Value.absent(), + Value<int?> affectedContactId = const Value.absent(), + Value<String?> oldGroupName = const Value.absent(), + Value<String?> newGroupName = const Value.absent(), + Value<int?> newDeleteMessagesAfterMilliseconds = const Value.absent(), + String? type, + DateTime? actionAt}) => + GroupHistoriesData( + groupHistoryId: groupHistoryId ?? this.groupHistoryId, + groupId: groupId ?? this.groupId, + contactId: contactId.present ? contactId.value : this.contactId, + affectedContactId: affectedContactId.present + ? affectedContactId.value + : this.affectedContactId, + oldGroupName: + oldGroupName.present ? oldGroupName.value : this.oldGroupName, + newGroupName: + newGroupName.present ? newGroupName.value : this.newGroupName, + newDeleteMessagesAfterMilliseconds: + newDeleteMessagesAfterMilliseconds.present + ? newDeleteMessagesAfterMilliseconds.value + : this.newDeleteMessagesAfterMilliseconds, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + ); + GroupHistoriesData copyWithCompanion(GroupHistoriesCompanion data) { + return GroupHistoriesData( + groupHistoryId: data.groupHistoryId.present + ? data.groupHistoryId.value + : this.groupHistoryId, + groupId: data.groupId.present ? data.groupId.value : this.groupId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + affectedContactId: data.affectedContactId.present + ? data.affectedContactId.value + : this.affectedContactId, + oldGroupName: data.oldGroupName.present + ? data.oldGroupName.value + : this.oldGroupName, + newGroupName: data.newGroupName.present + ? data.newGroupName.value + : this.newGroupName, + newDeleteMessagesAfterMilliseconds: + data.newDeleteMessagesAfterMilliseconds.present + ? data.newDeleteMessagesAfterMilliseconds.value + : this.newDeleteMessagesAfterMilliseconds, + type: data.type.present ? data.type.value : this.type, + actionAt: data.actionAt.present ? data.actionAt.value : this.actionAt, + ); + } + + @override + String toString() { + return (StringBuffer('GroupHistoriesData(') + ..write('groupHistoryId: $groupHistoryId, ') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('affectedContactId: $affectedContactId, ') + ..write('oldGroupName: $oldGroupName, ') + ..write('newGroupName: $newGroupName, ') + ..write( + 'newDeleteMessagesAfterMilliseconds: $newDeleteMessagesAfterMilliseconds, ') + ..write('type: $type, ') + ..write('actionAt: $actionAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + groupHistoryId, + groupId, + contactId, + affectedContactId, + oldGroupName, + newGroupName, + newDeleteMessagesAfterMilliseconds, + type, + actionAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GroupHistoriesData && + other.groupHistoryId == this.groupHistoryId && + other.groupId == this.groupId && + other.contactId == this.contactId && + other.affectedContactId == this.affectedContactId && + other.oldGroupName == this.oldGroupName && + other.newGroupName == this.newGroupName && + other.newDeleteMessagesAfterMilliseconds == + this.newDeleteMessagesAfterMilliseconds && + other.type == this.type && + other.actionAt == this.actionAt); +} + +class GroupHistoriesCompanion extends UpdateCompanion<GroupHistoriesData> { + final Value<String> groupHistoryId; + final Value<String> groupId; + final Value<int?> contactId; + final Value<int?> affectedContactId; + final Value<String?> oldGroupName; + final Value<String?> newGroupName; + final Value<int?> newDeleteMessagesAfterMilliseconds; + final Value<String> type; + final Value<DateTime> actionAt; + final Value<int> rowid; + const GroupHistoriesCompanion({ + this.groupHistoryId = const Value.absent(), + this.groupId = const Value.absent(), + this.contactId = const Value.absent(), + this.affectedContactId = const Value.absent(), + this.oldGroupName = const Value.absent(), + this.newGroupName = const Value.absent(), + this.newDeleteMessagesAfterMilliseconds = const Value.absent(), + this.type = const Value.absent(), + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupHistoriesCompanion.insert({ + required String groupHistoryId, + required String groupId, + this.contactId = const Value.absent(), + this.affectedContactId = const Value.absent(), + this.oldGroupName = const Value.absent(), + this.newGroupName = const Value.absent(), + this.newDeleteMessagesAfterMilliseconds = const Value.absent(), + required String type, + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupHistoryId = Value(groupHistoryId), + groupId = Value(groupId), + type = Value(type); + static Insertable<GroupHistoriesData> custom({ + Expression<String>? groupHistoryId, + Expression<String>? groupId, + Expression<int>? contactId, + Expression<int>? affectedContactId, + Expression<String>? oldGroupName, + Expression<String>? newGroupName, + Expression<int>? newDeleteMessagesAfterMilliseconds, + Expression<String>? type, + Expression<DateTime>? actionAt, + Expression<int>? rowid, + }) { + return RawValuesInsertable({ + if (groupHistoryId != null) 'group_history_id': groupHistoryId, + if (groupId != null) 'group_id': groupId, + if (contactId != null) 'contact_id': contactId, + if (affectedContactId != null) 'affected_contact_id': affectedContactId, + if (oldGroupName != null) 'old_group_name': oldGroupName, + if (newGroupName != null) 'new_group_name': newGroupName, + if (newDeleteMessagesAfterMilliseconds != null) + 'new_delete_messages_after_milliseconds': + newDeleteMessagesAfterMilliseconds, + if (type != null) 'type': type, + if (actionAt != null) 'action_at': actionAt, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupHistoriesCompanion copyWith( + {Value<String>? groupHistoryId, + Value<String>? groupId, + Value<int?>? contactId, + Value<int?>? affectedContactId, + Value<String?>? oldGroupName, + Value<String?>? newGroupName, + Value<int?>? newDeleteMessagesAfterMilliseconds, + Value<String>? type, + Value<DateTime>? actionAt, + Value<int>? rowid}) { + return GroupHistoriesCompanion( + groupHistoryId: groupHistoryId ?? this.groupHistoryId, + groupId: groupId ?? this.groupId, + contactId: contactId ?? this.contactId, + affectedContactId: affectedContactId ?? this.affectedContactId, + oldGroupName: oldGroupName ?? this.oldGroupName, + newGroupName: newGroupName ?? this.newGroupName, + newDeleteMessagesAfterMilliseconds: newDeleteMessagesAfterMilliseconds ?? + this.newDeleteMessagesAfterMilliseconds, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map<String, Expression> toColumns(bool nullToAbsent) { + final map = <String, Expression>{}; + if (groupHistoryId.present) { + map['group_history_id'] = Variable<String>(groupHistoryId.value); + } + if (groupId.present) { + map['group_id'] = Variable<String>(groupId.value); + } + if (contactId.present) { + map['contact_id'] = Variable<int>(contactId.value); + } + if (affectedContactId.present) { + map['affected_contact_id'] = Variable<int>(affectedContactId.value); + } + if (oldGroupName.present) { + map['old_group_name'] = Variable<String>(oldGroupName.value); + } + if (newGroupName.present) { + map['new_group_name'] = Variable<String>(newGroupName.value); + } + if (newDeleteMessagesAfterMilliseconds.present) { + map['new_delete_messages_after_milliseconds'] = + Variable<int>(newDeleteMessagesAfterMilliseconds.value); + } + if (type.present) { + map['type'] = Variable<String>(type.value); + } + if (actionAt.present) { + map['action_at'] = Variable<DateTime>(actionAt.value); + } + if (rowid.present) { + map['rowid'] = Variable<int>(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupHistoriesCompanion(') + ..write('groupHistoryId: $groupHistoryId, ') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('affectedContactId: $affectedContactId, ') + ..write('oldGroupName: $oldGroupName, ') + ..write('newGroupName: $newGroupName, ') + ..write( + 'newDeleteMessagesAfterMilliseconds: $newDeleteMessagesAfterMilliseconds, ') + ..write('type: $type, ') + ..write('actionAt: $actionAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV7 extends GeneratedDatabase { + DatabaseAtV7(QueryExecutor e) : super(e); + late final Contacts contacts = Contacts(this); + late final Groups groups = Groups(this); + late final MediaFiles mediaFiles = MediaFiles(this); + late final Messages messages = Messages(this); + late final MessageHistories messageHistories = MessageHistories(this); + late final Reactions reactions = Reactions(this); + late final GroupMembers groupMembers = GroupMembers(this); + late final Receipts receipts = Receipts(this); + late final ReceivedReceipts receivedReceipts = ReceivedReceipts(this); + late final SignalIdentityKeyStores signalIdentityKeyStores = + SignalIdentityKeyStores(this); + late final SignalPreKeyStores signalPreKeyStores = SignalPreKeyStores(this); + late final SignalSenderKeyStores signalSenderKeyStores = + SignalSenderKeyStores(this); + late final SignalSessionStores signalSessionStores = + SignalSessionStores(this); + late final SignalContactPreKeys signalContactPreKeys = + SignalContactPreKeys(this); + late final SignalContactSignedPreKeys signalContactSignedPreKeys = + SignalContactSignedPreKeys(this); + late final MessageActions messageActions = MessageActions(this); + late final GroupHistories groupHistories = GroupHistories(this); + @override + Iterable<TableInfo<Table, Object?>> get allTables => + allSchemaEntities.whereType<TableInfo<Table, Object?>>(); + @override + List<DatabaseSchemaEntity> get allSchemaEntities => [ + contacts, + groups, + mediaFiles, + messages, + messageHistories, + reactions, + groupMembers, + receipts, + receivedReceipts, + signalIdentityKeyStores, + signalPreKeyStores, + signalSenderKeyStores, + signalSessionStores, + signalContactPreKeys, + signalContactSignedPreKeys, + messageActions, + groupHistories + ]; + @override + int get schemaVersion => 7; +} diff --git a/test/features/link_parser_test.dart b/test/features/link_parser_test.dart index 53f1c9f..ea637c7 100644 --- a/test/features/link_parser_test.dart +++ b/test/features/link_parser_test.dart @@ -96,6 +96,13 @@ void main() { image: 'https://pbs.twimg.com/media/ECF8Z5KWwAIBZ6o.jpg:large', vendor: Vendor.twitterPosting, ), + LinkParserTest( + title: 'twonly Public Launch', + desc: + 'After about a year of development, twonly is finally ready for its public launch.', + url: 'https://twonly.eu/en/blog/2026-public-launch.html', + image: 'https://twonly.eu/assets/blog/2026-public-launch.webp', + ), ]; for (final testCase in testCases) { From 76c56b06feea58cd00a016761f381383f83bedad Mon Sep 17 00:00:00 2001 From: otsmr <git@tsmr.eu> Date: Thu, 22 Jan 2026 23:58:33 +0100 Subject: [PATCH 10/17] fix #383 --- CHANGELOG.md | 1 + .../NotificationService.swift | 108 +++++++++++------- .../generated/app_localizations.dart | 18 ++- .../generated/app_localizations_de.dart | 9 +- .../generated/app_localizations_en.dart | 9 +- .../generated/app_localizations_sv.dart | 9 +- .../background.notifications.dart | 17 ++- 7 files changed, 112 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e222556..1bbdec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.0.89 +- Adds link preview to images - Adds option to manual focus in the camera - Adds support to switch between front and back cameras during video recording - Adds basic face filters diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index ab236bd..f8e68be 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -109,10 +109,10 @@ func getPushNotificationData(pushData: String) -> ( } else if pushUser != nil { return ( pushUser!.displayName, - getPushNotificationText(pushNotification: pushNotification).0, pushUser!.userID + getPushNotificationText(pushNotification: pushNotification, true).0, pushUser!.userID ) } else { - let content = getPushNotificationText(pushNotification: pushNotification) + let content = getPushNotificationText(pushNotification: pushNotification, false) return ( content.1, content.0, 1 ) @@ -205,57 +205,77 @@ func readFromKeychain(key: String) -> String? { return nil } -func getPushNotificationText(pushNotification: PushNotification) -> (String, String) { +func getPushNotificationText(pushNotification: PushNotification, userKnown: bool) -> (String, String) { let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language var pushNotificationText: [PushKind: String] = [:] - var title = "[Unknown]" + var title = "You" + var noTranslationFoundTitle = "You have a new message." + var noTranslationFoundBody = "Open twonly to learn more." // Define the messages based on the system language if systemLanguage.contains("de") { // German - title = "[Unbekannt]" - pushNotificationText = [ - .text: "hat eine Nachricht{inGroup} gesendet.", - .twonly: "hat ein twonly{inGroup} gesendet.", - .video: "hat ein Video{inGroup} gesendet.", - .image: "hat ein Bild{inGroup} gesendet.", - .audio: "hat eine Sprachnachricht{inGroup} gesendet.", - .contactRequest: "möchte sich mit dir vernetzen.", - .acceptRequest: "ist jetzt mit dir vernetzt.", - .storedMediaFile: "hat dein Bild gespeichert.", - .reaction: "hat auf dein Bild reagiert.", - .testNotification: "Das ist eine Testbenachrichtigung.", - .reopenedMedia: "hat dein Bild erneut geöffnet.", - .reactionToVideo: "hat mit {{content}} auf dein Video reagiert.", - .reactionToText: "hat mit {{content}} auf deinen Text reagiert.", - .reactionToImage: "hat mit {{content}} auf dein Bild reagiert.", - .reactionToAudio: "hat mit {{content}} auf deine Sprachnachricht reagiert.", - .response: "hat dir{inGroup} geantwortet.", - .addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt.", - ] - } else { // Default to English - pushNotificationText = [ - .text: "sent a message{inGroup}.", - .twonly: "sent a twonly{inGroup}.", - .video: "sent a video{inGroup}.", - .image: "sent an image{inGroup}.", - .audio: "sent a voice message{inGroup}.", - .contactRequest: "wants to connect with you.", - .acceptRequest: "is now connected with you.", - .storedMediaFile: "has stored your image.", - .reaction: "has reacted to your image.", - .testNotification: "This is a test notification.", - .reopenedMedia: "has reopened your image.", - .reactionToVideo: "has reacted with {{content}} to your video.", - .reactionToText: "has reacted with {{content}} to your text.", - .reactionToImage: "has reacted with {{content}} to your image.", - .reactionToAudio: "has reacted with {{content}} to your voice message.", - .response: "has responded{inGroup}.", - .addedToGroup: "has added you to \"{{content}}\"", - ] + title = "Du" + noTranslationFoundTitle = "Du hast eine neue Nachricht." + noTranslationFoundBody = "Öffne twonly um mehr zu erfahren." + if (userKnown) { + pushNotificationText = [ + .text: "hat eine Nachricht{inGroup} gesendet.", + .twonly: "hat ein twonly{inGroup} gesendet.", + .video: "hat ein Video{inGroup} gesendet.", + .image: "hat ein Bild{inGroup} gesendet.", + .audio: "hat eine Sprachnachricht{inGroup} gesendet.", + .contactRequest: "möchte sich mit dir vernetzen.", + .acceptRequest: "ist jetzt mit dir vernetzt.", + .storedMediaFile: "hat dein Bild gespeichert.", + .reaction: "hat auf dein Bild reagiert.", + .testNotification: "Das ist eine Testbenachrichtigung.", + .reopenedMedia: "hat dein Bild erneut geöffnet.", + .reactionToVideo: "hat mit {{content}} auf dein Video reagiert.", + .reactionToText: "hat mit {{content}} auf deinen Text reagiert.", + .reactionToImage: "hat mit {{content}} auf dein Bild reagiert.", + .reactionToAudio: "hat mit {{content}} auf deine Sprachnachricht reagiert.", + .response: "hat dir{inGroup} geantwortet.", + .addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt.", + ] + } else { + pushNotificationText = [ + .contactRequest: "hast eine neue Kontaktanfrage erhalten.", + ] + } + } else { + if (userKnown) { + pushNotificationText = [ + .text: "sent a message{inGroup}.", + .twonly: "sent a twonly{inGroup}.", + .video: "sent a video{inGroup}.", + .image: "sent an image{inGroup}.", + .audio: "sent a voice message{inGroup}.", + .contactRequest: "wants to connect with you.", + .acceptRequest: "is now connected with you.", + .storedMediaFile: "has stored your image.", + .reaction: "has reacted to your image.", + .testNotification: "This is a test notification.", + .reopenedMedia: "has reopened your image.", + .reactionToVideo: "has reacted with {{content}} to your video.", + .reactionToText: "has reacted with {{content}} to your text.", + .reactionToImage: "has reacted with {{content}} to your image.", + .reactionToAudio: "has reacted with {{content}} to your voice message.", + .response: "has responded{inGroup}.", + .addedToGroup: "has added you to \"{{content}}\"", + ] + } else { + pushNotificationText = [ + .contactRequest: "have received a new contact request.", + ] + } } var content = pushNotificationText[pushNotification.kind] ?? "" + if (content == "") { + title = noTranslationFoundTitle + body = noTranslationFoundBody + } if pushNotification.hasAdditionalContent { content.replace("{{content}}", with: pushNotification.additionalContent) diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 2a6f815..fb8ea1b 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2518,6 +2518,12 @@ abstract class AppLocalizations { /// **'wants to connect with you.'** String get notificationContactRequest; + /// No description provided for @notificationContactRequestUnknownUser. + /// + /// In en, this message translates to: + /// **'have received a new contact request.'** + String get notificationContactRequestUnknownUser; + /// No description provided for @notificationAcceptRequest. /// /// In en, this message translates to: @@ -2572,11 +2578,17 @@ abstract class AppLocalizations { /// **'has responded{inGroup}.'** String notificationResponse(Object inGroup); - /// No description provided for @notificationTitleUnknownUser. + /// No description provided for @notificationTitleUnknown. /// /// In en, this message translates to: - /// **'[Unknown]'** - String get notificationTitleUnknownUser; + /// **'You have a new message.'** + String get notificationTitleUnknown; + + /// No description provided for @notificationBodyUnknown. + /// + /// In en, this message translates to: + /// **'Open twonly to learn more.'** + String get notificationBodyUnknown; /// No description provided for @notificationCategoryMessageTitle. /// diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index c328e26..3a3c99a 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1380,6 +1380,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get notificationContactRequest => 'möchte sich mit dir vernetzen.'; + @override + String get notificationContactRequestUnknownUser => + 'hast eine neue Kontaktanfrage erhalten.'; + @override String get notificationAcceptRequest => 'ist jetzt mit dir vernetzt.'; @@ -1418,7 +1422,10 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get notificationTitleUnknownUser => '[Unbekannt]'; + String get notificationTitleUnknown => 'Du hast eine neue Nachricht.'; + + @override + String get notificationBodyUnknown => 'Öffne twonly um mehr zu erfahren.'; @override String get notificationCategoryMessageTitle => 'Nachrichten'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 13f17c7..fecc2ac 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1372,6 +1372,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get notificationContactRequest => 'wants to connect with you.'; + @override + String get notificationContactRequestUnknownUser => + 'have received a new contact request.'; + @override String get notificationAcceptRequest => 'is now connected with you.'; @@ -1410,7 +1414,10 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get notificationTitleUnknownUser => '[Unknown]'; + String get notificationTitleUnknown => 'You have a new message.'; + + @override + String get notificationBodyUnknown => 'Open twonly to learn more.'; @override String get notificationCategoryMessageTitle => 'Messages'; diff --git a/lib/src/localization/generated/app_localizations_sv.dart b/lib/src/localization/generated/app_localizations_sv.dart index 7b15afc..bcd7c52 100644 --- a/lib/src/localization/generated/app_localizations_sv.dart +++ b/lib/src/localization/generated/app_localizations_sv.dart @@ -1372,6 +1372,10 @@ class AppLocalizationsSv extends AppLocalizations { @override String get notificationContactRequest => 'wants to connect with you.'; + @override + String get notificationContactRequestUnknownUser => + 'have received a new contact request.'; + @override String get notificationAcceptRequest => 'is now connected with you.'; @@ -1410,7 +1414,10 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get notificationTitleUnknownUser => '[Unknown]'; + String get notificationTitleUnknown => 'You have a new message.'; + + @override + String get notificationBodyUnknown => 'Open twonly to learn more.'; @override String get notificationCategoryMessageTitle => 'Messages'; diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index 4c652d2..f7eaacc 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -99,9 +99,10 @@ Future<void> handlePushData(String pushDataB64) async { } } catch (e) { Log.error(e); + final lang = getLocalizations(); await customLocalPushNotification( - 'Du hast eine neue Nachricht.', - 'Öffne twonly um mehr zu erfahren.', + lang.notificationTitleUnknown, + lang.notificationBodyUnknown, ); } } @@ -186,16 +187,14 @@ Future<void> showLocalPushNotification( Future<void> showLocalPushNotificationWithoutUserId( PushNotification pushNotification, ) async { - String? body; - - body = getPushNotificationText(pushNotification); - final lang = getLocalizations(); - final title = lang.notificationTitleUnknownUser; + var title = lang.notificationTitleUnknown; + var body = lang.notificationBodyUnknown; - if (body == '') { - Log.error('No push notification type defined!'); + if (pushNotification.kind == PushKind.contactRequest) { + title = lang.you; + body = lang.notificationContactRequestUnknownUser; } final androidNotificationDetails = AndroidNotificationDetails( From 1b72ec5649f19df2dad33fbc1c5e37e37166c385 Mon Sep 17 00:00:00 2001 From: otsmr <git@tsmr.eu> Date: Fri, 23 Jan 2026 00:34:12 +0100 Subject: [PATCH 11/17] add to share menu #366 --- android/app/src/main/AndroidManifest.xml | 15 +++++++++++++++ ios/NotificationService/NotificationService.swift | 8 ++++---- ios/ShareExtension/Info.plist | 2 ++ lib/src/localization/translations | 2 +- .../camera_preview_controller_view.dart | 8 +------- 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index bc0b3a7..deed6bf 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -33,6 +33,21 @@ <data android:scheme="http" android:host="me.twonly.eu" /> <data android:scheme="https" /> </intent-filter> + <!-- Allow other apps to share links --> + <intent-filter> + <action android:name="android.intent.action.SEND" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:host="*" /> + </intent-filter> + <!-- Allow other apps to share links via plain text --> + <intent-filter> + <action android:name="android.intent.action.SEND" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:mimeType="text/plain" /> + </intent-filter> <intent-filter> <action android:name="android.intent.action.SEND" /> <category android:name="android.intent.category.DEFAULT" /> diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index f8e68be..5ec1185 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -109,10 +109,10 @@ func getPushNotificationData(pushData: String) -> ( } else if pushUser != nil { return ( pushUser!.displayName, - getPushNotificationText(pushNotification: pushNotification, true).0, pushUser!.userID + getPushNotificationText(pushNotification: pushNotification, userKnown: true).0, pushUser!.userID ) } else { - let content = getPushNotificationText(pushNotification: pushNotification, false) + let content = getPushNotificationText(pushNotification: pushNotification, userKnown: false) return ( content.1, content.0, 1 ) @@ -205,7 +205,7 @@ func readFromKeychain(key: String) -> String? { return nil } -func getPushNotificationText(pushNotification: PushNotification, userKnown: bool) -> (String, String) { +func getPushNotificationText(pushNotification: PushNotification, userKnown: Bool) -> (String, String) { let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language var pushNotificationText: [PushKind: String] = [:] @@ -274,7 +274,7 @@ func getPushNotificationText(pushNotification: PushNotification, userKnown: bool var content = pushNotificationText[pushNotification.kind] ?? "" if (content == "") { title = noTranslationFoundTitle - body = noTranslationFoundBody + content = noTranslationFoundBody } if pushNotification.hasAdditionalContent { diff --git a/ios/ShareExtension/Info.plist b/ios/ShareExtension/Info.plist index 3e8521b..5edbc20 100644 --- a/ios/ShareExtension/Info.plist +++ b/ios/ShareExtension/Info.plist @@ -22,6 +22,8 @@ <key>NSExtensionActivationRule</key> <dict> + <key>NSExtensionActivationSupportsWebURLWithMaxCount</key> + <integer>1</integer> <key>NSExtensionActivationSupportsWebURLWithMaxCount</key> <integer>1</integer> <key>NSExtensionActivationSupportsImageWithMaxCount</key> diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 20f3c2f..9d04e9e 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 20f3c2f0a49e4c9be452ecbc84d98054c92974e1 +Subproject commit 9d04e9e1d0cdba8f1be4b0cbba341706c3cffac9 diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index 870a06b..c7dbfa1 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:camera/camera.dart'; import 'package:clock/clock.dart'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_android_volume_keydown/flutter_android_volume_keydown.dart'; @@ -353,12 +352,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { sendToGroup: widget.sendToGroup, mediaFileService: mediaFileService, mainCameraController: mc, - // previewLink: mc.sharedLinkForPreview, - previewLink: kDebugMode - ? Uri.parse( - 'https://mastodon.social/@islieb/115883317936171927', - ) - : mc.sharedLinkForPreview, + previewLink: mc.sharedLinkForPreview, ), transitionsBuilder: (context, animation, secondaryAnimation, child) { return child; From 3aa35a95c5a70842be98573a715da77d0f58fa9b Mon Sep 17 00:00:00 2001 From: otsmr <git@tsmr.eu> Date: Fri, 23 Jan 2026 00:35:07 +0100 Subject: [PATCH 12/17] show emojis above the link --- lib/src/views/chats/media_viewer.view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index aa7f821..645f93b 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -690,6 +690,8 @@ class _MediaViewerViewState extends State<MediaViewerView> { ), ), ), + if (currentMessage != null) + AdditionalMessageContent(currentMessage!), if (currentMedia != null) ReactionButtons( show: showShortReactions, @@ -708,8 +710,6 @@ class _MediaViewerViewState extends State<MediaViewerView> { Positioned.fill( child: EmojiFloatWidget(key: emojiKey), ), - if (currentMessage != null) - AdditionalMessageContent(currentMessage!), ], ), ), From 8e8027c63c18b66b1fa73847aaaa43bdac60bf25 Mon Sep 17 00:00:00 2001 From: otsmr <git@tsmr.eu> Date: Fri, 23 Jan 2026 00:43:00 +0100 Subject: [PATCH 13/17] fix #384 --- lib/src/views/chats/chat_list_components/group_list_item.dart | 1 + lib/src/views/components/blink.component.dart | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index 7a6cbbb..763e389 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -135,6 +135,7 @@ class _UserListItem extends State<GroupListItem> { _previewMessages.where((x) => x.type == MessageType.media).toList(); if (msgs.isNotEmpty && msgs.first.type == MessageType.media && + !msgs.first.isDeletedFromSender && msgs.first.senderId != null && msgs.first.openedAt == null) { _hasNonOpenedMediaFile = true; diff --git a/lib/src/views/components/blink.component.dart b/lib/src/views/components/blink.component.dart index 718cfb4..b114b4b 100644 --- a/lib/src/views/components/blink.component.dart +++ b/lib/src/views/components/blink.component.dart @@ -6,8 +6,8 @@ class BlinkWidget extends StatefulWidget { required this.child, required this.enabled, super.key, - this.blinkDuration = const Duration(milliseconds: 2500), - this.interval = const Duration(milliseconds: 100), + this.blinkDuration = const Duration(milliseconds: 2000), + this.interval = const Duration(milliseconds: 300), this.visibleOpacity = 1.0, this.hiddenOpacity = 0.05, }); From 6720604fc3ce8fa27627a4c3a96ef59876bbfc56 Mon Sep 17 00:00:00 2001 From: otsmr <git@tsmr.eu> Date: Fri, 23 Jan 2026 15:09:22 +0100 Subject: [PATCH 14/17] fix #386 --- .../camera_preview.dart | 3 +- .../camera_preview_controller_view.dart | 48 +++++++++---------- .../main_camera_controller.dart | 16 ++++++- 3 files changed, 39 insertions(+), 28 deletions(-) diff --git a/lib/src/views/camera/camera_preview_components/camera_preview.dart b/lib/src/views/camera/camera_preview_components/camera_preview.dart index 10783db..1f196ea 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview.dart @@ -58,7 +58,8 @@ class MainCameraPreview extends StatelessWidget { ), ), ), - if (mainCameraController.focusPointOffset != null) + if (mainCameraController.focusPointOffset != null && + !mainCameraController.isSharePreviewIsShown) AspectRatio( aspectRatio: 9 / 16, child: ClipRect( diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index c7dbfa1..c3064ee 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -109,12 +109,10 @@ class CameraPreviewView extends StatefulWidget { } class _CameraPreviewViewState extends State<CameraPreviewView> { - bool _sharePreviewIsShown = false; bool _galleryLoadedImageIsShown = false; bool _showSelfieFlash = false; double _basePanY = 0; double _baseScaleFactor = 0; - bool _isVideoRecording = false; bool _hasAudioPermission = true; DateTime? _videoRecordingStarted; Timer? _videoRecordingTimer; @@ -270,10 +268,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { } Future<void> takePicture() async { - if (_sharePreviewIsShown || _isVideoRecording) return; + if (mc.isSharePreviewIsShown || mc.isVideoRecording) return; setState(() { - _sharePreviewIsShown = true; + mc.isSharePreviewIsShown = true; }); if (mc.selectedCameraDetails.isFlashOn) { if (isFront) { @@ -306,7 +304,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { return; } setState(() { - _sharePreviewIsShown = false; + mc.isSharePreviewIsShown = false; }); } @@ -363,7 +361,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { ) as bool?; if (mounted) { setState(() { - _sharePreviewIsShown = false; + mc.isSharePreviewIsShown = false; _showSelfieFlash = false; }); } @@ -413,7 +411,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { Future<void> pickImageFromGallery() async { setState(() { _galleryLoadedImageIsShown = true; - _sharePreviewIsShown = true; + mc.isSharePreviewIsShown = true; }); final picker = ImagePicker(); final pickedFile = await picker.pickMedia(); @@ -456,7 +454,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { } setState(() { _galleryLoadedImageIsShown = false; - _sharePreviewIsShown = false; + mc.isSharePreviewIsShown = false; }); } @@ -496,7 +494,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { return; } setState(() { - _isVideoRecording = true; + mc.isVideoRecording = true; }); try { @@ -516,11 +514,11 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { }); setState(() { _videoRecordingStarted = clock.now(); - _isVideoRecording = true; + mc.isVideoRecording = true; }); } on CameraException catch (e) { setState(() { - _isVideoRecording = false; + mc.isVideoRecording = false; }); _showCameraException(e); return; @@ -535,7 +533,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { setState(() { _videoRecordingStarted = null; - _isVideoRecording = false; + mc.isVideoRecording = false; }); if (mc.cameraController == null || @@ -544,7 +542,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { } setState(() { - _sharePreviewIsShown = true; + mc.isSharePreviewIsShown = true; }); try { @@ -630,23 +628,23 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { ), ), ), - if (!_sharePreviewIsShown && + if (!mc.isSharePreviewIsShown && widget.sendToGroup != null && - !_isVideoRecording) + !mc.isVideoRecording) ShowTitleText( title: widget.sendToGroup!.groupName, desc: context.lang.cameraPreviewSendTo, ), - if (!_sharePreviewIsShown && + if (!mc.isSharePreviewIsShown && mc.sharedLinkForPreview != null && - !_isVideoRecording) + !mc.isVideoRecording) ShowTitleText( title: mc.sharedLinkForPreview?.host ?? '', desc: 'Link', isLink: true, ), - if (!_sharePreviewIsShown && - !_isVideoRecording && + if (!mc.isSharePreviewIsShown && + !mc.isVideoRecording && !widget.hideControllers) Positioned( right: 5, @@ -702,7 +700,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { ), ), ), - if (!_sharePreviewIsShown && !widget.hideControllers) + if (!mc.isSharePreviewIsShown && !widget.hideControllers) Positioned( bottom: 30, left: 0, @@ -713,7 +711,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { children: [ if (mc.cameraController!.value.isInitialized && mc.selectedCameraDetails.isZoomAble && - !_isVideoRecording) + !mc.isVideoRecording) SizedBox( width: 120, child: CameraZoomButtons( @@ -729,7 +727,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (!_isVideoRecording) + if (!mc.isVideoRecording) GestureDetector( onTap: pressSideButtonLeft, child: Align( @@ -765,7 +763,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { shape: BoxShape.circle, border: Border.all( width: 7, - color: _isVideoRecording + color: mc.isVideoRecording ? Colors.red : Colors.white, ), @@ -774,7 +772,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { ), ), ), - if (!_isVideoRecording) + if (!mc.isVideoRecording) if (isFront) GestureDetector( onTap: pressSideButtonRight, @@ -813,7 +811,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { videoRecordingStarted: _videoRecordingStarted, maxVideoRecordingTime: maxVideoRecordingTime, ), - if (!_sharePreviewIsShown && widget.sendToGroup != null || + if (!mc.isSharePreviewIsShown && widget.sendToGroup != null || widget.hideControllers) Positioned( left: 5, diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart index 50bc290..09675cd 100644 --- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart @@ -55,6 +55,9 @@ class MainCameraController { GlobalKey cameraPreviewKey = GlobalKey(); bool isSelectingFaceFilters = false; + bool isSharePreviewIsShown = false; + bool isVideoRecording = false; + Uri? sharedLinkForPreview; void setSharedLinkForPreview(Uri url) { @@ -132,7 +135,9 @@ class MainCameraController { await cameraController?.startImageStream(_processCameraImage); } else { await HapticFeedback.lightImpact(); + await cameraController?.stopImageStream(); await cameraController?.setDescription(gCameras[cameraId]); + await cameraController?.startImageStream(_processCameraImage); } await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor); @@ -177,8 +182,12 @@ class MainCameraController { setState(); await HapticFeedback.lightImpact(); - await cameraController?.setFocusPoint(Offset(dx, dy)); - await cameraController?.setFocusMode(FocusMode.auto); + try { + await cameraController?.setFocusPoint(Offset(dx, dy)); + await cameraController?.setFocusMode(FocusMode.auto); + } catch (e) { + Log.error(e); + } focusPointOffset = null; setState(); @@ -204,6 +213,9 @@ class MainCameraController { }; void _processCameraImage(CameraImage image) { + if (isVideoRecording || isSharePreviewIsShown) { + return; + } final inputImage = _inputImageFromCameraImage(image); if (inputImage == null) return; _processBarcode(inputImage); From 9813698e59866357766c559e27383289529bdae5 Mon Sep 17 00:00:00 2001 From: otsmr <git@tsmr.eu> Date: Fri, 23 Jan 2026 16:34:01 +0100 Subject: [PATCH 15/17] fix #380 --- CHANGELOG.md | 1 + .../camera_preview_controller_view.dart | 4 +- .../main_camera_controller.dart | 5 +- .../save_to_gallery.dart | 4 +- .../share_image_contact_selection.view.dart | 23 ++++---- .../views/camera/share_image_editor.view.dart | 59 +++++++++++++------ .../camera/share_image_editor/image_item.dart | 34 +++-------- .../layers/background.layer.dart | 46 +++++++++++---- lib/src/views/chats/chat_list.view.dart | 37 ++++++++---- .../memories/memories_photo_slider.view.dart | 6 +- 10 files changed, 134 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bbdec6..5aae546 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Adds support to switch between front and back cameras during video recording - Adds basic face filters - Improves image editor, like emojis or text under a drawing can be moved +- Improves speed after taking a picture - Fixes issue with emojis disappearing in the image editor ## 0.0.86 diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index c3064ee..34e1e78 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -309,7 +309,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { } Future<bool> pushMediaEditor( - ScreenshotImage? imageBytes, + ScreenshotImage? screenshotImage, File? videoFilePath, { bool sharedFromGallery = false, MediaType? mediaType, @@ -345,7 +345,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> { PageRouteBuilder( opaque: false, pageBuilder: (context, a1, a2) => ShareImageEditorView( - imageBytesFuture: imageBytes, + screenshotImage: screenshotImage, sharedFromGallery: sharedFromGallery, sendToGroup: widget.sendToGroup, mediaFileService: mediaFileService, diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart index 09675cd..a6de9e9 100644 --- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart @@ -92,7 +92,10 @@ class MainCameraController { } final cameraControllerTemp = cameraController; cameraController = null; - await cameraControllerTemp?.dispose(); + // prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.) + Future.delayed(const Duration(milliseconds: 100), () async { + await cameraControllerTemp?.dispose(); + }); initCameraStarted = false; selectedCameraDetails = SelectedCameraDetails(); } diff --git a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart index 85c57d4..282ccc5 100644 --- a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:clock/clock.dart'; import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; @@ -8,6 +7,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/screenshot.dart'; class SaveToGalleryButton extends StatefulWidget { const SaveToGalleryButton({ @@ -17,7 +17,7 @@ class SaveToGalleryButton extends StatefulWidget { this.storeImageAsOriginal, super.key, }); - final Future<Uint8List?> Function()? storeImageAsOriginal; + final Future<ScreenshotImage?> Function()? storeImageAsOriginal; final bool displayButtonLabel; final MediaFileService mediaService; final bool isLoading; diff --git a/lib/src/views/camera/share_image_contact_selection.view.dart b/lib/src/views/camera/share_image_contact_selection.view.dart index 7cdf082..b93fadb 100644 --- a/lib/src/views/camera/share_image_contact_selection.view.dart +++ b/lib/src/views/camera/share_image_contact_selection.view.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:collection'; -import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; @@ -14,7 +13,9 @@ import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/screenshot.dart'; import 'package:twonly/src/views/camera/share_image_contact_selection/best_friends_selector.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/background.layer.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/headline.dart'; @@ -30,7 +31,7 @@ class ShareImageView extends StatefulWidget { }); final HashSet<String> selectedGroupIds; final void Function(String, bool) updateSelectedGroupIds; - final Future<Uint8List?>? mediaStoreFuture; + final Future<ScreenshotImage?>? mediaStoreFuture; final MediaFileService mediaFileService; final AdditionalMessageData? additionalData; @@ -46,7 +47,7 @@ class _ShareImageView extends State<ShareImageView> { bool sendingImage = false; bool mediaStoreFutureReady = false; - Uint8List? _imageBytes; + ScreenshotImage? _screenshotImage; bool hideArchivedUsers = true; final TextEditingController searchUserName = TextEditingController(); late StreamSubscription<List<Group>> allGroupSub; @@ -69,7 +70,7 @@ class _ShareImageView extends State<ShareImageView> { Future<void> initAsync() async { if (widget.mediaStoreFuture != null) { - _imageBytes = await widget.mediaStoreFuture; + _screenshotImage = await widget.mediaStoreFuture; } mediaStoreFutureReady = true; if (!mounted) return; @@ -247,10 +248,11 @@ class _ShareImageView extends State<ShareImageView> { mainAxisAlignment: MainAxisAlignment.end, children: [ if (widget.mediaFileService.mediaFile.type == MediaType.image && - _imageBytes != null && + _screenshotImage?.image != null && gUser.showShowImagePreviewWhenSending) SizedBox( height: 100, + width: 100 * 9 / 16, child: Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( @@ -261,7 +263,9 @@ class _ShareImageView extends State<ShareImageView> { ), child: ClipRRect( borderRadius: BorderRadius.circular(12), - child: Image.memory(_imageBytes!), + child: CustomPaint( + painter: UiImagePainter(_screenshotImage!.image!), + ), ), ), ), @@ -286,6 +290,7 @@ class _ShareImageView extends State<ShareImageView> { sendingImage = true; }); + // in case mediaStoreFutureReady is ready, the image is stored in the originalPath await insertMediaFileInMessagesTable( widget.mediaFileService, widget.selectedGroupIds.toList(), @@ -294,12 +299,6 @@ class _ShareImageView extends State<ShareImageView> { if (context.mounted) { Navigator.pop(context, true); - // if (widget.preselectedUser != null) { - // Navigator.pop(context, true); - // } else { - // Navigator.popUntil(context, (route) => route.isFirst, true); - // globalUpdateOfHomeViewPageIndex(1); - // } } }, style: ButtonStyle( diff --git a/lib/src/views/camera/share_image_editor.view.dart b/lib/src/views/camera/share_image_editor.view.dart index a27f826..387e749 100644 --- a/lib/src/views/camera/share_image_editor.view.dart +++ b/lib/src/views/camera/share_image_editor.view.dart @@ -39,13 +39,13 @@ class ShareImageEditorView extends StatefulWidget { const ShareImageEditorView({ required this.sharedFromGallery, required this.mediaFileService, + this.screenshotImage, this.previewLink, super.key, - this.imageBytesFuture, this.sendToGroup, this.mainCameraController, }); - final ScreenshotImage? imageBytesFuture; + final ScreenshotImage? screenshotImage; final Group? sendToGroup; final bool sharedFromGallery; final MediaFileService mediaFileService; @@ -64,7 +64,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> { double widthRatio = 1; double heightRatio = 1; double pixelRatio = 1; - Uint8List? imageBytes; VideoPlayerController? videoController; ImageItem currentImage = ImageItem(); ScreenshotController screenshotController = ScreenshotController(); @@ -93,8 +92,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> { if (widget.mediaFileService.mediaFile.type == MediaType.image || widget.mediaFileService.mediaFile.type == MediaType.gif) { - if (widget.imageBytesFuture != null) { - loadImage(widget.imageBytesFuture!); + if (widget.screenshotImage != null) { + loadImage(widget.screenshotImage!); } else { if (widget.mediaFileService.tempPath.existsSync()) { loadImage(ScreenshotImage(file: widget.mediaFileService.tempPath)); @@ -435,8 +434,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> { Future<ScreenshotImage?> getEditedImageBytes() async { if (layers.length == 1) { if (layers.first is BackgroundLayerData) { - final image = (layers.first as BackgroundLayerData).image.bytes; - return ScreenshotImage(imageBytes: image); + return (layers.first as BackgroundLayerData).image.image; } } @@ -465,7 +463,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> { return image; } - Future<Uint8List?> storeImageAsOriginal() async { + Future<ScreenshotImage?> storeImageAsOriginal() async { if (mediaService.overlayImagePath.existsSync()) { mediaService.overlayImagePath.deleteSync(); } @@ -477,11 +475,16 @@ class _ShareImageEditorView extends State<ShareImageEditorView> { mediaService.originalPath.deleteSync(); } } - var bytes = imageBytes; + ScreenshotImage? image; + var bytes = await widget.screenshotImage?.getBytes(); if (media.type == MediaType.gif) { - mediaService.originalPath.writeAsBytesSync(imageBytes!.toList()); + if (bytes != null) { + mediaService.originalPath.writeAsBytesSync(bytes.toList()); + } else { + Log.error('Could not load image bytes for gif!'); + } } else { - final image = await getEditedImageBytes(); + image = await getEditedImageBytes(); if (image == null) return null; bytes = await image.getBytes(); if (bytes == null) { @@ -496,16 +499,38 @@ class _ShareImageEditorView extends State<ShareImageEditorView> { Log.error('MediaType not supported: ${media.type}'); } } - - return bytes; + return image; } - Future<void> loadImage(ScreenshotImage imageBytesFuture) async { - imageBytes = await imageBytesFuture.getBytes(); - // store this image so it can be used as a draft in case the app is restarted + Future<void> storeIoImageAsDraft(ScreenshotImage screenshotImage) async { + final imageBytes = await screenshotImage.getBytes(); mediaService.originalPath.writeAsBytesSync(imageBytes!.toList()); + } + + Future<void> loadImage(ScreenshotImage screenshotImage) async { + if (screenshotImage.image == null && + screenshotImage.imageBytes == null && + screenshotImage.imageBytesFuture != null) { + // this ensures that the imageBytes are defined + await storeIoImageAsDraft(screenshotImage); + } else { + // store this image so it can be used as a draft in case the app is restarted + unawaited(storeIoImageAsDraft(screenshotImage)); + } + + if (screenshotImage.image == null) { + final imageBytes = await screenshotImage.getBytes(); + if (imageBytes != null) { + screenshotImage.image = await decodeImageFromList(imageBytes); + } + } + if (screenshotImage.image == null) { + Log.error('Could not load screenshotImage.image'); + return; + } + + currentImage.load(screenshotImage); - await currentImage.load(imageBytes); if (isDisposed) return; if (!context.mounted) return; diff --git a/lib/src/views/camera/share_image_editor/image_item.dart b/lib/src/views/camera/share_image_editor/image_item.dart index 6468ca7..c2511d5 100755 --- a/lib/src/views/camera/share_image_editor/image_item.dart +++ b/lib/src/views/camera/share_image_editor/image_item.dart @@ -1,36 +1,18 @@ import 'dart:async'; -import 'dart:typed_data'; -import 'package:flutter/material.dart'; +import 'package:twonly/src/utils/screenshot.dart'; class ImageItem { - ImageItem([dynamic image]) { - if (image != null) unawaited(load(image)); - } + ImageItem(); int width = 1; int height = 1; - Uint8List bytes = Uint8List.fromList([]); + ScreenshotImage? image; Completer<bool> loader = Completer<bool>(); - Future<void> load(dynamic image) async { - loader = Completer<bool>(); - - if (image is ImageItem) { - bytes = image.bytes; - - height = image.height; - width = image.width; - - return loader.complete(true); - } else if (image is Uint8List) { - bytes = image; - final decodedImage = await decodeImageFromList(bytes); - - height = decodedImage.height; - width = decodedImage.width; - - return loader.complete(true); - } else { - return loader.complete(false); + void load(ScreenshotImage img) { + image = img; + if (image?.image != null) { + height = image!.image!.height; + width = image!.image!.width; } } } diff --git a/lib/src/views/camera/share_image_editor/layers/background.layer.dart b/lib/src/views/camera/share_image_editor/layers/background.layer.dart index f12816c..ecd3ab7 100755 --- a/lib/src/views/camera/share_image_editor/layers/background.layer.dart +++ b/lib/src/views/camera/share_image_editor/layers/background.layer.dart @@ -1,3 +1,5 @@ +import 'dart:ui' as ui; + import 'package:flutter/material.dart'; import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; @@ -15,24 +17,46 @@ class BackgroundLayer extends StatefulWidget { } class _BackgroundLayerState extends State<BackgroundLayer> { + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.layerData.imageLoaded = true; + }); + super.initState(); + } + @override Widget build(BuildContext context) { + final scImage = widget.layerData.image.image; + if (scImage == null || scImage.image == null) return Container(); return Container( width: widget.layerData.image.width.toDouble(), height: widget.layerData.image.height.toDouble(), - // color: Theme.of(context).colorScheme.surface, padding: EdgeInsets.zero, - child: Image.memory( - widget.layerData.image.bytes, - frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { - if (wasSynchronouslyLoaded || frame != null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - widget.layerData.imageLoaded = true; - }); - } - return child; - }, + color: Colors.green, + child: CustomPaint( + painter: UiImagePainter(scImage.image!), ), ); } } + +class UiImagePainter extends CustomPainter { + UiImagePainter(this.image); + final ui.Image image; + + @override + void paint(Canvas canvas, Size size) { + canvas.drawImageRect( + image, + Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()), + Rect.fromLTWH(0, 0, size.width, size.height), + Paint(), + ); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index 9bcf0ed..e15f2d0 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -314,21 +314,32 @@ class _ChatListViewState extends State<ChatListView> { child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ - IconButton.filled( + Material( + elevation: 3, + shape: const CircleBorder(), color: context.color.primary, - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const PublicProfileView(); - }, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const PublicProfileView(); + }, + ), + ); + }, + child: SizedBox( + width: 45, + height: 45, + child: Center( + child: FaIcon( + FontAwesomeIcons.qrcode, + color: isDarkMode(context) ? Colors.black : Colors.white, + ), ), - ); - }, - icon: FaIcon( - FontAwesomeIcons.qrcode, - color: isDarkMode(context) ? Colors.black : Colors.white, + ), ), ), const SizedBox(height: 12), diff --git a/lib/src/views/memories/memories_photo_slider.view.dart b/lib/src/views/memories/memories_photo_slider.view.dart index 2b1c9b4..65c16ce 100644 --- a/lib/src/views/memories/memories_photo_slider.view.dart +++ b/lib/src/views/memories/memories_photo_slider.view.dart @@ -105,7 +105,11 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> { return; } - orgMediaService.storedPath.copySync(newMediaService.originalPath.path); + if (orgMediaService.storedPath.existsSync()) { + orgMediaService.storedPath.copySync(newMediaService.originalPath.path); + } else if (orgMediaService.tempPath.existsSync()) { + orgMediaService.tempPath.copySync(newMediaService.originalPath.path); + } if (!mounted) return; From fa767977dd8c031f7cbcfa5cda4d35d44841b6d1 Mon Sep 17 00:00:00 2001 From: otsmr <git@tsmr.eu> Date: Fri, 23 Jan 2026 16:39:27 +0100 Subject: [PATCH 16/17] fix #382 --- lib/src/services/api.service.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 8ba0a5c..7c3c395 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -26,6 +26,7 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart as server; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/server_messages.dart'; import 'package:twonly/src/services/api/utils.dart'; @@ -319,6 +320,12 @@ class ApiService { return user; }); globalCallbackUpdatePlan(planFromString(authenticated.plan)); + + // this was triggered by apiService.ipaPurchase, so call the onAuthenticated again + if (isAuthenticated) { + // Trigger the re-upload from images, after Plan change, in case the limit was reached before... + unawaited(finishStartedPreprocessing()); + } } } if (res.isError) { From f93a337aa99884540792f69a947be19171554c59 Mon Sep 17 00:00:00 2001 From: otsmr <git@tsmr.eu> Date: Fri, 23 Jan 2026 16:57:29 +0100 Subject: [PATCH 17/17] small bug fixes --- .../mediafiles/mediafile.service.dart | 105 +++++++++--------- .../main_camera_controller.dart | 26 +++-- .../views/camera/share_image_editor.view.dart | 1 - lib/src/views/chats/media_viewer.view.dart | 3 +- .../user_study_questionnaire.view.dart | 2 + 5 files changed, 77 insertions(+), 60 deletions(-) diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index 2901006..2528ea0 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -24,66 +24,71 @@ class MediaFileService { } static Future<void> purgeTempFolder() async { - final tempDirectory = MediaFileService.buildDirectoryPath( - 'tmp', - globalApplicationSupportDirectory, - ); + try { + final tempDirectory = MediaFileService.buildDirectoryPath( + 'tmp', + globalApplicationSupportDirectory, + ); - final files = tempDirectory.listSync(); - for (final file in files) { - final mediaId = basename(file.path).split('.').first; + final files = tempDirectory.listSync(); + for (final file in files) { + final mediaId = basename(file.path).split('.').first; - // in case the mediaID is unknown the file will be deleted - var delete = true; + // in case the mediaID is unknown the file will be deleted + var delete = true; - final service = await MediaFileService.fromMediaId(mediaId); + final service = await MediaFileService.fromMediaId(mediaId); - if (service != null) { - if (service.mediaFile.isDraftMedia) { - delete = false; - } - - final messages = - await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); - - // in case messages in empty the file will be deleted, as delete is true by default - - for (final message in messages) { - if (service.mediaFile.type == MediaType.audio) { - delete = false; // do not delete voice messages - } - - if (message.openedAt == null) { - // Message was not yet opened from all persons, so wait... + if (service != null) { + if (service.mediaFile.isDraftMedia) { delete = false; - } else if (service.mediaFile.requiresAuthentication || - service.mediaFile.displayLimitInMilliseconds != null) { - // Message was opened by all persons, and they can not reopen the image. - // This branch will prevent to reach the next if condition, with would otherwise store the image for two days - // delete = true; // do not overwrite a previous delete = false - // this is just to make it easier to understand :) - } else if (message.openedAt! - .isAfter(clock.now().subtract(const Duration(days: 2)))) { - // In case the image was opened, but send with unlimited time or no authentication. - if (message.senderId == null) { - delete = false; - } else { - // Check weather the image was send in a group. Then the images is preserved for two days in case another person stores the image. - // This also allows to reopen this image for two days. - final group = await twonlyDB.groupsDao.getGroup(message.groupId); - if (group != null && !group.isDirectChat) { - delete = false; - } + } + + final messages = + await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); + + // in case messages in empty the file will be deleted, as delete is true by default + + for (final message in messages) { + if (service.mediaFile.type == MediaType.audio) { + delete = false; // do not delete voice messages + } + + if (message.openedAt == null) { + // Message was not yet opened from all persons, so wait... + delete = false; + } else if (service.mediaFile.requiresAuthentication || + service.mediaFile.displayLimitInMilliseconds != null) { + // Message was opened by all persons, and they can not reopen the image. + // This branch will prevent to reach the next if condition, with would otherwise store the image for two days + // delete = true; // do not overwrite a previous delete = false + // this is just to make it easier to understand :) + } else if (message.openedAt! + .isAfter(clock.now().subtract(const Duration(days: 2)))) { + // In case the image was opened, but send with unlimited time or no authentication. + if (message.senderId == null) { + delete = false; + } else { + // Check weather the image was send in a group. Then the images is preserved for two days in case another person stores the image. + // This also allows to reopen this image for two days. + final group = + await twonlyDB.groupsDao.getGroup(message.groupId); + if (group != null && !group.isDirectChat) { + delete = false; + } + } + // In case the app was send in a direct chat, then it can be deleted. } - // In case the app was send in a direct chat, then it can be deleted. } } - } - if (delete) { - Log.info('Purging media file $mediaId'); - file.deleteSync(); + if (delete) { + Log.info('Purging media file $mediaId'); + file.deleteSync(); + } } + } catch (e) { + Log.error(e); } } diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart index a6de9e9..cfbef1a 100644 --- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart @@ -120,10 +120,6 @@ class MainCameraController { } selectedCameraDetails.isZoomAble = false; - if (selectedCameraDetails.cameraId != cameraId) { - // switched camera so reset the scaleFactor - selectedCameraDetails.scaleFactor = 1; - } if (cameraController == null) { cameraController = CameraController( @@ -136,14 +132,28 @@ class MainCameraController { ); await cameraController?.initialize(); await cameraController?.startImageStream(_processCameraImage); + await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor); } else { - await HapticFeedback.lightImpact(); - await cameraController?.stopImageStream(); + try { + if (!isVideoRecording) { + await cameraController?.stopImageStream(); + } + } catch (e) { + Log.info(e); + } + selectedCameraDetails.scaleFactor = 1; + + await cameraController?.setZoomLevel(1); await cameraController?.setDescription(gCameras[cameraId]); - await cameraController?.startImageStream(_processCameraImage); + try { + if (!isVideoRecording) { + await cameraController?.startImageStream(_processCameraImage); + } + } catch (e) { + Log.info(e); + } } - await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor); await cameraController ?.lockCaptureOrientation(DeviceOrientation.portraitUp); await cameraController?.setFlashMode( diff --git a/lib/src/views/camera/share_image_editor.view.dart b/lib/src/views/camera/share_image_editor.view.dart index 387e749..164fcf2 100644 --- a/lib/src/views/camera/share_image_editor.view.dart +++ b/lib/src/views/camera/share_image_editor.view.dart @@ -5,7 +5,6 @@ import 'dart:collection'; import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index 645f93b..c20edfd 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -92,8 +92,9 @@ class _MediaViewerViewState extends State<MediaViewerView> { _noScreenshot.screenshotOn(); _subscription.cancel(); downloadStateListener?.cancel(); - videoController?.dispose(); + final tmp = videoController; videoController = null; + tmp?.dispose(); super.dispose(); } diff --git a/lib/src/views/user_study/user_study_questionnaire.view.dart b/lib/src/views/user_study/user_study_questionnaire.view.dart index bfdf4d1..7058c5c 100644 --- a/lib/src/views/user_study/user_study_questionnaire.view.dart +++ b/lib/src/views/user_study/user_study_questionnaire.view.dart @@ -1,5 +1,6 @@ // ignore_for_file: avoid_dynamic_calls +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -30,6 +31,7 @@ class _UserStudyQuestionnaireState extends State<UserStudyQuestionnaire> { 'comp_knowledge': null, 'security_knowledge': null, 'messengers': [], + 'is_release_mode': kReleaseMode, }; final List<String> _messengerOptions = [